1use std::collections::HashMap;
16use std::path::PathBuf;
17
18use crate::files::{self, Files};
19use crate::{FontManager, Slant, Style, Typeface, Watch, NORMAL_WIDTH};
20
21const CONFIG: &str = "/system/etc/fonts.xml";
23const FONT_DIR: &str = "/system/fonts";
25const DEFAULT_FAMILY: &str = "sans-serif";
27
28pub struct Android {
36 families: Vec<Family>,
37 by_name: HashMap<String, Named>,
39 fallback: Vec<usize>,
42 files: Files,
43 dir: PathBuf,
44}
45
46struct Family {
48 name: Option<String>,
50 languages: Vec<String>,
53 faces: Vec<Face>,
54}
55
56struct Face {
58 file: String,
59 index: u32,
61 style: Style,
62}
63
64#[derive(Clone, Copy)]
66struct Named {
67 family: usize,
68 weight: Option<u16>,
71}
72
73impl Android {
74 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 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 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 fn named(&self, name: &str) -> Option<Named> {
128 self.by_name.get(&name.to_lowercase()).copied()
129 }
130
131 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 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 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 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 fn watch(&mut self, _on_change: Box<dyn Fn() + Send + Sync>) -> Option<Watch> {
255 None
256 }
257}
258
259fn 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#[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
284fn 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
345fn 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
370struct Tag<'a> {
372 name: &'a str,
373 attributes: &'a str,
375 closing: bool,
377 text: &'a str,
379}
380
381struct 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
421fn 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 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}