diff --git a/assets/files/chinese.svg b/assets/files/chinese.svg
new file mode 100644
index 000000000..b643e033a
--- /dev/null
+++ b/assets/files/chinese.svg
@@ -0,0 +1,4 @@
+
diff --git a/crates/typst-library/src/visualize/image.rs b/crates/typst-library/src/visualize/image.rs
index e6269198c..9e425f4fd 100644
--- a/crates/typst-library/src/visualize/image.rs
+++ b/crates/typst-library/src/visualize/image.rs
@@ -160,7 +160,7 @@ impl Layout for ImageElem {
data.into(),
format,
vt.world,
- families(styles).next().map(|s| s.as_str().into()),
+ families(styles).map(|s| s.as_str().into()).collect(),
self.alt(styles),
)
.at(self.span())?;
diff --git a/crates/typst/src/font/variant.rs b/crates/typst/src/font/variant.rs
index d4508a59e..74053e34b 100644
--- a/crates/typst/src/font/variant.rs
+++ b/crates/typst/src/font/variant.rs
@@ -62,6 +62,16 @@ impl Default for FontStyle {
}
}
+impl From for FontStyle {
+ fn from(style: usvg::FontStyle) -> Self {
+ match style {
+ usvg::FontStyle::Normal => Self::Normal,
+ usvg::FontStyle::Italic => Self::Italic,
+ usvg::FontStyle::Oblique => Self::Oblique,
+ }
+ }
+}
+
/// The weight of a font.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[derive(Serialize, Deserialize)]
@@ -244,6 +254,22 @@ impl Debug for FontStretch {
}
}
+impl From for FontStretch {
+ fn from(stretch: usvg::FontStretch) -> Self {
+ match stretch {
+ usvg::FontStretch::UltraCondensed => Self::ULTRA_CONDENSED,
+ usvg::FontStretch::ExtraCondensed => Self::EXTRA_CONDENSED,
+ usvg::FontStretch::Condensed => Self::CONDENSED,
+ usvg::FontStretch::SemiCondensed => Self::SEMI_CONDENSED,
+ usvg::FontStretch::Normal => Self::NORMAL,
+ usvg::FontStretch::SemiExpanded => Self::SEMI_EXPANDED,
+ usvg::FontStretch::Expanded => Self::EXPANDED,
+ usvg::FontStretch::ExtraExpanded => Self::EXTRA_EXPANDED,
+ usvg::FontStretch::UltraExpanded => Self::ULTRA_EXPANDED,
+ }
+ }
+}
+
cast! {
FontStretch,
self => self.to_ratio().into_value(),
diff --git a/crates/typst/src/image.rs b/crates/typst/src/image.rs
index fa03d0ebd..040c7f41a 100644
--- a/crates/typst/src/image.rs
+++ b/crates/typst/src/image.rs
@@ -1,7 +1,7 @@
//! Image handling.
use std::cell::RefCell;
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt::{self, Debug, Formatter};
use std::io;
use std::rc::Rc;
@@ -19,7 +19,7 @@ use usvg::{NodeExt, TreeParsing, TreeTextToPath};
use crate::diag::{bail, format_xml_like_error, StrResult};
use crate::eval::Bytes;
-use crate::font::Font;
+use crate::font::{Font, FontBook, FontInfo, FontVariant, FontWeight};
use crate::geom::Axes;
use crate::World;
@@ -76,10 +76,10 @@ impl Image {
data: Bytes,
format: ImageFormat,
world: Tracked,
- fallback_family: Option,
+ fallback_families: EcoVec,
alt: Option,
) -> StrResult {
- let loader = WorldLoader::new(world, fallback_family);
+ let loader = WorldLoader::new(world, fallback_families);
let decoded = match format {
ImageFormat::Raster(format) => decode_raster(&data, format)?,
ImageFormat::Vector(VectorFormat::Svg) => {
@@ -306,56 +306,112 @@ fn decode_svg(
Ok(Rc::new(DecodedImage::Svg(tree)))
}
+/// A font family and its variants.
+#[derive(Clone)]
+struct FontData {
+ /// The usvg-compatible font family name.
+ usvg_family: EcoString,
+ /// The font variants included in the family.
+ fonts: EcoVec,
+}
+
/// Discover and load the fonts referenced by an SVG.
fn load_svg_fonts(
tree: &usvg::Tree,
loader: Tracked,
) -> fontdb::Database {
let mut fontdb = fontdb::Database::new();
- let mut referenced = BTreeMap::>::new();
+ let mut font_cache = HashMap::>::new();
+ let mut loaded = HashSet::::new();
- // Loads a font family by its Typst name and returns its usvg-compatible
- // name.
- let mut load = |family: &str| -> Option {
+ // Loads a font family by its Typst name and returns its data.
+ let mut load = |family: &str| -> Option {
let family = EcoString::from(family.trim()).to_lowercase();
- if let Some(success) = referenced.get(&family) {
+ if let Some(success) = font_cache.get(&family) {
return success.clone();
}
+ let fonts = loader.load(&family);
+ let usvg_family = fonts.iter().find_map(|font| {
+ font.find_name(ttf_parser::name_id::TYPOGRAPHIC_FAMILY)
+ .or_else(|| font.find_name(ttf_parser::name_id::FAMILY))
+ .map(Into::::into)
+ });
+
+ let font_data = usvg_family.map(|usvg_family| FontData { usvg_family, fonts });
+ font_cache.insert(family, font_data.clone());
+ font_data
+ };
+
+ // Loads a font family into the fontdb database.
+ let mut load_into_db = |font_data: &FontData| {
+ if loaded.contains(&font_data.usvg_family) {
+ return;
+ }
+
// We load all variants for the family, since we don't know which will
// be used.
- 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));
- 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);
- }
+ for font in &font_data.fonts {
+ fontdb.load_font_data(font.data().to_vec());
}
- referenced.insert(family, name.clone());
- name
+ loaded.insert(font_data.usvg_family.clone());
};
- // Load fallback family.
- let mut fallback_usvg_compatible = None;
- if let Some(family) = loader.fallback_family() {
- fallback_usvg_compatible = load(family);
- }
+ let fallback_families = loader.fallback_families();
+ let fallback_fonts = fallback_families
+ .iter()
+ .filter_map(|family| load(family.as_str()))
+ .collect::>();
- // Find out which font families are referenced by the SVG.
+ // Determine the best font for each text node.
traverse_svg(&tree.root, &mut |node| {
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 in &mut span.font.families {
- if family.is_empty() || load(family).is_none() {
- if let Some(fallback) = &fallback_usvg_compatible {
- *family = fallback.into();
- }
+ let Some(text) = chunk.text.get(span.start..span.end) else { continue };
+
+ let inline_families = &span.font.families;
+ let inline_fonts = inline_families
+ .iter()
+ .filter(|family| !family.is_empty())
+ .filter_map(|family| load(family.as_str()))
+ .collect::>();
+
+ // Find a font that covers all characters in the span while
+ // taking the fallback order into account.
+ let font =
+ inline_fonts.iter().chain(fallback_fonts.iter()).find(|font_data| {
+ font_data.fonts.iter().any(|font| {
+ text.chars().all(|c| font.info().coverage.contains(c as u32))
+ })
+ });
+
+ if let Some(font) = font {
+ load_into_db(font);
+ span.font.families = vec![font.usvg_family.to_string()];
+ } else if !fallback_families.is_empty() {
+ // If no font covers all characters, use last resort fallback
+ // (only if fallback is enabled <=> fallback_families is not empty)
+ let like = inline_fonts
+ .first()
+ .or(fallback_fonts.first())
+ .and_then(|font_data| font_data.fonts.first())
+ .map(|font| font.info().clone());
+
+ let variant = FontVariant {
+ style: span.font.style.into(),
+ weight: FontWeight::from_number(span.font.weight),
+ stretch: span.font.stretch.into(),
+ };
+
+ let fallback = loader
+ .find_fallback(text, like, variant)
+ .and_then(|family| load(family.as_str()));
+
+ if let Some(font) = fallback {
+ load_into_db(&font);
+ span.font.families = vec![font.usvg_family.to_string()];
}
}
}
@@ -393,29 +449,39 @@ trait SvgFontLoader {
/// Load all fonts for the given lowercased font family.
fn load(&self, family: &str) -> EcoVec;
- /// The fallback family.
- fn fallback_family(&self) -> Option<&str>;
+ /// Prioritized sequence of fallback font families.
+ fn fallback_families(&self) -> &[String];
+
+ /// Find a last resort fallback for a given text and font variant.
+ fn find_fallback(
+ &self,
+ text: &str,
+ like: Option,
+ font: FontVariant,
+ ) -> Option;
}
/// Loads fonts for an SVG from a world
struct WorldLoader<'a> {
world: Tracked<'a, dyn World + 'a>,
seen: RefCell>>,
- fallback_family: Option,
+ fallback_families: EcoVec,
}
impl<'a> WorldLoader<'a> {
fn new(
world: Tracked<'a, dyn World + 'a>,
- fallback_family: Option,
+ fallback_families: EcoVec,
) -> Self {
- Self { world, fallback_family, seen: Default::default() }
+ Self { world, seen: Default::default(), fallback_families }
}
fn into_prepared(self) -> PreparedLoader {
+ let fonts = self.seen.into_inner().into_values().flatten().collect::>();
PreparedLoader {
- families: self.seen.into_inner(),
- fallback_family: self.fallback_family,
+ book: FontBook::from_fonts(fonts.iter()),
+ fonts,
+ fallback_families: self.fallback_families,
}
}
}
@@ -435,25 +501,55 @@ impl SvgFontLoader for WorldLoader<'_> {
.clone()
}
- fn fallback_family(&self) -> Option<&str> {
- self.fallback_family.as_deref()
+ fn fallback_families(&self) -> &[String] {
+ self.fallback_families.as_slice()
+ }
+
+ fn find_fallback(
+ &self,
+ text: &str,
+ like: Option,
+ variant: FontVariant,
+ ) -> Option {
+ self.world
+ .book()
+ .select_fallback(like.as_ref(), variant, text)
+ .and_then(|id| self.world.font(id))
+ .map(|font| font.info().family.to_lowercase().as_str().into())
}
}
/// Loads fonts for an SVG from a prepared list.
#[derive(Default, Hash)]
struct PreparedLoader {
- families: BTreeMap>,
- fallback_family: Option,
+ book: FontBook,
+ fonts: EcoVec,
+ fallback_families: EcoVec,
}
impl SvgFontLoader for PreparedLoader {
fn load(&self, family: &str) -> EcoVec {
- self.families.get(family).cloned().unwrap_or_default()
+ self.book
+ .select_family(family)
+ .filter_map(|id| self.fonts.get(id))
+ .cloned()
+ .collect()
}
- fn fallback_family(&self) -> Option<&str> {
- self.fallback_family.as_deref()
+ fn fallback_families(&self) -> &[String] {
+ self.fallback_families.as_slice()
+ }
+
+ fn find_fallback(
+ &self,
+ text: &str,
+ like: Option,
+ variant: FontVariant,
+ ) -> Option {
+ self.book
+ .select_fallback(like.as_ref(), variant, text)
+ .and_then(|id| self.fonts.get(id))
+ .map(|font| font.info().family.to_lowercase().as_str().into())
}
}
diff --git a/tests/ref/bugs/new-cm-svg.png b/tests/ref/bugs/new-cm-svg.png
index 2d445c3ab..d75a6dbb0 100644
Binary files a/tests/ref/bugs/new-cm-svg.png and b/tests/ref/bugs/new-cm-svg.png differ
diff --git a/tests/ref/layout/place-float-figure.png b/tests/ref/layout/place-float-figure.png
index b2755f126..bf9d21b4f 100644
Binary files a/tests/ref/layout/place-float-figure.png and b/tests/ref/layout/place-float-figure.png differ
diff --git a/tests/ref/visualize/svg-text.png b/tests/ref/visualize/svg-text.png
index 106d1c16b..b2bbe320b 100644
Binary files a/tests/ref/visualize/svg-text.png and b/tests/ref/visualize/svg-text.png differ
diff --git a/tests/typ/visualize/svg-text.typ b/tests/typ/visualize/svg-text.typ
index 838f73605..11283a76f 100644
--- a/tests/typ/visualize/svg-text.typ
+++ b/tests/typ/visualize/svg-text.typ
@@ -7,3 +7,12 @@
image("/files/diagram.svg"),
caption: [A textful diagram],
)
+
+---
+#set page(width: 250pt)
+#show image: set text(font: ("Roboto", "Noto Serif CJK SC"))
+
+#figure(
+ image("/files/chinese.svg"),
+ caption: [Bilingual text]
+)