@@ -2791,6 +2791,7 @@ out HashSet<string> badFonts
27912791 }
27922792 }
27932793 var sb = new StringBuilder ( ) ;
2794+ var normalFacesAdded = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
27942795 foreach ( var font in _fontsUsedInBook . OrderBy ( x => x . ToString ( ) ) )
27952796 {
27962797 if ( badFonts . Contains ( font . fontFamily ) )
@@ -2816,7 +2817,14 @@ out HashSet<string> badFonts
28162817 // The fonts.css file is stored in a subfolder as are the font files. They are in different
28172818 // subfolders, and the reference to the font file has to take the relative path to fonts.css
28182819 // into account.
2819- AddFontFace ( sb , font , group , "../" + kFontsFolder + "/" , true ) ;
2820+ AddFontFace (
2821+ sb ,
2822+ font ,
2823+ group ,
2824+ normalFacesAdded ,
2825+ "../" + kFontsFolder + "/" ,
2826+ true
2827+ ) ;
28202828 }
28212829 }
28222830 if (
@@ -2889,26 +2897,127 @@ internal static void AddFontFace(
28892897 StringBuilder sb ,
28902898 PublishHelper . FontInfo font ,
28912899 FontGroup group ,
2900+ HashSet < string > normalFacesAdded ,
28922901 string relativePathFromCss = "" ,
28932902 bool sanitizeFileName = false
28942903 )
28952904 {
2896- var weight = font . fontWeight == "700" ? "bold" : "normal" ;
2897- string path = null ;
2898- if ( font . fontStyle == "italic" && font . fontWeight == "700" )
2899- path = group . BoldItalic ;
2900- if ( string . IsNullOrEmpty ( path ) && font . fontStyle == "italic" )
2901- path = group . Italic ;
2902- if ( string . IsNullOrEmpty ( path ) && font . fontWeight == "700" )
2903- path = group . Bold ;
2904- if ( string . IsNullOrEmpty ( path ) )
2905- path = group . Normal ;
2905+ // What we are doing here (See BL-15558)
2906+ // On the one hand, we have the fonts that the html/css call for.
2907+ // On the other hand, we have the actual fonts (not typefaces, actual "fonts")
2908+ // that are available on this machine. When we "add a font face" here, we are
2909+ // creating a css rule that points to an actual font file that will be embedded.
2910+ // If the font face we want isn't available, the browser can synthesize one.
2911+ // So for example if you want italic, but we only have normal, that's fine we
2912+ // have no italic font face to add, so we just add the normal one and the browser will
2913+ // do its best. This is what is needed for WYSIWYG, because that is what is happening
2914+ // in the Editor view. But if you *do* have a font file matching the requested face,
2915+ // then we want to emit a font-face rule for it which points to the font file.
2916+
2917+ var wantsItalic = font . fontStyle == "italic" ;
2918+ var wantsBold = font . fontWeight == "700" ;
2919+ string chosenPath = null ;
2920+ string declaredWeight = null ;
2921+ string declaredStyle = null ;
2922+
2923+ // Step 1: Find the best available face for the requested style/weight.
2924+ // For bold-italic requests, we prefer: BoldItalic > Italic > Bold > Normal
2925+ // This cascade means the browser only synthesizes what's missing (e.g., if we have
2926+ // Italic but not BoldItalic, the browser just synthesizes bold on top of real italic).
2927+ // For non-combined requests (just bold or just italic), we require an exact match
2928+ // or fall back to Normal in Step 3.
2929+ if ( wantsItalic && wantsBold )
2930+ {
2931+ // For combined requests, emit the most specific real face we have and let the
2932+ // browser synthesize the missing axis.
2933+ if ( ! string . IsNullOrEmpty ( group . BoldItalic ) )
2934+ {
2935+ chosenPath = group . BoldItalic ;
2936+ declaredWeight = "bold" ;
2937+ declaredStyle = "italic" ;
2938+ }
2939+ // the order of italic vs bold here is intentional,
2940+ // the thinking being that bold is easier to synthesize than italic,
2941+ // so if we have to pick just one, we prefer to provide the italic face
2942+ // and let the browser fake the bold.
2943+ else if ( ! string . IsNullOrEmpty ( group . Italic ) )
2944+ {
2945+ chosenPath = group . Italic ;
2946+ declaredWeight = "normal" ;
2947+ declaredStyle = "italic" ;
2948+ }
2949+ else if ( ! string . IsNullOrEmpty ( group . Bold ) )
2950+ {
2951+ chosenPath = group . Bold ;
2952+ declaredWeight = "bold" ;
2953+ declaredStyle = "normal" ;
2954+ }
2955+ else if ( ! string . IsNullOrEmpty ( group . Normal ) )
2956+ {
2957+ chosenPath = group . Normal ;
2958+ declaredWeight = "normal" ;
2959+ declaredStyle = "normal" ;
2960+ }
2961+ }
2962+ else if ( wantsItalic && ! wantsBold && ! string . IsNullOrEmpty ( group . Italic ) )
2963+ {
2964+ chosenPath = group . Italic ;
2965+ declaredWeight = "normal" ;
2966+ declaredStyle = "italic" ;
2967+ }
2968+ else if ( wantsBold && ! wantsItalic && ! string . IsNullOrEmpty ( group . Bold ) )
2969+ {
2970+ chosenPath = group . Bold ;
2971+ declaredWeight = "bold" ;
2972+ declaredStyle = "normal" ;
2973+ }
2974+ else if ( ! wantsItalic && ! wantsBold && ! string . IsNullOrEmpty ( group . Normal ) )
2975+ {
2976+ chosenPath = group . Normal ;
2977+ declaredWeight = "normal" ;
2978+ declaredStyle = "normal" ;
2979+ }
2980+
2981+ if ( ! string . IsNullOrEmpty ( chosenPath ) )
2982+ {
2983+ // Step 2: only emit one normal/normal rule per family to keep output deterministic.
2984+ // Note: we could still duplicate non-normal faces if a bold-italic request falls back
2985+ // to italic/bold that is also directly requested. This is rare and harmless, so we
2986+ // do not add broader dedupe logic.
2987+ if ( declaredStyle == "normal" && declaredWeight == "normal" )
2988+ {
2989+ if ( normalFacesAdded . Contains ( font . fontFamily ) )
2990+ return ;
2991+ normalFacesAdded . Add ( font . fontFamily ) ;
2992+ }
2993+ AddFontFace (
2994+ sb ,
2995+ font . fontFamily ,
2996+ declaredWeight ,
2997+ declaredStyle ?? font . fontStyle ,
2998+ chosenPath ,
2999+ relativePathFromCss ,
3000+ sanitizeFileName
3001+ ) ;
3002+ return ;
3003+ }
3004+
3005+ // Step 3: no exact face. Don't fake a bold/italic face by pointing at some other file.
3006+ // Instead, declare just a normal/normal face so the reading browser can synthesize
3007+ // missing styles, matching what happens in Bloom's editor (See BL-15558).
3008+ if ( string . IsNullOrEmpty ( group . Normal ) )
3009+ // If there's no Normal slot for this family, we avoid substituting bold/italic
3010+ // here; that would misrepresent the file and still not satisfy a normal request.
3011+ return ;
3012+ if ( normalFacesAdded . Contains ( font . fontFamily ) )
3013+ return ;
3014+ normalFacesAdded . Add ( font . fontFamily ) ;
29063015 AddFontFace (
29073016 sb ,
29083017 font . fontFamily ,
2909- weight ,
2910- font . fontStyle ,
2911- path ,
3018+ "normal" ,
3019+ "normal" ,
3020+ group . Normal ,
29123021 relativePathFromCss ,
29133022 sanitizeFileName
29143023 ) ;
0 commit comments