From 073effc7407bd314beea09ac8f83e809bdb10497 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 9 Sep 2023 22:31:37 +0200 Subject: [PATCH] Fix font family mismatch between Typst and usvg Fixes #2051 --- crates/typst-library/src/visualize/image.rs | 2 +- crates/typst/src/image.rs | 82 ++++++++++---------- tests/ref/bugs/new-cm-svg.png | Bin 0 -> 2911 bytes tests/typ/bugs/new-cm-svg.typ | 2 + 4 files changed, 44 insertions(+), 42 deletions(-) create mode 100644 tests/ref/bugs/new-cm-svg.png create mode 100644 tests/typ/bugs/new-cm-svg.typ diff --git a/crates/typst-library/src/visualize/image.rs b/crates/typst-library/src/visualize/image.rs index a5c8eae48..a06509ddd 100644 --- a/crates/typst-library/src/visualize/image.rs +++ b/crates/typst-library/src/visualize/image.rs @@ -168,7 +168,7 @@ impl Layout for ImageElem { data.into(), format, vt.world, - families(styles).next().as_ref().map(|f| f.as_str()), + families(styles).next().map(|s| s.as_str().into()), self.alt(styles), ) .at(self.span())?; diff --git a/crates/typst/src/image.rs b/crates/typst/src/image.rs index 40dad6152..83d8f3a78 100644 --- a/crates/typst/src/image.rs +++ b/crates/typst/src/image.rs @@ -76,7 +76,7 @@ impl Image { data: Bytes, format: ImageFormat, world: Tracked, - fallback_family: Option<&str>, + fallback_family: Option, alt: Option, ) -> StrResult { let loader = WorldLoader::new(world, fallback_family); @@ -313,31 +313,39 @@ fn load_svg_fonts( tree: &usvg::Tree, loader: Tracked, ) -> fontdb::Database { - let mut referenced = BTreeMap::::new(); let mut fontdb = fontdb::Database::new(); - let mut load = |family_cased: &str| { - let family = EcoString::from(family_cased.trim()).to_lowercase(); - if let Some(&success) = referenced.get(&family) { - return success; + let mut referenced = BTreeMap::>::new(); + + // Loads a font family by its Typst name and returns its usvg-compatible + // name. + let mut load = |family: &str| -> Option { + let family = EcoString::from(family.trim()).to_lowercase(); + if let Some(success) = referenced.get(&family) { + return success.clone(); } // We load all variants for the family, since we don't know which will // be used. - let mut success = false; + let mut name = None; for font in loader.load(&family) { let source = Arc::new(font.data().clone()); fontdb.load_font_source(fontdb::Source::Binary(source)); - success = true; + if name.is_none() { + name = font + .find_name(ttf_parser::name_id::TYPOGRAPHIC_FAMILY) + .or_else(|| font.find_name(ttf_parser::name_id::FAMILY)) + .map(Into::into); + } } - referenced.insert(family, success); - success + referenced.insert(family, name.clone()); + name }; // Load fallback family. - let fallback_cased = loader.fallback(); - if let Some(family_cased) = fallback_cased { - load(family_cased); + let mut fallback_usvg_compatible = None; + if let Some(family) = loader.fallback_family() { + fallback_usvg_compatible = load(family); } // Find out which font families are referenced by the SVG. @@ -345,10 +353,11 @@ fn load_svg_fonts( let usvg::NodeKind::Text(text) = &mut *node.borrow_mut() else { return }; for chunk in &mut text.chunks { for span in &mut chunk.spans { - for family_cased in &mut span.font.families { - if family_cased.is_empty() || !load(family_cased) { - let Some(fallback) = fallback_cased else { continue }; - *family_cased = fallback.into(); + for family in &mut span.font.families { + if family.is_empty() || load(family).is_none() { + if let Some(fallback) = &fallback_usvg_compatible { + *family = fallback.into(); + } } } } @@ -384,40 +393,31 @@ where #[comemo::track] trait SvgFontLoader { /// Load all fonts for the given lowercased font family. - fn load(&self, lower_family: &str) -> EcoVec; + fn load(&self, family: &str) -> EcoVec; - /// The case-sensitive name of the fallback family. - fn fallback(&self) -> Option<&str>; + /// The fallback family. + fn fallback_family(&self) -> Option<&str>; } /// Loads fonts for an SVG from a world struct WorldLoader<'a> { world: Tracked<'a, dyn World + 'a>, seen: RefCell>>, - fallback_family_cased: Option, + fallback_family: Option, } impl<'a> WorldLoader<'a> { - fn new(world: Tracked<'a, dyn World + 'a>, fallback_family: Option<&str>) -> Self { - // Recover the non-lowercased version of the family because - // usvg is case sensitive. - let book = world.book(); - let fallback_family_cased = fallback_family - .and_then(|lowercase| book.select_family(lowercase).next()) - .and_then(|index| book.info(index)) - .map(|info| info.family.clone()); - - Self { - world, - fallback_family_cased, - seen: Default::default(), - } + fn new( + world: Tracked<'a, dyn World + 'a>, + fallback_family: Option, + ) -> Self { + Self { world, fallback_family, seen: Default::default() } } fn into_prepared(self) -> PreparedLoader { PreparedLoader { families: self.seen.into_inner(), - fallback_family_cased: self.fallback_family_cased, + fallback_family: self.fallback_family, } } } @@ -437,8 +437,8 @@ impl SvgFontLoader for WorldLoader<'_> { .clone() } - fn fallback(&self) -> Option<&str> { - self.fallback_family_cased.as_deref() + fn fallback_family(&self) -> Option<&str> { + self.fallback_family.as_deref() } } @@ -446,7 +446,7 @@ impl SvgFontLoader for WorldLoader<'_> { #[derive(Default, Hash)] struct PreparedLoader { families: BTreeMap>, - fallback_family_cased: Option, + fallback_family: Option, } impl SvgFontLoader for PreparedLoader { @@ -454,8 +454,8 @@ impl SvgFontLoader for PreparedLoader { self.families.get(family).cloned().unwrap_or_default() } - fn fallback(&self) -> Option<&str> { - self.fallback_family_cased.as_deref() + fn fallback_family(&self) -> Option<&str> { + self.fallback_family.as_deref() } } diff --git a/tests/ref/bugs/new-cm-svg.png b/tests/ref/bugs/new-cm-svg.png new file mode 100644 index 0000000000000000000000000000000000000000..c011bbfce0ef287a3b0f5d4b0841588b2258cf7e GIT binary patch literal 2911 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