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