Skip to main content

cranpose_render_common/
font_source.rs

1//! Turning app-declared font families into parsed faces.
2//!
3//! [`FontFamily::FileBacked`] and [`FontFamily::LoadedTypeface`] name faces by
4//! file rather than by a name inside the font, so something has to read those
5//! files and hand the bytes to the rasterizer. That is this module: it parses
6//! each face exactly once, at registration, and produces an immutable
7//! [`SoftwareTextFontSet`] that measurement and rasterization then share.
8//! The process keeps one copy of each font file's contents, shared by every
9//! face instanced from it and by later reads that find the file unchanged.
10//!
11//! Nothing here runs per frame or per string. A registry is built at startup,
12//! consumed into a font set, and the font set is cloned (it is `Arc`-backed)
13//! into every measurer and rasterizer that needs it.
14//!
15//! Fonts that are not files on disk come in through
16//! [`SoftwareTextFontRegistry::register_face_reader`] or
17//! [`SoftwareTextFontRegistry::register_face_bytes`]: an APK asset opened with
18//! `AndroidApp::asset_manager()`, or anything `cranpose-assets` resolved out of
19//! a desktop bundle. `cranpose-assets` is a filesystem path resolver, so it
20//! covers bundles but not APK entries, which are not filesystem paths.
21
22use std::{
23    io::Read,
24    path::{Path, PathBuf},
25    sync::{Mutex, PoisonError},
26};
27
28use cranpose_ui::text::{FontFamily, FontFile, FontStyle, FontWeight};
29
30use crate::software_text_raster::{
31    FontBytes, FontFamilyKey, SoftwareTextFont, SoftwareTextFontError, SoftwareTextFontSet,
32};
33
34/// Directory Android keeps its system font files in.
35pub const ANDROID_SYSTEM_FONT_DIR: &str = "/system/fonts";
36
37/// The weights [`SoftwareTextFontRegistry::register_system_family`] registers
38/// when an app does not name its own: Compose's Regular/Medium/Bold set.
39pub const DEFAULT_SYSTEM_FAMILY_WEIGHTS: &[FontWeight] =
40    &[FontWeight::NORMAL, FontWeight::MEDIUM, FontWeight::BOLD];
41
42/// Why an app-supplied face could not be registered.
43///
44/// Every variant is recoverable: the caller logs it and keeps whatever faces
45/// did load, and resolution falls back to the default face for families that
46/// ended up with none.
47#[derive(Debug, thiserror::Error)]
48pub enum FontLoadError {
49    /// The family declares an empty list of faces.
50    #[error("font family declares no faces")]
51    EmptyFamily,
52    /// The family is not backed by font files, so there is nothing to read from disk.
53    #[error("font family is not backed by files, so it has nothing to load")]
54    NotFileBacked,
55    /// No file for this family exists in the system font directory.
56    #[error("no system font file for this family under {directory}")]
57    NoSystemFontFile {
58        /// The system font directory that was searched.
59        directory: PathBuf,
60    },
61    /// A font file could not be read from the filesystem.
62    #[error("failed to read font file {path}: {source}")]
63    Read {
64        /// Path of the file that failed to read.
65        path: PathBuf,
66        /// The operating system error.
67        #[source]
68        source: std::io::Error,
69    },
70    /// A font file was read but its contents are not a parsable font.
71    #[error("failed to parse font file {path}: {source}")]
72    Parse {
73        /// Path of the file that failed to parse.
74        path: PathBuf,
75        /// The parse failure from the font parser.
76        #[source]
77        source: SoftwareTextFontError,
78    },
79    /// The supplied bytes are not a parsable font, or the variation axes are invalid.
80    #[error("failed to parse font bytes: {source}")]
81    ParseBytes {
82        /// The parse failure from the font parser.
83        #[source]
84        source: SoftwareTextFontError,
85    },
86}
87
88/// Parsed app-supplied faces, on their way to a [`SoftwareTextFontSet`].
89///
90/// Register everything an app needs once at startup, then call
91/// [`SoftwareTextFontRegistry::into_font_set`]. Registration is
92/// where files are read and faces parsed; nothing after it touches the disk.
93#[derive(Clone, Default)]
94pub struct SoftwareTextFontRegistry {
95    faces: Vec<SoftwareTextFont>,
96    system_faces: Vec<(FontFamilyKey, FontWeight, FontStyle)>,
97}
98
99#[derive(Default)]
100struct TolerantLoad {
101    first_error: Option<FontLoadError>,
102    loaded: usize,
103}
104
105impl TolerantLoad {
106    fn record(&mut self, result: Result<(), FontLoadError>) {
107        match result {
108            Ok(()) => self.loaded += 1,
109            Err(error) => self.first_error = self.first_error.take().or(Some(error)),
110        }
111    }
112
113    fn finish(self) -> Result<(), FontLoadError> {
114        match self.first_error {
115            Some(error) if self.loaded == 0 => Err(error),
116            _ => Ok(()),
117        }
118    }
119}
120
121impl SoftwareTextFontRegistry {
122    /// Creates an empty registry.
123    pub fn new() -> Self {
124        Self::default()
125    }
126
127    /// Register every face of a file-backed family, reading each file from the
128    /// filesystem.
129    ///
130    /// Each [`FontFile`]'s declared weight and style are what resolution
131    /// matches on, so one family can carry Regular/Medium/Bold/Italic faces and
132    /// a `FontWeight`/`FontStyle` picks between them. A family whose files all
133    /// fail to load registers nothing and reports the first failure; text
134    /// asking for it then falls back to the default face rather than
135    /// disappearing.
136    pub fn register_family(&mut self, family: &FontFamily) -> Result<(), FontLoadError> {
137        let files = font_files_for(family)?;
138        if files.is_empty() {
139            return Err(FontLoadError::EmptyFamily);
140        }
141
142        let mut load = TolerantLoad::default();
143        for file in &files {
144            load.record(self.register_read_face(
145                family,
146                file.weight,
147                file.style,
148                Path::new(&file.path),
149                &[],
150            ));
151        }
152        load.finish()
153    }
154
155    fn register_read_face(
156        &mut self,
157        family: &FontFamily,
158        weight: FontWeight,
159        style: FontStyle,
160        path: &Path,
161        variations: &[([u8; 4], f32)],
162    ) -> Result<(), FontLoadError> {
163        let bytes = read_font_file(path)?;
164        let face = SoftwareTextFont::from_registered_bytes_with_variations(
165            family, weight, style, bytes, variations,
166        )
167        .map_err(|source| FontLoadError::Parse {
168            path: path.to_path_buf(),
169            source,
170        })?;
171        self.faces.push(face);
172        Ok(())
173    }
174
175    /// Register one face read from an arbitrary stream.
176    ///
177    /// This is the seam for fonts that are not files on disk — an APK asset
178    /// opened through `AndroidApp::asset_manager()`, an archive entry, a
179    /// download cache. It mirrors
180    /// `SoftwareTextMeasurer::register_hyphenation_dictionary_reader`.
181    pub fn register_face_reader(
182        &mut self,
183        family: &FontFamily,
184        weight: FontWeight,
185        style: FontStyle,
186        reader: &mut impl Read,
187    ) -> Result<(), FontLoadError> {
188        let mut bytes = Vec::new();
189        reader
190            .read_to_end(&mut bytes)
191            .map_err(|source| FontLoadError::Read {
192                path: PathBuf::new(),
193                source,
194            })?;
195        self.register_face_bytes(family, weight, style, bytes)
196    }
197
198    /// Register one face from bytes the app already holds.
199    pub fn register_face_bytes(
200        &mut self,
201        family: &FontFamily,
202        weight: FontWeight,
203        style: FontStyle,
204        bytes: impl Into<FontBytes>,
205    ) -> Result<(), FontLoadError> {
206        self.register_face_bytes_with_variations(family, weight, style, bytes, &[])
207    }
208
209    /// Register bytes with explicit OpenType axes, overriding the declared weight/style
210    /// coordinates. Invalid axes fail without registering a face.
211    pub fn register_face_bytes_with_variations(
212        &mut self,
213        family: &FontFamily,
214        weight: FontWeight,
215        style: FontStyle,
216        bytes: impl Into<FontBytes>,
217        variations: &[([u8; 4], f32)],
218    ) -> Result<(), FontLoadError> {
219        let face = SoftwareTextFont::from_registered_bytes_with_variations(
220            family, weight, style, bytes, variations,
221        )
222        .map_err(|source| FontLoadError::ParseBytes { source })?;
223        self.faces.push(face);
224        Ok(())
225    }
226
227    /// Register a face that belongs to no declared family.
228    ///
229    /// These are the fallbacks: they are eligible for any request that names no
230    /// family, and for a `Named` request their own `name` table decides.
231    pub fn register_fallback_bytes(
232        &mut self,
233        bytes: impl Into<FontBytes>,
234    ) -> Result<(), FontLoadError> {
235        let face = SoftwareTextFont::from_bytes(bytes)
236            .map_err(|source| FontLoadError::ParseBytes { source })?;
237        self.faces.push(face);
238        Ok(())
239    }
240
241    /// Register the platform's own face for a generic family alias, at each of
242    /// `weights`, so styles keep naming `FontFamily::SansSerif` and get the
243    /// real system typeface.
244    ///
245    /// Android backs `sans-serif` with a single variable `Roboto-Regular.ttf`
246    /// and describes each weight as a `wght` axis position on it, so most
247    /// devices resolve every weight to one file instanced several ways. Where a
248    /// build does ship weight-specific static files (`Roboto-Medium.ttf`), they
249    /// are preferred. Each weight is first resolved through
250    /// [`system_declared_weight`], so a request the platform's font config does
251    /// not declare registers the face Android would have returned for it rather
252    /// than a `wght` position Android cannot reach. Faces are registered in
253    /// `FontStyle::Normal`; an app that
254    /// wants a real italic rather than a synthesized slant should call
255    /// [`SoftwareTextFontRegistry::register_system_face`] for it.
256    pub fn register_system_family(
257        &mut self,
258        directory: impl AsRef<Path>,
259        family: &FontFamily,
260        weights: &[FontWeight],
261    ) -> Result<(), FontLoadError> {
262        let directory = directory.as_ref();
263        let mut load = TolerantLoad::default();
264        for weight in weights {
265            load.record(self.register_read_system_face(
266                directory,
267                family,
268                *weight,
269                FontStyle::Normal,
270                &[],
271            ));
272        }
273        load.finish()
274    }
275
276    /// Register one weight/style of a generic family alias from the platform's
277    /// font directory.
278    ///
279    /// `weight` is resolved through [`system_declared_weight`] before anything
280    /// is read, so the registered face is one the platform's font config
281    /// declares. Use [`Self::register_system_face_with_variations`] when matching
282    /// explicit coordinates supplied by the platform's text API.
283    pub fn register_system_face(
284        &mut self,
285        directory: impl AsRef<Path>,
286        family: &FontFamily,
287        weight: FontWeight,
288        style: FontStyle,
289    ) -> Result<(), FontLoadError> {
290        self.register_system_face_with_variations(directory, family, weight, style, &[])
291    }
292
293    /// Register a system face at explicit OpenType coordinates, such as the optical
294    /// size and weight returned by a platform text API. The first registration of a
295    /// family/weight/style wins, as with [`Self::register_system_face`].
296    pub fn register_system_face_with_variations(
297        &mut self,
298        directory: impl AsRef<Path>,
299        family: &FontFamily,
300        weight: FontWeight,
301        style: FontStyle,
302        variations: &[([u8; 4], f32)],
303    ) -> Result<(), FontLoadError> {
304        self.register_read_system_face(directory.as_ref(), family, weight, style, variations)
305    }
306
307    fn register_read_system_face(
308        &mut self,
309        directory: &Path,
310        family: &FontFamily,
311        weight: FontWeight,
312        style: FontStyle,
313        variations: &[([u8; 4], f32)],
314    ) -> Result<(), FontLoadError> {
315        let weight = system_declared_weight(family, weight);
316        if self.has_system_face(family, weight, style) {
317            return Ok(());
318        }
319        let path = system_font_file(directory, family, weight).ok_or_else(|| {
320            FontLoadError::NoSystemFontFile {
321                directory: directory.to_path_buf(),
322            }
323        })?;
324        self.register_read_face(family, weight, style, &path, variations)?;
325        self.system_faces
326            .push((FontFamilyKey::of(family), weight, style));
327        Ok(())
328    }
329
330    fn has_system_face(&self, family: &FontFamily, weight: FontWeight, style: FontStyle) -> bool {
331        self.system_faces
332            .contains(&(FontFamilyKey::of(family), weight, style))
333    }
334
335    /// The faces registered so far.
336    pub fn faces(&self) -> &[SoftwareTextFont] {
337        &self.faces
338    }
339
340    /// Whether no face has been registered yet.
341    pub fn is_empty(&self) -> bool {
342        self.faces.is_empty()
343    }
344
345    /// Finish, folding in the static byte slices from `AppLauncher::with_fonts`
346    /// as unregistered fallbacks.
347    ///
348    /// Registered faces come first, so when a request names no family and the
349    /// scores tie, a face the app declared wins over one it merely handed over
350    /// as bytes. The set holds only what the app supplied; whether the
351    /// embedded face serves an app that supplied nothing is the launcher's
352    /// decision, made where the binary can leave the face out.
353    pub fn into_font_set(mut self, fonts: &[&'static [u8]]) -> SoftwareTextFontSet {
354        for bytes in fonts {
355            let _ = self.register_fallback_bytes(*bytes);
356        }
357        SoftwareTextFontSet::from_faces(self.faces)
358    }
359}
360
361struct ReadFontFile {
362    path: PathBuf,
363    bytes: &'static [u8],
364}
365
366static READ_FONT_FILES: Mutex<Vec<ReadFontFile>> = Mutex::new(Vec::new());
367
368pub(crate) fn read_font_file(path: &Path) -> Result<&'static [u8], FontLoadError> {
369    let mut read = std::fs::read(path).map_err(|source| FontLoadError::Read {
370        path: path.to_path_buf(),
371        source,
372    })?;
373    // Shortened here, while the bytes are still ours, the faces read them in
374    // place with no copy.
375    crate::full_glyph_count::fit_glyph_tables(&mut read);
376    let mut files = READ_FONT_FILES
377        .lock()
378        .unwrap_or_else(PoisonError::into_inner);
379    if let Some(file) = files
380        .iter()
381        .find(|file| file.path == path && file.bytes == read.as_slice())
382    {
383        return Ok(file.bytes);
384    }
385    let bytes: &'static [u8] = Box::leak(read.into_boxed_slice());
386    files.push(ReadFontFile {
387        path: path.to_path_buf(),
388        bytes,
389    });
390    Ok(bytes)
391}
392
393/// The weight a platform's own matcher resolves `weight` to for a generic
394/// family alias — never a value that family's font config does not declare.
395///
396/// Android's `sans-serif` is one variable `Roboto-Regular.ttf` described by
397/// `/system/etc/fonts.xml` as nine `<font weight="…">` entries, one per hundred,
398/// each pinning `wght` to that hundred. The `wght` axis is therefore reachable
399/// to the *font config*, not to a caller: an app asking `sans-serif` for 450
400/// gets whichever declared entry Minikin's matcher picks, and Minikin has no
401/// way to express a weight between two entries. Instancing the axis at 450
402/// anyway draws a face the platform cannot draw — text that measures and lays
403/// out perfectly and is simply heavier than every other app on the device.
404///
405/// The rule is `computeMatch` in `frameworks/minikin/libs/minikin/FontFamily.cpp`:
406///
407/// ```text
408/// int score = abs(style1.weight() / 100 - style2.weight() / 100);
409/// if (style1.slant() != style2.slant()) score += 2;
410/// ```
411///
412/// picked by `getClosestMatch`, which keeps a candidate only on `match <
413/// bestMatch`. Two consequences carry the behaviour, and both are load-bearing:
414///
415/// * The division is **integer**, so the request is truncated to its hundred
416///   before anything is compared. Against a full hundreds grid the nearest
417///   declared entry is therefore always the hundred *below* — 450 resolves to
418///   400 and 550 to 500, not by a tie-break but outright, and 599 resolves to
419///   500 too.
420/// * Where a request does tie between two declared entries — 500 against a
421///   `serif` declaring only 400 and 700 scores 1 and 2, but 600 scores 2 and 1,
422///   and a family declaring 300 and 500 ties at 400 — the strict `<` keeps the
423///   entry declared **first**. Android declares ascending, so a tie goes to the
424///   lighter face.
425///
426/// A slant mismatch costs 2, i.e. 200 weight units, which is why `style` never
427/// competes with `weight` here: this resolves within one slant, as
428/// [`SoftwareTextFontRegistry::register_system_face`] registers one.
429///
430/// Families this crate knows no system files for resolve to `weight` unchanged;
431/// there is no declared set to honour, and nothing will register for them.
432///
433/// This applies to the system-font path alone. A face an app supplies is its
434/// own font, not an entry in the platform's config, and stays instanceable at
435/// any axis value — that is what a variable font is for.
436pub fn system_declared_weight(family: &FontFamily, weight: FontWeight) -> FontWeight {
437    let Some(files) = system_family_files(family) else {
438        return weight;
439    };
440    closest_declared_weight(files.declared, weight).unwrap_or(weight)
441}
442
443fn closest_declared_weight(declared: &[u16], requested: FontWeight) -> Option<FontWeight> {
444    let mut best: Option<(u16, u16)> = None;
445    for candidate in declared {
446        let score = weight_match_score(*candidate, requested.value());
447        if best.is_none_or(|(_, best_score)| score < best_score) {
448            best = Some((*candidate, score));
449        }
450    }
451    best.map(|(candidate, _)| FontWeight(candidate))
452}
453
454fn weight_match_score(declared: u16, requested: u16) -> u16 {
455    (declared / 100).abs_diff(requested / 100)
456}
457
458/// The file a platform backs `family` with at `weight`, if one is present.
459///
460/// Weight-specific static files win when the build ships them; otherwise the
461/// family's regular file is returned and instanced on its `wght` axis at
462/// registration. `weight` is expected to be one the family declares — callers
463/// on the system path run it through [`system_declared_weight`] first.
464pub fn system_font_file(
465    directory: &Path,
466    family: &FontFamily,
467    weight: FontWeight,
468) -> Option<PathBuf> {
469    let files = system_family_files(family)?;
470    files
471        .weighted
472        .iter()
473        .filter(|(candidate_weight, _)| *candidate_weight == weight.value())
474        .map(|(_, name)| directory.join(name))
475        .chain(files.regular.iter().map(|name| directory.join(name)))
476        .find(|path| path.is_file())
477}
478
479struct SystemFamilyFiles {
480    regular: &'static [&'static str],
481    weighted: &'static [(u16, &'static str)],
482    declared: &'static [u16],
483}
484
485const DECLARED_HUNDREDS: &[u16] = &[100, 200, 300, 400, 500, 600, 700, 800, 900];
486
487fn system_family_files(family: &FontFamily) -> Option<SystemFamilyFiles> {
488    match family {
489        FontFamily::Default | FontFamily::SansSerif => Some(SystemFamilyFiles {
490            regular: &[
491                "Roboto-Regular.ttf",
492                "RobotoStatic-Regular.ttf",
493                "NotoSans-Regular.ttf",
494                "DroidSans.ttf",
495                "Core/SFUI.ttf",
496                "SFNS.ttf",
497            ],
498            weighted: &[
499                (300, "Roboto-Light.ttf"),
500                (500, "Roboto-Medium.ttf"),
501                (700, "Roboto-Bold.ttf"),
502                (900, "Roboto-Black.ttf"),
503            ],
504            declared: DECLARED_HUNDREDS,
505        }),
506        FontFamily::Serif | FontFamily::Fantasy => Some(SystemFamilyFiles {
507            regular: &["NotoSerif-Regular.ttf", "DroidSerif-Regular.ttf"],
508            weighted: &[(700, "NotoSerif-Bold.ttf"), (700, "DroidSerif-Bold.ttf")],
509            declared: &[400, 700],
510        }),
511        FontFamily::Monospace => Some(SystemFamilyFiles {
512            regular: &[
513                "DroidSansMono.ttf",
514                "RobotoMono-Regular.ttf",
515                "CutiveMono-Regular.ttf",
516            ],
517            weighted: &[(700, "RobotoMono-Bold.ttf")],
518            declared: &[400, 700],
519        }),
520        FontFamily::Cursive => Some(SystemFamilyFiles {
521            regular: &["DancingScript-Regular.ttf"],
522            weighted: &[(700, "DancingScript-Bold.ttf")],
523            declared: &[400, 700],
524        }),
525        FontFamily::Named(_) | FontFamily::FileBacked(_) | FontFamily::LoadedTypeface(_) => None,
526    }
527}
528
529fn font_files_for(family: &FontFamily) -> Result<Vec<FontFile>, FontLoadError> {
530    match family {
531        FontFamily::FileBacked(file_backed) => Ok(file_backed.fonts.clone()),
532        FontFamily::LoadedTypeface(typeface) => Ok(vec![FontFile::new(typeface.path.clone())]),
533        _ => Err(FontLoadError::NotFileBacked),
534    }
535}
536
537#[cfg(test)]
538#[path = "tests/font_source_tests.rs"]
539mod tests;