diff --git a/crates/resvg/tests/integration/render.rs b/crates/resvg/tests/integration/render.rs index ad79dd7e5..35f423490 100644 --- a/crates/resvg/tests/integration/render.rs +++ b/crates/resvg/tests/integration/render.rs @@ -1418,6 +1418,7 @@ use crate::render; #[test] fn text_font_family_double_quoted() { assert_eq!(render("tests/text/font-family/double-quoted"), 0); } #[test] fn text_font_family_fallback_1() { assert_eq!(render("tests/text/font-family/fallback-1"), 0); } #[test] fn text_font_family_fallback_2() { assert_eq!(render("tests/text/font-family/fallback-2"), 0); } +#[test] fn text_font_family_fallback_3() { assert_eq!(render("tests/text/font-family/fallback-3"), 0); } #[test] fn text_font_family_fantasy() { assert_eq!(render("tests/text/font-family/fantasy"), 0); } #[test] fn text_font_family_font_list() { assert_eq!(render("tests/text/font-family/font-list"), 0); } #[test] fn text_font_family_monospace() { assert_eq!(render("tests/text/font-family/monospace"), 0); } diff --git a/crates/resvg/tests/tests/text/direction/rtl.png b/crates/resvg/tests/tests/text/direction/rtl.png index 1d0416d25..3580b5d3b 100644 Binary files a/crates/resvg/tests/tests/text/direction/rtl.png and b/crates/resvg/tests/tests/text/direction/rtl.png differ diff --git a/crates/resvg/tests/tests/text/font-family/fallback-3.png b/crates/resvg/tests/tests/text/font-family/fallback-3.png new file mode 100644 index 000000000..2d14e0501 Binary files /dev/null and b/crates/resvg/tests/tests/text/font-family/fallback-3.png differ diff --git a/crates/resvg/tests/tests/text/font-family/fallback-3.svg b/crates/resvg/tests/tests/text/font-family/fallback-3.svg new file mode 100644 index 000000000..1073effe4 --- /dev/null +++ b/crates/resvg/tests/tests/text/font-family/fallback-3.svg @@ -0,0 +1,10 @@ + + Fallback (1) + + Text 😀 + Text 😀 + Text 😀 + + + + diff --git a/crates/usvg/src/text/layout.rs b/crates/usvg/src/text/layout.rs index 448d6545d..56d6a9ff7 100644 --- a/crates/usvg/src/text/layout.rs +++ b/crates/usvg/src/text/layout.rs @@ -5,8 +5,9 @@ use std::collections::{HashMap, HashSet}; use std::num::NonZeroU16; use std::sync::Arc; -use fontdb::{Database, ID}; +use fontdb::{Family, ID, Query}; use kurbo::{ParamCurve, ParamCurveArclen, ParamCurveDeriv}; +use log::debug; use rustybuzz::ttf_parser; use rustybuzz::ttf_parser::{GlyphId, Tag}; use strict_num::NonZeroPositiveF32; @@ -900,14 +901,9 @@ fn process_chunk( let mut glyphs = Vec::new(); for span in &chunk.spans { - let font = match fonts_cache.get(&span.font) { - Some(v) => v.clone(), - None => continue, - }; - let tmp_glyphs = shape_text( &chunk.text, - font, + &span.font, span.small_caps, span.apply_kerning, &span.font.variations, @@ -1203,7 +1199,7 @@ pub(crate) trait DatabaseExt { fn has_char(&self, id: ID, c: char) -> bool; } -impl DatabaseExt for Database { +impl DatabaseExt for fontdb::Database { #[inline(never)] fn load_font(&self, id: ID) -> Option { self.with_face_data(id, |data, face_index| -> Option { @@ -1289,10 +1285,21 @@ impl DatabaseExt for Database { } } +fn as_fontdb_family(family: &'_ crate::FontFamily) -> Family<'_> { + match family { + crate::FontFamily::Serif => Family::Serif, + crate::FontFamily::SansSerif => Family::SansSerif, + crate::FontFamily::Cursive => Family::Cursive, + crate::FontFamily::Fantasy => Family::Fantasy, + crate::FontFamily::Monospace => Family::Monospace, + crate::FontFamily::Named(s) => Family::Name(s), + } +} + /// Text shaping with font fallback. pub(crate) fn shape_text( text: &str, - font: Arc, + span_font: &Font, small_caps: bool, apply_kerning: bool, variations: &[crate::FontVariation], @@ -1301,9 +1308,15 @@ pub(crate) fn shape_text( resolver: &FontResolver, fontdb: &mut Arc, ) -> Vec { + let Some(initial_font) = + (resolver.select_font)(span_font, fontdb).and_then(|x| fontdb.load_font(x)) + else { + return vec![]; + }; + let initial_font = Arc::new(initial_font); let mut glyphs = shape_text_with_font( text, - font.clone(), + initial_font.clone(), small_caps, apply_kerning, variations, @@ -1314,7 +1327,7 @@ pub(crate) fn shape_text( .unwrap_or_default(); // Remember all fonts used for shaping. - let mut used_fonts = vec![font.id]; + let mut used_fonts = vec![initial_font.id]; // Loop until all glyphs become resolved or until no more fonts are left. 'outer: loop { @@ -1327,12 +1340,34 @@ pub(crate) fn shape_text( } if let Some(c) = missing { - let fallback_font = match (resolver.select_fallback)(c, &used_fonts, fontdb) - .and_then(|id| fontdb.load_font(id)) - { + let mut fallback_id = None; + + for family in &span_font.families { + let query = Query { + families: &[as_fontdb_family(family)], + weight: fontdb::Weight(span_font.weight), + stretch: span_font.stretch.into(), + style: span_font.style.into(), + }; + if let Some(id) = fontdb.query(&query) { + if !used_fonts.contains(&id) && fontdb.has_char(id, c) { + fallback_id = Some(id); + break; + } + } + } + + let fallback_id = + fallback_id.or_else(|| (resolver.select_fallback)(c, &used_fonts, fontdb)); + + let fallback_font = match fallback_id.and_then(|id| fontdb.load_font(id)) { Some(v) => Arc::new(v), None => break 'outer, }; + debug!( + "Fallback font postscript name: {}", + fontdb.face(fallback_font.id).unwrap().post_script_name + ); // Shape again, using a new font. let fallback_glyphs = shape_text_with_font( @@ -1347,13 +1382,6 @@ pub(crate) fn shape_text( ) .unwrap_or_default(); - let all_matched = fallback_glyphs.iter().all(|g| !g.is_missing()); - if all_matched { - // Replace all glyphs when all of them were matched. - glyphs = fallback_glyphs; - break 'outer; - } - // We assume, that shaping with an any font will produce the same amount of glyphs. // This is incorrect, but good enough for now. if glyphs.len() != fallback_glyphs.len() {