Skip to main content

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}