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}