@@ -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 ( ) ;
@@ -226,14 +244,69 @@ impl SvgRenderer {
226244 }
227245}
228246
247+ fn load_bundled_fonts ( asset_source : & dyn AssetSource , db : & mut usvg:: fontdb:: Database ) {
248+ let font_paths = [
249+ "fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf" ,
250+ "fonts/lilex/Lilex-Regular.ttf" ,
251+ ] ;
252+ for path in font_paths {
253+ match asset_source. load ( path) {
254+ Ok ( Some ( data) ) => db. load_font_data ( data. into_owned ( ) ) ,
255+ Ok ( None ) => log:: warn!( "Bundled font not found: {path}" ) ,
256+ Err ( error) => log:: warn!( "Failed to load bundled font {path}: {error}" ) ,
257+ }
258+ }
259+ }
260+
261+ // fontdb defaults generic families to Microsoft fonts ("Arial", "Times New Roman")
262+ // which aren't installed on most Linux systems. fontconfig normally overrides these,
263+ // but when it fails the defaults remain and all generic family queries return None.
264+ fn fix_generic_font_families ( db : & mut usvg:: fontdb:: Database ) {
265+ use usvg:: fontdb:: { Family , Query } ;
266+
267+ let families_and_fallbacks: & [ ( Family < ' _ > , & str ) ] = & [
268+ ( Family :: SansSerif , "IBM Plex Sans" ) ,
269+ // No serif font bundled; use sans-serif as best available fallback.
270+ ( Family :: Serif , "IBM Plex Sans" ) ,
271+ ( Family :: Monospace , "Lilex" ) ,
272+ ( Family :: Cursive , "IBM Plex Sans" ) ,
273+ ( Family :: Fantasy , "IBM Plex Sans" ) ,
274+ ] ;
275+
276+ for ( family, fallback_name) in families_and_fallbacks {
277+ let query = Query {
278+ families : & [ * family] ,
279+ ..Default :: default ( )
280+ } ;
281+ if db. query ( & query) . is_none ( ) {
282+ match family {
283+ Family :: SansSerif => db. set_sans_serif_family ( * fallback_name) ,
284+ Family :: Serif => db. set_serif_family ( * fallback_name) ,
285+ Family :: Monospace => db. set_monospace_family ( * fallback_name) ,
286+ Family :: Cursive => db. set_cursive_family ( * fallback_name) ,
287+ Family :: Fantasy => db. set_fantasy_family ( * fallback_name) ,
288+ _ => { }
289+ }
290+ }
291+ }
292+ }
293+
229294#[ cfg( test) ]
230295mod tests {
231296 use super :: * ;
297+ use usvg:: fontdb:: { Database , Family , Query } ;
232298
233299 const IBM_PLEX_REGULAR : & [ u8 ] =
234300 include_bytes ! ( "../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf" ) ;
235301 const LILEX_REGULAR : & [ u8 ] = include_bytes ! ( "../../../assets/fonts/lilex/Lilex-Regular.ttf" ) ;
236302
303+ fn db_with_bundled_fonts ( ) -> Database {
304+ let mut db = Database :: new ( ) ;
305+ db. load_font_data ( IBM_PLEX_REGULAR . to_vec ( ) ) ;
306+ db. load_font_data ( LILEX_REGULAR . to_vec ( ) ) ;
307+ db
308+ }
309+
237310 #[ test]
238311 fn test_is_emoji_presentation ( ) {
239312 let cases = [
@@ -266,11 +339,33 @@ mod tests {
266339 }
267340
268341 #[ test]
269- fn test_select_emoji_font_skips_family_without_glyph ( ) {
270- let mut db = usvg:: fontdb:: Database :: new ( ) ;
342+ fn fix_generic_font_families_sets_all_families ( ) {
343+ let mut db = db_with_bundled_fonts ( ) ;
344+ fix_generic_font_families ( & mut db) ;
345+
346+ let families = [
347+ Family :: SansSerif ,
348+ Family :: Serif ,
349+ Family :: Monospace ,
350+ Family :: Cursive ,
351+ Family :: Fantasy ,
352+ ] ;
271353
272- db. load_font_data ( IBM_PLEX_REGULAR . to_vec ( ) ) ;
273- db. load_font_data ( LILEX_REGULAR . to_vec ( ) ) ;
354+ for family in families {
355+ let query = Query {
356+ families : & [ family] ,
357+ ..Default :: default ( )
358+ } ;
359+ assert ! (
360+ db. query( & query) . is_some( ) ,
361+ "Expected generic family {family:?} to resolve after fix_generic_font_families"
362+ ) ;
363+ }
364+ }
365+
366+ #[ test]
367+ fn test_select_emoji_font_skips_family_without_glyph ( ) {
368+ let mut db = db_with_bundled_fonts ( ) ;
274369
275370 let ibm_plex_sans = db
276371 . query ( & usvg:: fontdb:: Query {
@@ -294,4 +389,22 @@ mod tests {
294389 assert ! ( !font_has_char( & db, ibm_plex_sans, '│' ) ) ;
295390 assert ! ( font_has_char( & db, selected, '│' ) ) ;
296391 }
392+
393+ #[ test]
394+ fn fix_generic_font_families_monospace_resolves_to_lilex ( ) {
395+ let mut db = db_with_bundled_fonts ( ) ;
396+ fix_generic_font_families ( & mut db) ;
397+
398+ let query = Query {
399+ families : & [ Family :: Monospace ] ,
400+ ..Default :: default ( )
401+ } ;
402+ let id = db. query ( & query) . expect ( "Monospace should resolve" ) ;
403+ let face = db. face ( id) . expect ( "Face should exist" ) ;
404+ assert ! (
405+ face. families. iter( ) . any( |( name, _) | name. contains( "Lilex" ) ) ,
406+ "Monospace should map to Lilex, got {:?}" ,
407+ face. families
408+ ) ;
409+ }
297410}
0 commit comments