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