usvg/text/mod.rs
1// Copyright 2024 the Resvg Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use std::sync::Arc;
5
6use fontdb::{Database, ID};
7use svgtypes::FontFamily;
8
9use self::layout::DatabaseExt;
10use crate::{Cache, Font, FontStretch, FontStyle, Text};
11
12pub(crate) mod flatten;
13mod transform;
14
15mod colr;
16/// Provides access to the layout of a text node.
17pub mod layout;
18
19/// The optical sizing variation axis tag.
20pub(crate) const OPSZ: skrifa::Tag = skrifa::Tag::from_be_bytes(*b"opsz");
21
22/// The ID of a glyph within a font
23#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
24pub struct GlyphId(pub u32);
25
26impl From<GlyphId> for skrifa::raw::types::GlyphId {
27 fn from(value: GlyphId) -> Self {
28 Self::new(value.0)
29 }
30}
31
32/// A shorthand for [FontResolver]'s font selection function.
33///
34/// This function receives a font specification (families + a style, weight,
35/// stretch triple) and a font database and should return the ID of the font
36/// that shall be used (if any).
37///
38/// In the basic case, the function will search the existing fonts in the
39/// database to find a good match, e.g. via
40/// [`Database::query`](fontdb::Database::query). This is what the [default
41/// implementation](FontResolver::default_font_selector) does.
42///
43/// Users with more complex requirements can mutate the database to load
44/// additional fonts dynamically. To perform mutation, it is recommended to call
45/// `Arc::make_mut` on the provided database. (This call is not done outside of
46/// the callback to not needless clone an underlying shared database if no
47/// mutation will be performed.) It is important that the database is only
48/// mutated additively. Removing fonts or replacing the entire database will
49/// break things.
50pub type FontSelectionFn<'a> =
51 Box<dyn Fn(&Font, &mut Arc<Database>) -> Option<ID> + Send + Sync + 'a>;
52
53/// A shorthand for [FontResolver]'s fallback selection function.
54///
55/// This function receives a specific character, a list of already used fonts,
56/// and a font database. It should return the ID of a font that
57/// - is not any of the already used fonts
58/// - is as close as possible to the first already used font (if any)
59/// - supports the given character
60///
61/// The function can search the existing database, but can also load additional
62/// fonts dynamically. See the documentation of [`FontSelectionFn`] for more
63/// details.
64pub type FallbackSelectionFn<'a> =
65 Box<dyn Fn(char, &[ID], &mut Arc<Database>) -> Option<ID> + Send + Sync + 'a>;
66
67/// A font resolver for `<text>` elements.
68///
69/// This type can be useful if you want to have an alternative font handling to
70/// the default one. By default, only fonts specified upfront in
71/// [`Options::fontdb`](crate::Options::fontdb) will be used. This type allows
72/// you to load additional fonts on-demand and customize the font selection
73/// process.
74pub struct FontResolver<'a> {
75 /// Resolver function that will be used when selecting a specific font
76 /// for a generic [`Font`] specification.
77 pub select_font: FontSelectionFn<'a>,
78
79 /// Resolver function that will be used when selecting a fallback font for a
80 /// character.
81 pub select_fallback: FallbackSelectionFn<'a>,
82}
83
84impl Default for FontResolver<'_> {
85 fn default() -> Self {
86 FontResolver {
87 select_font: FontResolver::default_font_selector(),
88 select_fallback: FontResolver::default_fallback_selector(),
89 }
90 }
91}
92
93impl FontResolver<'_> {
94 /// Creates a default font selection resolver.
95 ///
96 /// The default implementation forwards to
97 /// [`query`](fontdb::Database::query) on the font database specified in the
98 /// [`Options`](crate::Options).
99 pub fn default_font_selector() -> FontSelectionFn<'static> {
100 Box::new(move |font, fontdb| {
101 let mut name_list = Vec::new();
102 for family in &font.families {
103 name_list.push(match family {
104 FontFamily::Serif => fontdb::Family::Serif,
105 FontFamily::SansSerif => fontdb::Family::SansSerif,
106 FontFamily::Cursive => fontdb::Family::Cursive,
107 FontFamily::Fantasy => fontdb::Family::Fantasy,
108 FontFamily::Monospace => fontdb::Family::Monospace,
109 FontFamily::Named(s) => fontdb::Family::Name(s),
110 });
111 }
112
113 // Use the default font as fallback.
114 name_list.push(fontdb::Family::Serif);
115
116 let stretch = match font.stretch {
117 FontStretch::UltraCondensed => fontdb::Stretch::UltraCondensed,
118 FontStretch::ExtraCondensed => fontdb::Stretch::ExtraCondensed,
119 FontStretch::Condensed => fontdb::Stretch::Condensed,
120 FontStretch::SemiCondensed => fontdb::Stretch::SemiCondensed,
121 FontStretch::Normal => fontdb::Stretch::Normal,
122 FontStretch::SemiExpanded => fontdb::Stretch::SemiExpanded,
123 FontStretch::Expanded => fontdb::Stretch::Expanded,
124 FontStretch::ExtraExpanded => fontdb::Stretch::ExtraExpanded,
125 FontStretch::UltraExpanded => fontdb::Stretch::UltraExpanded,
126 };
127
128 let style = match font.style {
129 FontStyle::Normal => fontdb::Style::Normal,
130 FontStyle::Italic => fontdb::Style::Italic,
131 FontStyle::Oblique => fontdb::Style::Oblique,
132 };
133
134 let query = fontdb::Query {
135 families: &name_list,
136 weight: fontdb::Weight(font.weight),
137 stretch,
138 style,
139 };
140
141 let id = fontdb.query(&query);
142 if id.is_none() {
143 log::warn!(
144 "No match for '{}' font-family.",
145 font.families
146 .iter()
147 .map(|f| f.to_string())
148 .collect::<Vec<_>>()
149 .join(", ")
150 );
151 }
152
153 id
154 })
155 }
156
157 /// Creates a default font fallback selection resolver.
158 ///
159 /// The default implementation searches through the entire `fontdb`
160 /// to find a font that has the correct style and supports the character.
161 pub fn default_fallback_selector() -> FallbackSelectionFn<'static> {
162 Box::new(|c, exclude_fonts, fontdb| {
163 let base_font_id = exclude_fonts[0];
164
165 // Iterate over fonts and check if any of them support the specified char.
166 for face in fontdb.faces() {
167 // Ignore fonts, that were used for shaping already.
168 if exclude_fonts.contains(&face.id) {
169 continue;
170 }
171
172 // Check that the new face has the same style.
173 let base_face = fontdb.face(base_font_id)?;
174 if base_face.style != face.style
175 && base_face.weight != face.weight
176 && base_face.stretch != face.stretch
177 {
178 continue;
179 }
180
181 if !fontdb.has_char(face.id, c) {
182 continue;
183 }
184
185 let base_family = base_face
186 .families
187 .iter()
188 .find(|f| f.1 == fontdb::Language::English_UnitedStates)
189 .unwrap_or(&base_face.families[0]);
190
191 let new_family = face
192 .families
193 .iter()
194 .find(|f| f.1 == fontdb::Language::English_UnitedStates)
195 .unwrap_or(&base_face.families[0]);
196
197 log::warn!("Fallback from {} to {}.", base_family.0, new_family.0);
198 return Some(face.id);
199 }
200
201 None
202 })
203 }
204}
205
206impl std::fmt::Debug for FontResolver<'_> {
207 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 f.write_str("FontResolver { .. }")
209 }
210}
211
212/// Convert a text into its paths. This is done in two steps:
213/// 1. We convert the text into glyphs and position them according to the rules specified
214/// in the SVG specification. While doing so, we also calculate the text bbox (which
215/// is not based on the outlines of a glyph, but instead the glyph metrics as well
216/// as decoration spans).
217/// 2. We convert all of the positioned glyphs into outlines.
218pub(crate) fn convert(text: &mut Text, resolver: &FontResolver, cache: &mut Cache) -> Option<()> {
219 let (text_fragments, bbox) = layout::layout_text(text, resolver, &mut cache.fontdb)?;
220 text.layouted = text_fragments;
221 text.bounding_box = bbox.to_rect();
222 text.abs_bounding_box = bbox.transform(text.abs_transform)?.to_rect();
223
224 let (group, stroke_bbox) = flatten::flatten(text, cache)?;
225 text.flattened = Box::new(group);
226 text.stroke_bounding_box = stroke_bbox.to_rect();
227 text.abs_stroke_bounding_box = stroke_bbox.transform(text.abs_transform)?.to_rect();
228
229 Some(())
230}