| 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 | |