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