usvg/text/
mod.rs

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("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, 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}