@@ -105,18 +105,36 @@ pub enum SvgSize {
105105impl SvgRenderer {
106106 /// Creates a new SVG renderer with the provided asset source.
107107 pub fn new ( asset_source : Arc < dyn AssetSource > ) -> Self {
108- static FONT_DB : LazyLock < Arc < usvg:: fontdb:: Database > > = LazyLock :: new ( || {
108+ static SYSTEM_FONT_DB : LazyLock < Arc < usvg:: fontdb:: Database > > = LazyLock :: new ( || {
109109 let mut db = usvg:: fontdb:: Database :: new ( ) ;
110110 db. load_system_fonts ( ) ;
111111 Arc :: new ( db)
112112 } ) ;
113+
114+ let fontdb = {
115+ let mut db = ( * * SYSTEM_FONT_DB ) . clone ( ) ;
116+ load_bundled_fonts ( & * asset_source, & mut db) ;
117+ fix_generic_font_families ( & mut db) ;
118+ Arc :: new ( db)
119+ } ;
120+
113121 let default_font_resolver = usvg:: FontResolver :: default_font_selector ( ) ;
114122 let font_resolver = Box :: new (
115123 move |font : & usvg:: Font , db : & mut Arc < usvg:: fontdb:: Database > | {
116124 if db. is_empty ( ) {
117- * db = FONT_DB . clone ( ) ;
125+ * db = fontdb. clone ( ) ;
126+ }
127+ if let Some ( id) = default_font_resolver ( font, db) {
128+ return Some ( id) ;
118129 }
119- default_font_resolver ( font, db)
130+ // fontdb doesn't recognize CSS system font keywords like "system-ui"
131+ // or "ui-sans-serif", so fall back to sans-serif before any face.
132+ let sans_query = usvg:: fontdb:: Query {
133+ families : & [ usvg:: fontdb:: Family :: SansSerif ] ,
134+ ..Default :: default ( )
135+ } ;
136+ db. query ( & sans_query)
137+ . or_else ( || db. faces ( ) . next ( ) . map ( |f| f. id ) )
120138 } ,
121139 ) ;
122140 let default_fallback_selection = usvg:: FontResolver :: default_fallback_selector ( ) ;
@@ -229,13 +247,69 @@ impl SvgRenderer {
229247 }
230248}
231249
250+ fn load_bundled_fonts ( asset_source : & dyn AssetSource , db : & mut usvg:: fontdb:: Database ) {
251+ let font_paths = [
252+ "fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf" ,
253+ "fonts/lilex/Lilex-Regular.ttf" ,
254+ ] ;
255+ for path in font_paths {
256+ match asset_source. load ( path) {
257+ Ok ( Some ( data) ) => db. load_font_data ( data. into_owned ( ) ) ,
258+ Ok ( None ) => log:: warn!( "Bundled font not found: {path}" ) ,
259+ Err ( error) => log:: warn!( "Failed to load bundled font {path}: {error}" ) ,
260+ }
261+ }
262+ }
263+
264+ // fontdb defaults generic families to Microsoft fonts ("Arial", "Times New Roman")
265+ // which aren't installed on most Linux systems. fontconfig normally overrides these,
266+ // but when it fails the defaults remain and all generic family queries return None.
267+ fn fix_generic_font_families ( db : & mut usvg:: fontdb:: Database ) {
268+ use usvg:: fontdb:: { Family , Query } ;
269+
270+ let families_and_fallbacks: & [ ( Family < ' _ > , & str ) ] = & [
271+ ( Family :: SansSerif , "IBM Plex Sans" ) ,
272+ // No serif font bundled; use sans-serif as best available fallback.
273+ ( Family :: Serif , "IBM Plex Sans" ) ,
274+ ( Family :: Monospace , "Lilex" ) ,
275+ ( Family :: Cursive , "IBM Plex Sans" ) ,
276+ ( Family :: Fantasy , "IBM Plex Sans" ) ,
277+ ] ;
278+
279+ for ( family, fallback_name) in families_and_fallbacks {
280+ let query = Query {
281+ families : & [ * family] ,
282+ ..Default :: default ( )
283+ } ;
284+ if db. query ( & query) . is_none ( ) {
285+ match family {
286+ Family :: SansSerif => db. set_sans_serif_family ( * fallback_name) ,
287+ Family :: Serif => db. set_serif_family ( * fallback_name) ,
288+ Family :: Monospace => db. set_monospace_family ( * fallback_name) ,
289+ Family :: Cursive => db. set_cursive_family ( * fallback_name) ,
290+ Family :: Fantasy => db. set_fantasy_family ( * fallback_name) ,
291+ _ => { }
292+ }
293+ }
294+ }
295+ }
296+
232297#[ cfg( test) ]
233298mod tests {
234299 use super :: * ;
300+ use usvg:: fontdb:: { Database , Family , Query } ;
235301
236302 const IBM_PLEX_REGULAR : & [ u8 ] =
237303 include_bytes ! ( "../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf" ) ;
238- const LILEX_REGULAR : & [ u8 ] = include_bytes ! ( "../../../assets/fonts/lilex/Lilex-Regular.ttf" ) ;
304+ const LILEX_REGULAR : & [ u8 ] =
305+ include_bytes ! ( "../../../assets/fonts/lilex/Lilex-Regular.ttf" ) ;
306+
307+ fn db_with_bundled_fonts ( ) -> Database {
308+ let mut db = Database :: new ( ) ;
309+ db. load_font_data ( IBM_PLEX_REGULAR . to_vec ( ) ) ;
310+ db. load_font_data ( LILEX_REGULAR . to_vec ( ) ) ;
311+ db
312+ }
239313
240314 #[ test]
241315 fn test_is_emoji_presentation ( ) {
@@ -269,11 +343,33 @@ mod tests {
269343 }
270344
271345 #[ test]
272- fn test_select_emoji_font_skips_family_without_glyph ( ) {
273- let mut db = usvg:: fontdb:: Database :: new ( ) ;
346+ fn fix_generic_font_families_sets_all_families ( ) {
347+ let mut db = db_with_bundled_fonts ( ) ;
348+ fix_generic_font_families ( & mut db) ;
349+
350+ let families = [
351+ Family :: SansSerif ,
352+ Family :: Serif ,
353+ Family :: Monospace ,
354+ Family :: Cursive ,
355+ Family :: Fantasy ,
356+ ] ;
274357
275- db. load_font_data ( IBM_PLEX_REGULAR . to_vec ( ) ) ;
276- db. load_font_data ( LILEX_REGULAR . to_vec ( ) ) ;
358+ for family in families {
359+ let query = Query {
360+ families : & [ family] ,
361+ ..Default :: default ( )
362+ } ;
363+ assert ! (
364+ db. query( & query) . is_some( ) ,
365+ "Expected generic family {family:?} to resolve after fix_generic_font_families"
366+ ) ;
367+ }
368+ }
369+
370+ #[ test]
371+ fn test_select_emoji_font_skips_family_without_glyph ( ) {
372+ let mut db = db_with_bundled_fonts ( ) ;
277373
278374 let ibm_plex_sans = db
279375 . query ( & usvg:: fontdb:: Query {
@@ -297,4 +393,24 @@ mod tests {
297393 assert ! ( !font_has_char( & db, ibm_plex_sans, '│' ) ) ;
298394 assert ! ( font_has_char( & db, selected, '│' ) ) ;
299395 }
396+
397+ #[ test]
398+ fn fix_generic_font_families_monospace_resolves_to_lilex ( ) {
399+ let mut db = db_with_bundled_fonts ( ) ;
400+ fix_generic_font_families ( & mut db) ;
401+
402+ let query = Query {
403+ families : & [ Family :: Monospace ] ,
404+ ..Default :: default ( )
405+ } ;
406+ let id = db. query ( & query) . expect ( "Monospace should resolve" ) ;
407+ let face = db. face ( id) . expect ( "Face should exist" ) ;
408+ assert ! (
409+ face. families
410+ . iter( )
411+ . any( |( name, _) | name. contains( "Lilex" ) ) ,
412+ "Monospace should map to Lilex, got {:?}" ,
413+ face. families
414+ ) ;
415+ }
300416}
0 commit comments