cosmic_text/font/
mod.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use harfrust::Shaper;
4use linebender_resource_handle::{Blob, FontData};
5use skrifa::raw::{ReadError, TableProvider as _};
6use skrifa::{metrics::Metrics, prelude::*};
7// re-export skrifa
8pub use skrifa;
9// re-export peniko::Font;
10#[cfg(feature = "peniko")]
11pub use linebender_resource_handle::FontData as PenikoFont;
12
13use core::fmt;
14
15use alloc::sync::Arc;
16#[cfg(not(feature = "std"))]
17use alloc::vec::Vec;
18use fontdb::Style;
19use self_cell::self_cell;
20
21pub mod fallback;
22pub use fallback::{Fallback, PlatformFallback};
23
24pub use self::system::*;
25mod system;
26
27struct OwnedFaceData {
28    data: Arc<dyn AsRef<[u8]> + Send + Sync>,
29    shaper_data: harfrust::ShaperData,
30    shaper_instance: harfrust::ShaperInstance,
31    metrics: Metrics,
32}
33
34self_cell!(
35    struct OwnedFace {
36        owner: OwnedFaceData,
37
38        #[covariant]
39        dependent: Shaper,
40    }
41);
42
43struct FontMonospaceFallback {
44    monospace_em_width: Option<f32>,
45    scripts: Vec<[u8; 4]>,
46    unicode_codepoints: Vec<u32>,
47}
48
49/// A font
50pub struct Font {
51    #[cfg(feature = "swash")]
52    swash: (u32, swash::CacheKey),
53    harfrust: OwnedFace,
54    data: FontData,
55    id: fontdb::ID,
56    monospace_fallback: Option<FontMonospaceFallback>,
57    pub(crate) italic_or_oblique: bool,
58}
59
60impl fmt::Debug for Font {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        f.debug_struct("Font")
63            .field("id", &self.id)
64            .finish_non_exhaustive()
65    }
66}
67
68impl Font {
69    pub const fn id(&self) -> fontdb::ID {
70        self.id
71    }
72
73    pub fn monospace_em_width(&self) -> Option<f32> {
74        self.monospace_fallback
75            .as_ref()
76            .and_then(|x| x.monospace_em_width)
77    }
78
79    pub fn scripts(&self) -> &[[u8; 4]] {
80        self.monospace_fallback.as_ref().map_or(&[], |x| &x.scripts)
81    }
82
83    pub fn unicode_codepoints(&self) -> &[u32] {
84        self.monospace_fallback
85            .as_ref()
86            .map_or(&[], |x| &x.unicode_codepoints)
87    }
88
89    pub fn data(&self) -> &[u8] {
90        self.data.data.data()
91    }
92
93    pub fn shaper(&self) -> &harfrust::Shaper<'_> {
94        self.harfrust.borrow_dependent()
95    }
96
97    pub(crate) fn shaper_instance(&self) -> &harfrust::ShaperInstance {
98        &self.harfrust.borrow_owner().shaper_instance
99    }
100
101    pub fn metrics(&self) -> &Metrics {
102        &self.harfrust.borrow_owner().metrics
103    }
104
105    #[cfg(feature = "peniko")]
106    pub fn as_peniko(&self) -> PenikoFont {
107        self.data.clone()
108    }
109
110    #[cfg(feature = "swash")]
111    pub fn as_swash(&self) -> swash::FontRef<'_> {
112        let swash = &self.swash;
113        swash::FontRef {
114            data: self.data(),
115            offset: swash.0,
116            key: swash.1,
117        }
118    }
119}
120
121impl Font {
122    pub fn new(db: &fontdb::Database, id: fontdb::ID, weight: fontdb::Weight) -> Option<Self> {
123        let info = db.face(id)?;
124
125        let data = match &info.source {
126            fontdb::Source::Binary(data) => Arc::clone(data),
127            #[cfg(feature = "std")]
128            fontdb::Source::File(path) => {
129                log::warn!("Unsupported fontdb Source::File('{}')", path.display());
130                return None;
131            }
132            #[cfg(feature = "std")]
133            fontdb::Source::SharedFile(_path, data) => Arc::clone(data),
134        };
135
136        // It's a bit unfortunate but we need to parse the data into a `FontRef`
137        // twice--once to construct the HarfRust `ShaperInstance` and
138        // `ShaperData`, and once to create the persistent `FontRef` tied to the
139        // lifetime of the face data.
140        let font_ref = FontRef::from_index((*data).as_ref(), info.index).ok()?;
141        let location = font_ref
142            .axes()
143            .location([(Tag::new(b"wght"), weight.0 as f32)]);
144        let metrics = font_ref.metrics(Size::unscaled(), &location);
145
146        let monospace_fallback = if cfg!(feature = "monospace_fallback") {
147            (|| {
148                let glyph_metrics = font_ref.glyph_metrics(Size::unscaled(), &location);
149                let charmap = font_ref.charmap();
150                let monospace_em_width = info
151                    .monospaced
152                    .then(|| {
153                        let hor_advance = glyph_metrics.advance_width(charmap.map(' ')?)?;
154                        let upem = metrics.units_per_em;
155                        Some(hor_advance / f32::from(upem))
156                    })
157                    .flatten();
158
159                if info.monospaced && monospace_em_width.is_none() {
160                    None?;
161                }
162
163                let scripts = font_ref
164                    .gpos()
165                    .ok()?
166                    .script_list()
167                    .ok()?
168                    .script_records()
169                    .iter()
170                    .chain(
171                        font_ref
172                            .gsub()
173                            .ok()?
174                            .script_list()
175                            .ok()?
176                            .script_records()
177                            .iter(),
178                    )
179                    .map(|script| script.script_tag().into_bytes())
180                    .collect();
181
182                let mut unicode_codepoints = Vec::new();
183
184                for (code_point, _) in charmap.mappings() {
185                    unicode_codepoints.push(code_point);
186                }
187
188                unicode_codepoints.shrink_to_fit();
189
190                Some(FontMonospaceFallback {
191                    monospace_em_width,
192                    scripts,
193                    unicode_codepoints,
194                })
195            })()
196        } else {
197            None
198        };
199
200        let (shaper_instance, shaper_data) = {
201            (
202                harfrust::ShaperInstance::from_coords(&font_ref, location.coords().iter().copied()),
203                harfrust::ShaperData::new(&font_ref),
204            )
205        };
206
207        Some(Self {
208            id: info.id,
209            monospace_fallback,
210            #[cfg(feature = "swash")]
211            swash: {
212                let swash = swash::FontRef::from_index((*data).as_ref(), info.index as usize)?;
213                (swash.offset, swash.key)
214            },
215            harfrust: OwnedFace::try_new(
216                OwnedFaceData {
217                    data: Arc::clone(&data),
218                    shaper_data,
219                    shaper_instance,
220                    metrics,
221                },
222                |OwnedFaceData {
223                     data,
224                     shaper_data,
225                     shaper_instance,
226                     ..
227                 }| {
228                    let font_ref = FontRef::from_index((**data).as_ref(), info.index)?;
229                    let shaper = shaper_data
230                        .shaper(&font_ref)
231                        .instance(Some(shaper_instance))
232                        .build();
233                    Ok::<_, ReadError>(shaper)
234                },
235            )
236            .ok()?,
237            data: FontData::new(Blob::new(data), info.index),
238            italic_or_oblique: info.style == Style::Italic || info.style == Style::Oblique,
239        })
240    }
241}
242
243#[cfg(test)]
244mod test {
245    #[test]
246    fn test_fonts_load_time() {
247        use crate::FontSystem;
248        use sys_locale::get_locale;
249
250        #[cfg(not(target_arch = "wasm32"))]
251        let now = std::time::Instant::now();
252
253        let mut db = fontdb::Database::new();
254        let locale = get_locale().expect("Local available");
255        db.load_system_fonts();
256        FontSystem::new_with_locale_and_db(locale, db);
257
258        #[cfg(not(target_arch = "wasm32"))]
259        println!("Fonts load time {}ms.", now.elapsed().as_millis());
260    }
261}