1 | // Copyright 2024 the Resvg Authors |
2 | // SPDX-License-Identifier: Apache-2.0 OR MIT |
3 | |
4 | use std::sync::Arc; |
5 | |
6 | use fontdb::{Database, ID}; |
7 | use svgtypes::FontFamily; |
8 | |
9 | use self::layout::DatabaseExt; |
10 | use crate::{Font, FontStretch, FontStyle, Text}; |
11 | |
12 | mod flatten; |
13 | |
14 | mod colr; |
15 | /// Provides access to the layout of a text node. |
16 | pub 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. |
36 | pub 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. |
50 | pub 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. |
60 | pub 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 | |
70 | impl 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 | |
79 | impl 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 | |
192 | impl 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. |
204 | pub(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 | |