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