cosmic_text/font/
mod.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3// re-export ttf_parser
4pub use ttf_parser;
5// re-export peniko::Font;
6#[cfg(feature = "peniko")]
7pub use peniko::Font as PenikoFont;
8
9use core::fmt;
10
11use alloc::sync::Arc;
12#[cfg(not(feature = "std"))]
13use alloc::vec::Vec;
14
15use rustybuzz::Face as RustybuzzFace;
16use self_cell::self_cell;
17
18pub(crate) mod fallback;
19pub use fallback::{Fallback, PlatformFallback};
20
21pub use self::system::*;
22mod system;
23
24self_cell!(
25    struct OwnedFace {
26        owner: Arc<dyn AsRef<[u8]> + Send + Sync>,
27
28        #[covariant]
29        dependent: RustybuzzFace,
30    }
31);
32
33struct FontMonospaceFallback {
34    monospace_em_width: Option<f32>,
35    scripts: Vec<[u8; 4]>,
36    unicode_codepoints: Vec<u32>,
37}
38
39/// A font
40pub struct Font {
41    #[cfg(feature = "swash")]
42    swash: (u32, swash::CacheKey),
43    rustybuzz: OwnedFace,
44    #[cfg(not(feature = "peniko"))]
45    data: Arc<dyn AsRef<[u8]> + Send + Sync>,
46    #[cfg(feature = "peniko")]
47    data: peniko::Font,
48    id: fontdb::ID,
49    monospace_fallback: Option<FontMonospaceFallback>,
50}
51
52impl fmt::Debug for Font {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        f.debug_struct("Font")
55            .field("id", &self.id)
56            .finish_non_exhaustive()
57    }
58}
59
60impl Font {
61    pub fn id(&self) -> fontdb::ID {
62        self.id
63    }
64
65    pub fn monospace_em_width(&self) -> Option<f32> {
66        self.monospace_fallback
67            .as_ref()
68            .and_then(|x| x.monospace_em_width)
69    }
70
71    pub fn scripts(&self) -> &[[u8; 4]] {
72        self.monospace_fallback.as_ref().map_or(&[], |x| &x.scripts)
73    }
74
75    pub fn unicode_codepoints(&self) -> &[u32] {
76        self.monospace_fallback
77            .as_ref()
78            .map_or(&[], |x| &x.unicode_codepoints)
79    }
80
81    pub fn data(&self) -> &[u8] {
82        #[cfg(not(feature = "peniko"))]
83        {
84            (*self.data).as_ref()
85        }
86        #[cfg(feature = "peniko")]
87        {
88            self.data.data.data()
89        }
90    }
91
92    pub fn rustybuzz(&self) -> &RustybuzzFace<'_> {
93        self.rustybuzz.borrow_dependent()
94    }
95
96    #[cfg(feature = "peniko")]
97    pub fn as_peniko(&self) -> PenikoFont {
98        self.data.clone()
99    }
100
101    #[cfg(feature = "swash")]
102    pub fn as_swash(&self) -> swash::FontRef<'_> {
103        let swash = &self.swash;
104        swash::FontRef {
105            data: self.data(),
106            offset: swash.0,
107            key: swash.1,
108        }
109    }
110}
111
112impl Font {
113    pub fn new(db: &fontdb::Database, id: fontdb::ID) -> Option<Self> {
114        let info = db.face(id)?;
115
116        let monospace_fallback = if cfg!(feature = "monospace_fallback") {
117            db.with_face_data(id, |font_data, face_index| {
118                let face = ttf_parser::Face::parse(font_data, face_index).ok()?;
119                let monospace_em_width = info
120                    .monospaced
121                    .then(|| {
122                        let hor_advance = face.glyph_hor_advance(face.glyph_index(' ')?)? as f32;
123                        let upem = face.units_per_em() as f32;
124                        Some(hor_advance / upem)
125                    })
126                    .flatten();
127
128                if info.monospaced && monospace_em_width.is_none() {
129                    None?;
130                }
131
132                let scripts = face
133                    .tables()
134                    .gpos
135                    .into_iter()
136                    .chain(face.tables().gsub)
137                    .flat_map(|table| table.scripts)
138                    .map(|script| script.tag.to_bytes())
139                    .collect();
140
141                let mut unicode_codepoints = Vec::new();
142
143                face.tables()
144                    .cmap?
145                    .subtables
146                    .into_iter()
147                    .filter(|subtable| subtable.is_unicode())
148                    .for_each(|subtable| {
149                        unicode_codepoints.reserve(1024);
150                        subtable.codepoints(|code_point| {
151                            if subtable.glyph_index(code_point).is_some() {
152                                unicode_codepoints.push(code_point);
153                            }
154                        });
155                    });
156
157                unicode_codepoints.shrink_to_fit();
158
159                Some(FontMonospaceFallback {
160                    monospace_em_width,
161                    scripts,
162                    unicode_codepoints,
163                })
164            })?
165        } else {
166            None
167        };
168
169        let data = match &info.source {
170            fontdb::Source::Binary(data) => Arc::clone(data),
171            #[cfg(feature = "std")]
172            fontdb::Source::File(path) => {
173                log::warn!("Unsupported fontdb Source::File('{}')", path.display());
174                return None;
175            }
176            #[cfg(feature = "std")]
177            fontdb::Source::SharedFile(_path, data) => Arc::clone(data),
178        };
179
180        Some(Self {
181            id: info.id,
182            monospace_fallback,
183            #[cfg(feature = "swash")]
184            swash: {
185                let swash = swash::FontRef::from_index((*data).as_ref(), info.index as usize)?;
186                (swash.offset, swash.key)
187            },
188            rustybuzz: OwnedFace::try_new(Arc::clone(&data), |data| {
189                RustybuzzFace::from_slice((**data).as_ref(), info.index).ok_or(())
190            })
191            .ok()?,
192            #[cfg(not(feature = "peniko"))]
193            data,
194            #[cfg(feature = "peniko")]
195            data: peniko::Font::new(peniko::Blob::new(data), info.index),
196        })
197    }
198}
199
200#[cfg(test)]
201mod test {
202    #[test]
203    fn test_fonts_load_time() {
204        use crate::FontSystem;
205        use sys_locale::get_locale;
206
207        #[cfg(not(target_arch = "wasm32"))]
208        let now = std::time::Instant::now();
209
210        let mut db = fontdb::Database::new();
211        let locale = get_locale().expect("Local available");
212        db.load_system_fonts();
213        FontSystem::new_with_locale_and_db(locale, db);
214
215        #[cfg(not(target_arch = "wasm32"))]
216        println!("Fonts load time {}ms.", now.elapsed().as_millis());
217    }
218}