Skip to main content

valo_fontmgr/
android.rs

1//! Android's font configuration as the font manager.
2//!
3//! Android exposes no font API to a native program. It ships its fonts as a directory of
4//! files and one XML file that describes them, and every text stack on the platform reads
5//! that file, Skia's `SkFontMgr_android` included. It carries what the platform decides
6//! and a directory scan cannot know: which family a name such as `sans-serif` means, the
7//! other names that answer to it, and the order faces are tried in for a character the
8//! requested family has no glyph for.
9//!
10//! The file lists two kinds of family. A named one is what a request for that name
11//! answers with. An unnamed one is a link in the fallback chain, tagged with the
12//! languages it is for, and those are tried in the order the file lists them — the order
13//! the platform chose.
14
15use std::collections::HashMap;
16use std::path::PathBuf;
17
18use crate::files::{self, Files};
19use crate::{FontManager, Slant, Style, Typeface, Watch, NORMAL_WIDTH};
20
21/// Where the platform publishes its font configuration.
22const CONFIG: &str = "/system/etc/fonts.xml";
23/// Where the files the configuration names live.
24const FONT_DIR: &str = "/system/fonts";
25/// The family Android's user interface is set in, and what an unnamed request means.
26const DEFAULT_FAMILY: &str = "sans-serif";
27
28/// `Android` is the platform's font manager on Android.
29///
30/// The configuration is read once; a font file is read the first time one of its faces is
31/// answered. It builds on every system, since it reads files rather than calling an API,
32/// so a tool that cross-compiles for Android can read a configuration of its own with
33/// [`from_config`](Android::from_config); on a system that has no such file it knows no
34/// faces.
35pub struct Android {
36    families: Vec<Family>,
37    /// Every name a request may use — a family's own and each alias — lower-cased.
38    by_name: HashMap<String, Named>,
39    /// The families with no name of their own, in the order the configuration lists them:
40    /// the chain a character not covered by the requested family is looked for along.
41    fallback: Vec<usize>,
42    files: Files,
43    dir: PathBuf,
44}
45
46/// One family as the configuration lists it.
47struct Family {
48    /// The name a request answers to, or `None` for a link in the fallback chain.
49    name: Option<String>,
50    /// The languages this family is for, as the configuration tags them: a BCP 47 tag
51    /// such as `ja`, or `und-Arab` for a script with no language of its own.
52    languages: Vec<String>,
53    faces: Vec<Face>,
54}
55
56/// One face: the file it lives in, and the style the configuration gives it.
57struct Face {
58    file: String,
59    /// The face's index in a collection file; 0 for a file holding one face.
60    index: u32,
61    style: Style,
62}
63
64/// What a name resolves to: a family, and the one weight an alias narrows it to.
65#[derive(Clone, Copy)]
66struct Named {
67    family: usize,
68    /// `Some` for an alias that names one weight of its target, as `sans-serif-light`
69    /// names the 300 of `sans-serif`.
70    weight: Option<u16>,
71}
72
73impl Android {
74    /// `new` reads the platform's configuration now. It touches the file system, so read
75    /// it once and keep it.
76    pub fn new() -> Android {
77        let config = std::fs::read_to_string(CONFIG).unwrap_or_default();
78        Android::from_config(&config, PathBuf::from(FONT_DIR))
79    }
80
81    /// `from_config` reads a configuration whose files live in `dir`, for a test and for a
82    /// system that keeps them elsewhere.
83    pub fn from_config(xml: &str, dir: PathBuf) -> Android {
84        let Config { families, aliases } = parse(xml);
85        let mut by_name = HashMap::new();
86        for (index, family) in families.iter().enumerate() {
87            if let Some(name) = &family.name {
88                by_name.insert(
89                    name.to_lowercase(),
90                    Named {
91                        family: index,
92                        weight: None,
93                    },
94                );
95            }
96        }
97        // An alias names a family that the configuration lists before it, and may name
98        // another alias; resolving in order therefore reaches the family either way.
99        for alias in aliases {
100            let Some(target) = by_name.get(&alias.to.to_lowercase()).copied() else {
101                continue;
102            };
103            by_name.insert(
104                alias.name.to_lowercase(),
105                Named {
106                    family: target.family,
107                    weight: alias.weight.or(target.weight),
108                },
109            );
110        }
111        let fallback = families
112            .iter()
113            .enumerate()
114            .filter(|(_, family)| family.name.is_none())
115            .map(|(index, _)| index)
116            .collect();
117        Android {
118            families,
119            by_name,
120            fallback,
121            files: Files::default(),
122            dir,
123        }
124    }
125
126    /// The family a name means, with the weight an alias narrows it to.
127    fn named(&self, name: &str) -> Option<Named> {
128        self.by_name.get(&name.to_lowercase()).copied()
129    }
130
131    /// The faces of a family as typefaces, one per file rather than one per entry: a
132    /// variable file is listed once per weight it can be set to, and those weights are the
133    /// file's own to answer, not this manager's to multiply.
134    fn typefaces(&mut self, named: Named) -> Vec<Typeface> {
135        let family = &self.families[named.family];
136        let name = family.name.clone().unwrap_or_default();
137        let mut wanted: Vec<(String, u32, Style)> = Vec::new();
138        for face in &family.faces {
139            if named
140                .weight
141                .is_some_and(|weight| face.style.weight != weight)
142            {
143                continue;
144            }
145            if !wanted
146                .iter()
147                .any(|(file, index, _)| *file == face.file && *index == face.index)
148            {
149                wanted.push((face.file.clone(), face.index, face.style));
150            }
151        }
152        wanted
153            .into_iter()
154            .filter_map(|(file, index, style)| self.typeface(&name, &file, index, style))
155            .collect()
156    }
157
158    fn typeface(&mut self, family: &str, file: &str, index: u32, style: Style) -> Option<Typeface> {
159        let data = self.files.read(&self.dir.join(file))?;
160        Some(Typeface::new(data, index, family, style))
161    }
162
163    /// The families a character is looked for in, most preferred first: the requested one,
164    /// then the fallback chain with the links for the preferred languages brought forward,
165    /// then everything else the configuration lists.
166    fn search_order(&self, family: Option<&str>, locales: &[&str]) -> Vec<usize> {
167        let mut order: Vec<usize> = Vec::new();
168        let push = |index: usize, order: &mut Vec<usize>| {
169            if !order.contains(&index) {
170                order.push(index);
171            }
172        };
173        if let Some(named) = family.and_then(|name| self.named(name)) {
174            push(named.family, &mut order);
175        }
176        for locale in locales {
177            for &index in &self.fallback {
178                if speaks(&self.families[index], locale) {
179                    push(index, &mut order);
180                }
181            }
182        }
183        for &index in &self.fallback {
184            push(index, &mut order);
185        }
186        for index in 0..self.families.len() {
187            push(index, &mut order);
188        }
189        order
190    }
191}
192
193impl Default for Android {
194    fn default() -> Android {
195        Android::new()
196    }
197}
198
199impl FontManager for Android {
200    fn family(&mut self, family: &str) -> Vec<Typeface> {
201        match self.named(family) {
202            Some(named) => self.typefaces(named),
203            None => Vec::new(),
204        }
205    }
206
207    fn match_character(
208        &mut self,
209        family: Option<&str>,
210        style: Style,
211        locales: &[&str],
212        character: char,
213    ) -> Option<Typeface> {
214        for index in self.search_order(family, locales) {
215            let named = Named {
216                family: index,
217                weight: None,
218            };
219            let mut faces = self.typefaces(named);
220            // Nearest first, so the face answered is the closest one that covers.
221            faces.sort_by_key(|face| face.style().distance(style));
222            if let Some(covering) = faces
223                .into_iter()
224                .find(|face| files::covers(face.data(), face.index(), character))
225            {
226                return Some(covering);
227            }
228        }
229        None
230    }
231
232    /// Android's user-interface font is the default family; the platform names no size at
233    /// which it changes, as Apple's does.
234    fn system_font(&mut self, _size: f32, style: Style) -> Option<Typeface> {
235        crate::nearest(self.family(DEFAULT_FAMILY), style)
236    }
237
238    fn families(&self) -> Vec<String> {
239        let mut names: Vec<String> = self
240            .families
241            .iter()
242            .filter_map(|family| family.name.clone())
243            .collect();
244        names.sort();
245        names.dedup();
246        names
247    }
248
249    fn face_count(&self) -> usize {
250        self.families.iter().map(|family| family.faces.len()).sum()
251    }
252
253    /// The platform's fonts are part of the system image and do not change while it runs.
254    fn watch(&mut self, _on_change: Box<dyn Fn() + Send + Sync>) -> Option<Watch> {
255        None
256    }
257}
258
259/// Whether a family is for `locale`: the configuration tags a family with the language it
260/// serves, and a tag covers a locale that begins with it, so `zh-Hans` covers
261/// `zh-Hans-CN`. A tag for a script alone, `und-Arab`, names no language and is reached by
262/// coverage instead.
263fn speaks(family: &Family, locale: &str) -> bool {
264    let locale = locale.to_lowercase();
265    family.languages.iter().any(|language| {
266        let language = language.to_lowercase();
267        locale == language || locale.starts_with(&format!("{language}-"))
268    })
269}
270
271/// What the configuration says, as this manager reads it.
272#[derive(Default)]
273struct Config {
274    families: Vec<Family>,
275    aliases: Vec<Alias>,
276}
277
278struct Alias {
279    name: String,
280    to: String,
281    weight: Option<u16>,
282}
283
284/// Reads the part of the configuration this manager uses: `<family>`, its `<font>`
285/// children, and `<alias>`. The rest of the file — the axis values a variable file is set
286/// to, the PostScript names, which family a face is a fallback for — is skipped, because
287/// the font file answers for itself once it is read.
288fn parse(xml: &str) -> Config {
289    let mut config = Config::default();
290    let mut family: Option<Family> = None;
291    for tag in Tags::new(xml) {
292        match tag.name {
293            "family" if tag.closing => {
294                if let Some(family) = family.take() {
295                    config.families.push(family);
296                }
297            }
298            "family" => {
299                if let Some(family) = family.take() {
300                    config.families.push(family);
301                }
302                family = Some(Family {
303                    name: attribute(tag.attributes, "name"),
304                    languages: attribute(tag.attributes, "lang")
305                        .into_iter()
306                        .flat_map(|langs| {
307                            langs
308                                .split(',')
309                                .map(str::trim)
310                                .filter(|lang| !lang.is_empty())
311                                .map(str::to_owned)
312                                .collect::<Vec<String>>()
313                        })
314                        .collect(),
315                    faces: Vec::new(),
316                });
317            }
318            "font" if !tag.closing => {
319                if let (Some(family), Some(face)) = (family.as_mut(), face_of(&tag)) {
320                    family.faces.push(face);
321                }
322            }
323            "alias" if !tag.closing => {
324                if let (Some(name), Some(to)) = (
325                    attribute(tag.attributes, "name"),
326                    attribute(tag.attributes, "to"),
327                ) {
328                    config.aliases.push(Alias {
329                        name,
330                        to,
331                        weight: attribute(tag.attributes, "weight")
332                            .and_then(|weight| weight.parse().ok()),
333                    });
334                }
335            }
336            _ => {}
337        }
338    }
339    if let Some(family) = family {
340        config.families.push(family);
341    }
342    config
343}
344
345/// A `<font>` and the file name that follows it. A face with no file names nothing and is
346/// dropped.
347fn face_of(tag: &Tag) -> Option<Face> {
348    let file = tag.text.trim();
349    if file.is_empty() {
350        return None;
351    }
352    Some(Face {
353        file: file.to_owned(),
354        index: attribute(tag.attributes, "index")
355            .and_then(|index| index.parse().ok())
356            .unwrap_or(0),
357        style: Style {
358            weight: attribute(tag.attributes, "weight")
359                .and_then(|weight| weight.parse().ok())
360                .unwrap_or(400),
361            width: NORMAL_WIDTH,
362            slant: match attribute(tag.attributes, "style").as_deref() {
363                Some("italic") => Slant::Italic,
364                _ => Slant::Upright,
365            },
366        },
367    })
368}
369
370/// One tag of the configuration, with the text that follows it up to the next one.
371struct Tag<'a> {
372    name: &'a str,
373    /// Everything between the tag's name and its end, for [`attribute`] to read.
374    attributes: &'a str,
375    /// Whether this is a closing tag, `</family>`.
376    closing: bool,
377    /// The text between this tag and the next, which for a `<font>` is its file name.
378    text: &'a str,
379}
380
381/// The configuration's tags in order. Android generates this file, so it is the plain
382/// subset of XML the generator writes: no namespaces, no entities, no comments inside a
383/// tag.
384struct Tags<'a> {
385    rest: &'a str,
386}
387
388impl<'a> Tags<'a> {
389    fn new(xml: &'a str) -> Tags<'a> {
390        Tags { rest: xml }
391    }
392}
393
394impl<'a> Iterator for Tags<'a> {
395    type Item = Tag<'a>;
396
397    fn next(&mut self) -> Option<Tag<'a>> {
398        let start = self.rest.find('<')?;
399        let body = &self.rest[start + 1..];
400        let end = body.find('>')?;
401        let (body, after) = (&body[..end], &body[end + 1..]);
402        self.rest = after;
403        let text = match after.find('<') {
404            Some(next) => &after[..next],
405            None => after,
406        };
407        let closing = body.starts_with('/');
408        let body = body.trim_start_matches('/').trim_end_matches('/');
409        let name_end = body
410            .find(|c: char| c.is_ascii_whitespace())
411            .unwrap_or(body.len());
412        Some(Tag {
413            name: &body[..name_end],
414            attributes: &body[name_end..],
415            closing,
416            text,
417        })
418    }
419}
420
421/// The value of one attribute, read attribute by attribute so that a name is never found
422/// inside a longer one: `name` is not `postScriptName`.
423fn attribute(attributes: &str, wanted: &str) -> Option<String> {
424    let mut rest = attributes.trim_start();
425    while !rest.is_empty() {
426        let equals = rest.find('=')?;
427        let name = rest[..equals].trim();
428        let after = rest[equals + 1..].trim_start();
429        let quote = after.chars().next()?;
430        if quote != '"' && quote != '\'' {
431            return None;
432        }
433        let value_end = after[1..].find(quote)?;
434        let value = &after[1..1 + value_end];
435        if name == wanted {
436            return Some(value.to_owned());
437        }
438        rest = after[2 + value_end..].trim_start();
439    }
440    None
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446
447    /// The shapes Android's own file uses: a variable family listed once per weight, a
448    /// static family of four files, a collection face with an index, aliases with and
449    /// without a weight, and fallback families tagged by language and by script.
450    const CONFIG: &str = r#"<?xml version="1.0" encoding="utf-8"?>
451<familyset version="23">
452  <family name="sans-serif">
453    <font weight="400" style="normal">
454      Roboto-Regular.ttf
455      <axis tag="wght" stylevalue="400"/>
456    </font>
457    <font weight="700" style="normal">
458      Roboto-Regular.ttf
459      <axis tag="wght" stylevalue="700"/>
460    </font>
461  </family>
462  <family name="serif">
463    <font weight="400" style="normal" postScriptName="NotoSerif">NotoSerif-Regular.ttf</font>
464    <font weight="700" style="normal">NotoSerif-Bold.ttf</font>
465    <font weight="400" style="italic">NotoSerif-Italic.ttf</font>
466  </family>
467  <alias name="arial" to="sans-serif"/>
468  <alias name="sans-serif-light" to="sans-serif" weight="300"/>
469  <family lang="ja">
470    <font weight="400" style="normal" index="2" postScriptName="NotoSansCJKJP">NotoSansCJK-Regular.ttc</font>
471  </family>
472  <family lang="und-Arab" variant="elegant">
473    <font weight="400" style="normal">NotoNaskhArabic-Regular.ttf</font>
474  </family>
475</familyset>
476"#;
477
478    fn config() -> Config {
479        parse(CONFIG)
480    }
481
482    fn manager() -> Android {
483        Android::from_config(CONFIG, PathBuf::from("/system/fonts"))
484    }
485
486    #[test]
487    fn a_family_keeps_its_faces_with_their_files_and_styles() {
488        let config = config();
489        let serif = config
490            .families
491            .iter()
492            .find(|family| family.name.as_deref() == Some("serif"))
493            .expect("the named family");
494        assert_eq!(serif.faces.len(), 3);
495        assert_eq!(serif.faces[0].file, "NotoSerif-Regular.ttf");
496        assert_eq!(serif.faces[0].style.weight, 400);
497        assert_eq!(serif.faces[0].style.slant, Slant::Upright);
498        assert_eq!(serif.faces[2].style.slant, Slant::Italic);
499    }
500
501    #[test]
502    fn a_file_name_is_read_past_the_axes_that_follow_it() {
503        let config = config();
504        let sans = &config.families[0];
505        assert_eq!(
506            sans.faces.len(),
507            2,
508            "one entry per weight the file is set to"
509        );
510        for face in &sans.faces {
511            assert_eq!(face.file, "Roboto-Regular.ttf");
512        }
513    }
514
515    #[test]
516    fn a_collection_face_keeps_its_index() {
517        let config = config();
518        let japanese = config
519            .families
520            .iter()
521            .find(|family| family.languages.iter().any(|lang| lang == "ja"))
522            .expect("the fallback family");
523        assert_eq!(japanese.faces[0].index, 2);
524        assert_eq!(japanese.faces[0].file, "NotoSansCJK-Regular.ttc");
525    }
526
527    #[test]
528    fn an_alias_names_a_family_and_may_narrow_it_to_one_weight() {
529        let manager = manager();
530        let plain = manager.named("arial").expect("the alias");
531        assert_eq!(
532            manager.families[plain.family].name.as_deref(),
533            Some("sans-serif")
534        );
535        assert_eq!(plain.weight, None);
536        let light = manager.named("sans-serif-light").expect("the alias");
537        assert_eq!(light.weight, Some(300));
538    }
539
540    #[test]
541    fn a_name_is_matched_whatever_its_case_and_an_unknown_one_answers_nothing() {
542        let mut manager = manager();
543        assert!(manager.named("SANS-SERIF").is_some());
544        assert!(manager.named("Arial").is_some());
545        assert!(manager.named("comic sans").is_none());
546        assert!(manager.family("comic sans").is_empty());
547    }
548
549    #[test]
550    fn an_attribute_is_not_found_inside_a_longer_one() {
551        let attributes = r#" weight="400" style="normal" postScriptName="NotoSerif""#;
552        assert_eq!(attribute(attributes, "name"), None, "not postScriptName");
553        assert_eq!(
554            attribute(attributes, "postScriptName").as_deref(),
555            Some("NotoSerif")
556        );
557        assert_eq!(attribute(attributes, "weight").as_deref(), Some("400"));
558    }
559
560    #[test]
561    fn the_fallback_chain_is_the_unnamed_families_in_the_order_the_file_lists_them() {
562        let manager = manager();
563        assert_eq!(manager.fallback.len(), 2);
564        for &index in &manager.fallback {
565            assert!(manager.families[index].name.is_none());
566        }
567    }
568
569    #[test]
570    fn a_language_the_configuration_tags_brings_its_family_forward() {
571        let manager = manager();
572        let japanese = manager
573            .fallback
574            .iter()
575            .copied()
576            .find(|&index| speaks(&manager.families[index], "ja-JP"))
577            .expect("the family for the locale");
578        let order = manager.search_order(Some("sans-serif"), &["ja-JP"]);
579        let sans = manager.named("sans-serif").expect("the family").family;
580        assert_eq!(order[0], sans, "the requested family is tried first");
581        assert_eq!(order[1], japanese, "then the chain for the locale");
582    }
583
584    #[test]
585    fn a_script_tagged_family_is_reached_by_coverage_not_by_locale() {
586        let manager = manager();
587        let arabic = manager
588            .families
589            .iter()
590            .find(|family| family.languages.iter().any(|lang| lang == "und-Arab"))
591            .expect("the family");
592        assert!(!speaks(arabic, "ar-EG"));
593        assert!(manager.search_order(None, &["ar-EG"]).len() >= manager.families.len());
594    }
595
596    #[test]
597    fn every_family_is_searched_even_when_no_language_matches() {
598        let manager = manager();
599        let order = manager.search_order(None, &[]);
600        assert_eq!(order.len(), manager.families.len());
601    }
602
603    #[test]
604    fn face_count_is_every_entry_the_configuration_lists() {
605        assert_eq!(manager().face_count(), 7);
606    }
607
608    #[test]
609    fn families_lists_the_named_ones() {
610        assert_eq!(manager().families(), vec!["sans-serif", "serif"]);
611    }
612}