Skip to main content

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