Skip to main content

text/
fontdb.rs

1// fontdb -- system font database: enumerate the installed fonts, group them into families,
2// match styles, cache the result                                      [src/text/fontdb.s]
3//
4//   scan(flags) -> faces found      walk the font folders (recursively, depth <= 8) and read
5//        only the table directory, name, OS/2 and head of every .ttf / .otf / .ttc / .otc
6//        (faces with glyf, CFF, CFF2 or CBDT outlines; every face of a collection);
7//        files already in a loaded cache with the same size are not opened again (flags
8//        FDB_RESCAN: ignore the cache).  Folders: jfgui_FONTDIRS (';' separated, on Linux
9//        also ':'), else Windows %WINDIR%/Fonts and %LOCALAPPDATA%/Microsoft/Windows/Fonts;
10//        Linux /usr/share/fonts, /usr/local/share/fonts, $XDG_DATA_HOME/fonts (else
11//        ~/.local/share/fonts), ~/.fonts (directories are visited once: symlink loops)
12//   scan_dirs(dirs, flags) -> faces found   the same in these folders only (tests, apps with
13//        their own font folders)
14//   resolve(conf, family, lang, weight, italic) -> (face, FS_SYN_*, family)   the first installed
15//        of the FontConf's candidates (generic families, substitutes, the language's choices;
16//        fontconf.rs), else sans-serif's; load_resolved(text, conf, ...) -> (font id, family)
17//        (a variable face: its instance for weight / italic, load_var)
18//   clear()                         forget the scan result (the cache stays)
19//   family_count(), family_name(i), family_faces(i) -> &[FdFace], family_range(i)   families
20//        sorted case-insensitively
21//   face_count(), face(i)           every face, family order
22//   find(family, weight, italic) -> (face index, FS_SYN_* flags to add)   family = legacy
23//        family name (FN_FAMILY, like GDI), else a typographic family; closest weight,
24//        italic before upright; synthetic bold / italic asked when the family lacks them
25//   load(text, family, weight, italic) -> font id / ERR_*   find + load_file (+ style; a
26//        variable face: load_var); each (face, style) is loaded once until the next scan
27//   variations(face) -> Option<&FdVar>   a variable face's axes and named instances (fvar,
28//        names from its name table; read from the file on first use, kept until the next scan)
29//   var_values(face, weight, italic, size_pt, axes) -> (values, FS_SYN_* still needed)   the
30//        axis values of a choice: the axes named, else weight -> wght, italic -> ital = 1 or
31//        slnt = its most slanted value, the size in points -> opsz (clamped to each axis);
32//        synthetic bold / italic only where no axis gives them
33//   load_var(text, face, syn, weight, italic, size_pt, axes) -> font id / ERR_*   load_face +
34//        Text::vary at var_values (a face that is not variable: load_face)
35//   save_cache(path) / load_cache(path) -> 0 / -1
36//
37// Families group faces by the legacy family name (name ID 1); names prefer German, then
38// English.  Cache file (little endian): "FDB1", u32 version 4, u32 files, u32 faces, u32
39// pool bytes, u64 FNV-1a hash of everything after the 28-byte header; files {u32 path, u32
40// size, u32 first face, u32 faces}; faces {u32 file, u32 face index, u16 weight, u16 flags,
41// u32 family, u32 style, u32 full, u32 typo family, u32 0}; pool of NUL-terminated UTF-8
42// strings (offsets above point into it).  Entries are validated by path and file size.
43// Version 3: CFF2 faces count (a version 2 cache lists CFF2-only files without faces, so it
44// is not read).  Version 4: FDFF_NOLATIN.
45
46use alloc::string::String;
47use alloc::vec::Vec;
48
49use crate::font::{FN_FAMILY, FN_FULL, FN_STYLE, FN_TYPO_FAMILY, FS_SYN_BOLD, FS_SYN_ITALIC};
50use crate::fontconf::{is_generic, FontConf};
51use crate::ttf::{sfnt_name, Tb, ERR_FAIL, NAME_CAP};
52use crate::var::{Var, VarAxis, VarInstance};
53use crate::Text;
54
55pub const FDB_RESCAN: u32 = 1;
56pub const FDFF_ITALIC: u16 = 1;
57pub const FDFF_BOLD: u16 = 2;
58pub const FDFF_OBLIQUE: u16 = 4;
59pub const FDFF_CFF: u16 = 8;
60pub const FDFF_COLOR: u16 = 16;
61pub const FDFF_VAR: u16 = 32;
62pub const FDFF_SYMBOL: u16 = 64;
63/// CFF2 outlines (FDFF_CFF is set as well)
64pub const FDFF_CFF2: u16 = 128;
65/// the cmap lacks A or a: a font for another script (see FontDb::resolve)
66pub const FDFF_NOLATIN: u16 = 256;
67
68const DEPTH_MAX: u32 = 8;
69const PATH_CAP: usize = 4096;
70const HDR_READ: usize = 4096;
71const TTC_MAX: u32 = 64;
72const NT_MAX: u32 = 1024;
73const NAME_MAX: u32 = 1 << 20;
74const CMAP_MAX: u32 = 1 << 20;
75const FILES_MAX: usize = 20000;
76const LOADED_MAX: usize = 256;
77const CACHE_MAGIC: u32 = 0x3142_4446; // "FDB1"
78const CACHE_HDR: usize = 28;
79const CACHE_VERSION: u32 = 4;
80const VISIT_MAX: usize = 4096;
81const TAG_TTCF: u32 = 0x7474_6366;
82const TAG_TRUE: u32 = 0x7472_7565;
83const TAG_OTTO: u32 = 0x4F54_544F;
84const TAG_V1: u32 = 0x0001_0000;
85const T_NAME: u32 = 0x6E61_6D65;
86const T_OS2: u32 = 0x4F53_2F32;
87const T_HEAD: u32 = 0x6865_6164;
88const T_GLYF: u32 = 0x676C_7966;
89const T_CFF: u32 = 0x4346_4620;
90const T_CFF2: u32 = 0x4346_4632;
91const T_COLR: u32 = 0x434F_4C52;
92const T_CBDT: u32 = 0x4342_4454;
93const T_FVAR: u32 = 0x6676_6172;
94const T_CMAP: u32 = 0x636D_6170;
95const FVAR_MAX: u32 = 1 << 20;
96static NAME_WANT: [u32; 4] = [FN_FAMILY, FN_STYLE, FN_FULL, FN_TYPO_FAMILY];
97
98/// a face of the database (FDFACE)
99#[derive(Clone, Default)]
100pub struct FdFace {
101    pub path: String,
102    pub style: String,   // FN_STYLE
103    pub full: String,    // FN_FULL
104    pub family: String,  // FN_FAMILY, the grouping key
105    pub tfamily: String, // FN_TYPO_FAMILY
106    pub face: u32,       // face index inside the file (TTC / OTC)
107    pub weight: u16,     // usWeightClass
108    pub flags: u16,      // FDFF_*
109}
110
111/// the variations of an installed variable face (FontDb::variations)
112#[derive(Clone, Debug, Default, PartialEq)]
113pub struct FdVar {
114    /// fvar's axes (names from the face's name table)
115    pub axes: Vec<VarAxis>,
116    /// its named instances
117    pub instances: Vec<VarInstance>,
118}
119
120// file record {path, size, first face, faces}
121#[derive(Clone, Copy)]
122struct FileRec {
123    path: u32,
124    size: u32,
125    first: u32,
126    n: u32,
127}
128
129// internal face record (strings: pool offsets)
130#[derive(Clone, Copy, Default)]
131struct FaceRec {
132    file: u32,
133    face: u32,
134    weight: u16,
135    flags: u16,
136    fam: u32,
137    style: u32,
138    full: u32,
139    tfam: u32,
140}
141
142struct Family {
143    first: u32,
144    n: u32,
145}
146
147#[derive(Default)]
148struct Cache {
149    files: Vec<FileRec>,
150    faces: Vec<FaceRec>,
151    pool: Vec<u8>,
152    hash: Vec<u64>,
153    by_hash: Vec<(u64, u32)>, // (path hash, file index) sorted
154}
155
156#[derive(Default)]
157pub struct FontDb {
158    pool: Vec<u8>,
159    files: Vec<FileRec>,
160    fi: Vec<FaceRec>,
161    faces: Vec<FdFace>,
162    fam: Vec<Family>,
163    loaded: Vec<(usize, u32, i32)>, // (face, synthetic flags, font id)
164    vars: Vec<(usize, Option<FdVar>)>, // (face, its variations): read on first use
165    cache: Option<Cache>,
166    use_cache: bool,
167    visited: Vec<(u64, u64)>,
168}
169
170// fvar (and the name table for its names) of face `face` in a file
171fn read_var(path: &str, face: u32) -> Option<FdVar> {
172    let f = plat::File::open(path, plat::FO_READ)?;
173    let mut h = [0u8; 16];
174    if read_at(&f, 0, &mut h) < 12 {
175        return None;
176    }
177    let dir = if be32(&h, 0) == TAG_TTCF {
178        let mut o = [0u8; 4];
179        if read_at(&f, 12 + face as u64 * 4, &mut o) != 4 {
180            return None;
181        }
182        be32(&o, 0) as u64
183    } else {
184        0
185    };
186    let mut d = [0u8; 12];
187    if read_at(&f, dir, &mut d) != 12 {
188        return None;
189    }
190    let nt = be16(&d, 4).min(NT_MAX);
191    let mut recs = alloc::vec![0u8; (nt * 16) as usize];
192    if read_at(&f, dir + 12, &mut recs) != (nt * 16) as i64 {
193        return None;
194    }
195    let (mut fvar, mut name) = ((0u32, 0u32), (0u32, 0u32));
196    for i in 0..nt as usize {
197        let r = &recs[i * 16..];
198        match be32(r, 0) {
199            T_FVAR => fvar = (be32(r, 8), be32(r, 12)),
200            T_NAME => name = (be32(r, 8), be32(r, 12)),
201            _ => {}
202        }
203    }
204    let table = |(o, l): (u32, u32), max: u32| -> Vec<u8> {
205        let mut b = alloc::vec![0u8; l.min(max) as usize];
206        let n = read_at(&f, o as u64, &mut b);
207        b.truncate(n.max(0) as usize);
208        b
209    };
210    let fv = table(fvar, FVAR_MAX);
211    let nm = table(name, NAME_MAX);
212    let v = Var::parse(Tb(&fv), Tb(&[]), Tb(&[]), Tb(&[]), Tb(&[]), Tb(&[]), Tb(&nm))?;
213    Some(FdVar { axes: v.axes, instances: v.instances })
214}
215
216/// FNV-1a 64 (hash_fnv64, seed 0 = the offset basis)
217pub fn fnv64(d: &[u8]) -> u64 {
218    let mut h: u64 = 0xCBF2_9CE4_8422_2325;
219    for &b in d {
220        h ^= b as u64;
221        h = h.wrapping_mul(0x0000_0100_0000_01B3);
222    }
223    h
224}
225
226// ASCII case-insensitive compare, bytes otherwise  [ci_cmp]
227fn ci_cmp(a: &[u8], b: &[u8]) -> i32 {
228    let mut i = 0;
229    loop {
230        let mut x = a.get(i).copied().unwrap_or(0) as i32;
231        let mut y = b.get(i).copied().unwrap_or(0) as i32;
232        if (x - b'A' as i32) as u32 <= 25 {
233            x += 32;
234        }
235        if (y - b'A' as i32) as u32 <= 25 {
236            y += 32;
237        }
238        let d = x - y;
239        if d != 0 || y == 0 {
240            return d;
241        }
242        i += 1;
243    }
244}
245
246// a NUL-terminated pool string
247fn pstr(pool: &[u8], off: u32) -> &[u8] {
248    let s = &pool[off as usize..];
249    &s[..s.iter().position(|&c| c == 0).unwrap_or(s.len())]
250}
251
252fn pool_put(pool: &mut Vec<u8>, s: &[u8]) -> u32 {
253    let off = pool.len() as u32;
254    pool.extend_from_slice(s);
255    pool.push(0);
256    off
257}
258
259fn ends_ci(s: &str, ext: &str) -> bool {
260    s.len() >= ext.len() && s.as_bytes()[s.len() - ext.len()..].eq_ignore_ascii_case(ext.as_bytes())
261}
262
263fn be16(d: &[u8], o: usize) -> u32 {
264    u16::from_be_bytes([d[o], d[o + 1]]) as u32
265}
266
267fn be32(d: &[u8], o: usize) -> u32 {
268    u32::from_be_bytes([d[o], d[o + 1], d[o + 2], d[o + 3]])
269}
270
271// does a cmap table map both A and a?  Its first Unicode subtable of format 4 or 12, else its
272// Mac Roman one of format 0 or 6; none of them (a symbol font) or one past CMAP_MAX: nothing
273// known, so yes
274fn cmap_latin(f: &plat::File, off: u32, len: u32) -> bool {
275    let mut b = alloc::vec![0u8; len.min(CMAP_MAX) as usize];
276    let got = read_at(f, off as u64, &mut b);
277    if got < 4 {
278        return true;
279    }
280    b.truncate(got as usize);
281    for mac in [false, true] {
282        for i in 0..be16(&b, 2) as usize {
283            let r = 4 + i * 8;
284            if r + 8 > b.len() {
285                break;
286            }
287            let (pid, eid, s) = (be16(&b, r), be16(&b, r + 2), be32(&b, r + 4) as usize);
288            let kind = if mac { pid == 1 && eid == 0 } else { pid == 0 || (pid == 3 && (eid == 1 || eid == 10)) };
289            if !kind || s.saturating_add(16) > b.len() {
290                continue;
291            }
292            let mapped = |cp: u32| match be16(&b, s) {
293                0 if mac => Some(b.get(s + 6 + cp as usize).is_some_and(|&g| g != 0)),
294                6 if mac => {
295                    let (first, n) = (be16(&b, s + 6), be16(&b, s + 8));
296                    let g = s + 10 + cp.wrapping_sub(first) as usize * 2;
297                    Some(cp >= first && cp - first < n && g + 2 <= b.len() && be16(&b, g) != 0)
298                }
299                4 if !mac => {
300                    let seg = be16(&b, s + 6) as usize / 2;
301                    (0..seg).map(|k| (s + 14 + k * 2, s + 16 + seg * 2 + k * 2)).take_while(|&(_, st)| st + 2 <= b.len())
302                        .find(|&(e, _)| be16(&b, e) >= cp).map(|(_, st)| be16(&b, st) <= cp)
303                }
304                12 if !mac => {
305                    let n = be32(&b, s + 12) as usize;
306                    Some((0..n).map(|k| s + 16 + k * 12).take_while(|&g| g + 8 <= b.len())
307                        .any(|g| be32(&b, g) <= cp && cp <= be32(&b, g + 4)))
308                }
309                _ => None,
310            };
311            if let (Some(upper), Some(lower)) = (mapped(0x41), mapped(0x61)) {
312                return upper && lower;
313            }
314        }
315    }
316    true
317}
318
319fn le32(d: &[u8], o: usize) -> u32 {
320    u32::from_le_bytes([d[o], d[o + 1], d[o + 2], d[o + 3]])
321}
322
323fn read_at(f: &plat::File, off: u64, buf: &mut [u8]) -> i64 {
324    match f.read_at(off, buf) {
325        Some(n) => n as i64,
326        None => -1,
327    }
328}
329
330impl FontDb {
331    pub fn new() -> FontDb {
332        FontDb::default()
333    }
334
335    /// fontdb_clear(): forget the scan result (the cache stays)
336    pub fn clear(&mut self) {
337        self.pool = Vec::new();
338        self.files = Vec::new();
339        self.fi = Vec::new();
340        self.faces = Vec::new();
341        self.fam = Vec::new();
342        self.loaded.clear();
343        self.vars.clear();
344    }
345
346    pub fn family_count(&self) -> usize {
347        self.fam.len()
348    }
349
350    /// the family's name ("" when i is out of range)
351    pub fn family_name(&self, i: usize) -> &str {
352        match self.fam.get(i) {
353            Some(f) => &self.faces[f.first as usize].family,
354            None => "",
355        }
356    }
357
358    /// the indices (face(i), variations(i)) of family i's faces (empty when out of range)
359    pub fn family_range(&self, i: usize) -> core::ops::Range<usize> {
360        match self.fam.get(i) {
361            Some(f) => f.first as usize..(f.first + f.n) as usize,
362            None => 0..0,
363        }
364    }
365
366    /// the faces of family i (empty when out of range)
367    pub fn family_faces(&self, i: usize) -> &[FdFace] {
368        match self.fam.get(i) {
369            Some(f) => &self.faces[f.first as usize..(f.first + f.n) as usize],
370            None => &[],
371        }
372    }
373
374    pub fn face_count(&self) -> usize {
375        self.faces.len()
376    }
377
378    pub fn face(&self, i: usize) -> Option<&FdFace> {
379        self.faces.get(i)
380    }
381
382    // ------------------------------------------------------------------ scanning
383
384    /// fontdb_scan(flags) -> number of faces
385    pub fn scan(&mut self, flags: u32) -> usize {
386        let mut ws: Vec<(String, u32)> = Vec::new();
387        Self::roots(&mut ws);
388        self.scan_from(ws, flags)
389    }
390
391    /// scan these folders instead of the system's -> number of faces
392    pub fn scan_dirs(&mut self, dirs: &[&str], flags: u32) -> usize {
393        let ws = dirs.iter().filter(|d| !d.is_empty()).map(|d| (String::from(*d), 0)).collect();
394        self.scan_from(ws, flags)
395    }
396
397    fn scan_from(&mut self, mut ws: Vec<(String, u32)>, flags: u32) -> usize {
398        self.clear();
399        self.visited.clear();
400        self.use_cache = flags & FDB_RESCAN == 0 && self.cache.as_ref().is_some_and(|c| !c.files.is_empty());
401        while let Some((path, depth)) = ws.pop() {
402            if !self.visit_once(&path) {
403                continue;
404            }
405            // list the directory
406            let mut ents: Vec<(String, bool, u32)> = Vec::new();
407            if !plat::dir_list(&path, &mut |name, is_dir, size| ents.push((String::from(name), is_dir, size))) {
408                continue;
409            }
410            for (name, is_dir, size) in ents {
411                // path2 = path + '/' + name
412                let mut p2 = path.clone();
413                if p2.len() >= PATH_CAP - 2 {
414                    continue; // no room for '/' + name
415                }
416                if !p2.ends_with('/') && !p2.ends_with('\\') {
417                    p2.push('/');
418                }
419                p2.push_str(&name);
420                if p2.len() >= PATH_CAP - 2 {
421                    continue; // too long: skip
422                }
423                if is_dir {
424                    if depth < DEPTH_MAX {
425                        ws.push((p2, depth + 1));
426                    }
427                    continue;
428                }
429                if ![".ttf", ".otf", ".ttc", ".otc"].iter().any(|e| ends_ci(&p2, e)) {
430                    continue;
431                }
432                if self.files.len() >= FILES_MAX {
433                    continue;
434                }
435                self.add_file(&p2, size);
436            }
437        }
438        self.finalize();
439        self.faces.len()
440    }
441
442    // first visit of a directory (Linux: device + inode; Windows: always)  [fd_visit_once]
443    fn visit_once(&mut self, path: &str) -> bool {
444        let Some(id) = plat::dev_ino(path) else { return true };
445        if self.visited.contains(&id) {
446            return false;
447        }
448        if self.visited.len() < VISIT_MAX {
449            self.visited.push(id);
450        }
451        true
452    }
453
454    // push the font folders onto the work stack (plat::policy::font_dirs: the same folders
455    // the app's permissions always let it read), the first one on top  [fd_roots]
456    fn roots(ws: &mut Vec<(String, u32)>) {
457        for d in plat::policy::font_dirs().into_iter().rev() {
458            ws.push((d, 0));
459        }
460    }
461
462    // add a file record and its faces (from the cache or parsed)  [fd_add_file]
463    fn add_file(&mut self, path: &str, size: u32) {
464        let poff = pool_put(&mut self.pool, path.as_bytes());
465        let first = self.fi.len() as u32;
466        self.files.push(FileRec { path: poff, size, first, n: 0 });
467        let file = self.files.len() as u32 - 1;
468        if !(self.use_cache && self.from_cache(path, size, file)) {
469            self.parse(path, file);
470        }
471        // faces of this file = records added since first
472        let n = self.fi.len() as u32 - first;
473        self.files[file as usize].n = n;
474    }
475
476    // copy the cached faces of (path, size)  [fd_from_cache]
477    fn from_cache(&mut self, path: &str, size: u32, file: u32) -> bool {
478        let Some(c) = &self.cache else { return false };
479        let h = fnv64(path.as_bytes());
480        let start = c.by_hash.partition_point(|e| e.0 < h);
481        for &(eh, i) in &c.by_hash[start..] {
482            if eh != h {
483                break;
484            }
485            let (i, f) = (i as usize, &c.files[i as usize]);
486            if c.hash[i] != h || f.size != size || pstr(&c.pool, f.path) != path.as_bytes() {
487                continue;
488            }
489            for k in f.first..f.first + f.n {
490                let mut r = c.faces[k as usize];
491                r.file = file;
492                // strings into the new pool
493                r.fam = pool_put(&mut self.pool, pstr(&c.pool, r.fam));
494                r.style = pool_put(&mut self.pool, pstr(&c.pool, r.style));
495                r.full = pool_put(&mut self.pool, pstr(&c.pool, r.full));
496                r.tfam = pool_put(&mut self.pool, pstr(&c.pool, r.tfam));
497                self.fi.push(r);
498            }
499            return true;
500        }
501        false
502    }
503
504    // open the file and add every face  [fd_parse]
505    fn parse(&mut self, path: &str, file: u32) {
506        let Some(f) = plat::File::open(path, plat::FO_READ) else { return };
507        let mut hdr = alloc::vec![0u8; HDR_READ];
508        let n = read_at(&f, 0, &mut hdr);
509        if n < 12 {
510            return;
511        }
512        match be32(&hdr, 0) {
513            TAG_TTCF => {
514                let nf = be32(&hdr, 8).min(TTC_MAX);
515                // offsets must lie in what was read
516                if (nf * 4 + 12) as i64 > n {
517                    return;
518                }
519                for i in 0..nf {
520                    let dir = be32(&hdr, (12 + i * 4) as usize);
521                    self.face_rec(&f, dir, i, file);
522                }
523            }
524            TAG_V1 | TAG_TRUE | TAG_OTTO => self.face_rec(&f, 0, 0, file),
525            _ => {}
526        }
527    }
528
529    // parse one face's tables and add it  [fd_face]
530    fn face_rec(&mut self, f: &plat::File, dir: u32, face: u32, file: u32) {
531        let (mut name, mut os2, mut head, mut cmap) = ((0u32, 0u32), (0u32, 0u32), (0u32, 0u32), (0u32, 0u32));
532        let mut flags = 0u16;
533        let mut outl = false;
534        // table directory
535        let mut d = [0u8; 12];
536        if read_at(f, dir as u64, &mut d) != 12 {
537            return;
538        }
539        let nt = be16(&d, 4).min(NT_MAX);
540        let mut recs = alloc::vec![0u8; (nt * 16) as usize];
541        if read_at(f, dir as u64 + 12, &mut recs) != (nt * 16) as i64 {
542            return;
543        }
544        for i in 0..nt as usize {
545            let r = &recs[i * 16..];
546            let (tag, off, len) = (be32(r, 0), be32(r, 8), be32(r, 12));
547            match tag {
548                T_NAME => name = (off, len),
549                T_OS2 => os2 = (off, len),
550                T_HEAD => head = (off, len),
551                T_CMAP => cmap = (off, len),
552                T_GLYF => outl = true,
553                T_CFF => {
554                    flags |= FDFF_CFF;
555                    outl = true;
556                }
557                T_CFF2 => {
558                    flags |= FDFF_CFF | FDFF_CFF2;
559                    outl = true;
560                }
561                T_COLR => flags |= FDFF_COLOR,
562                T_CBDT => {
563                    outl = true;
564                    flags |= FDFF_COLOR;
565                }
566                T_FVAR => flags |= FDFF_VAR,
567                _ => {}
568            }
569        }
570        if !outl || name.1 < 6 {
571            return; // no usable outlines (e.g. sbix / SVG only)
572        }
573        // head
574        let mut mac_style = 0u32;
575        if head.1 >= 54 {
576            let mut h = [0u8; 54];
577            if read_at(f, head.0 as u64, &mut h) == 54 {
578                mac_style = be16(&h, 44);
579            }
580        }
581        // OS/2 (first 96 bytes)
582        let mut o = [0u8; 96];
583        let mut os2n = os2.1.min(96) as i64;
584        if os2n != 0 {
585            os2n = read_at(f, os2.0 as u64, &mut o[..os2n as usize]);
586            if os2n < 0 {
587                return;
588            }
589        }
590        // name table
591        let nl = name.1.min(NAME_MAX) as usize;
592        let mut nb = alloc::vec![0u8; nl];
593        let got = read_at(f, name.0 as u64, &mut nb);
594        if got <= 0 {
595            return;
596        }
597        let tb = Tb(&nb[..got as usize]);
598        let mut names: [Vec<u8>; 4] = Default::default();
599        for (i, &id) in NAME_WANT.iter().enumerate() {
600            names[i] = sfnt_name(tb, id, NAME_CAP);
601        }
602        if names[0].is_empty() {
603            return; // no family name
604        }
605        if names[3].is_empty() {
606            names[3] = names[0].clone();
607        }
608        // weight / style flags
609        let mut weight = 400u32;
610        if os2n >= 64 {
611            weight = be16(&o, 4);
612            if weight < 10 {
613                weight *= 100;
614            }
615            if weight == 0 {
616                weight = 400;
617            }
618            if weight > 1000 {
619                weight = 1000;
620            }
621            let s = be16(&o, 62);
622            if s & 1 != 0 {
623                flags |= FDFF_ITALIC;
624            }
625            if s & 0x20 != 0 {
626                flags |= FDFF_BOLD;
627            }
628            if s & 0x200 != 0 {
629                flags |= FDFF_OBLIQUE;
630            }
631            if os2n >= 82 && be32(&o, 78) & 0x8000_0000 != 0 {
632                flags |= FDFF_SYMBOL; // ulCodePageRange1 bit 31
633            }
634        } else {
635            if mac_style & 1 != 0 {
636                flags |= FDFF_BOLD;
637                weight = 700;
638            }
639            if mac_style & 2 != 0 {
640                flags |= FDFF_ITALIC;
641            }
642        }
643        if cmap.1 != 0 && !cmap_latin(f, cmap.0, cmap.1) {
644            flags |= FDFF_NOLATIN;
645        }
646        let mut r = FaceRec { file, face, weight: weight as u16, flags, ..Default::default() };
647        r.fam = pool_put(&mut self.pool, &names[0]);
648        r.style = pool_put(&mut self.pool, &names[1]);
649        r.full = pool_put(&mut self.pool, &names[2]);
650        r.tfam = pool_put(&mut self.pool, &names[3]);
651        self.fi.push(r);
652    }
653
654    // ------------------------------------------------------------------ families
655
656    // order of internal faces: family, weight, italic, style, file, face  [fd_cmp]
657    fn cmp(&self, i: usize, j: usize) -> i32 {
658        let (a, b) = (&self.fi[i], &self.fi[j]);
659        let p = &self.pool;
660        let c = ci_cmp(pstr(p, a.fam), pstr(p, b.fam));
661        if c != 0 {
662            return c;
663        }
664        let c = a.weight as i32 - b.weight as i32;
665        if c != 0 {
666            return c;
667        }
668        let m = FDFF_ITALIC | FDFF_OBLIQUE;
669        let c = (a.flags & m) as i32 - (b.flags & m) as i32;
670        if c != 0 {
671            return c;
672        }
673        let c = ci_cmp(pstr(p, a.style), pstr(p, b.style));
674        if c != 0 {
675            return c;
676        }
677        let c = a.file.wrapping_sub(b.file) as i32;
678        if c != 0 {
679            return c;
680        }
681        a.face.wrapping_sub(b.face) as i32
682    }
683
684    // duplicates: same family, full name, weight and style flags  [fd_same]
685    fn same(&self, i: usize, j: usize) -> bool {
686        let (a, b) = (&self.fi[i], &self.fi[j]);
687        let p = &self.pool;
688        a.weight == b.weight
689            && a.flags == b.flags
690            && ci_cmp(pstr(p, a.fam), pstr(p, b.fam)) == 0
691            && ci_cmp(pstr(p, a.full), pstr(p, b.full)) == 0
692    }
693
694    // sort (shell sort gaps 701 .. 1), drop duplicates, build the faces and families
695    // [fd_finalize]
696    fn finalize(&mut self) {
697        let n = self.fi.len();
698        if n == 0 {
699            return;
700        }
701        let mut idx: Vec<usize> = (0..n).collect();
702        for gap in [701usize, 301, 132, 57, 23, 10, 4, 1] {
703            for i in gap..n {
704                let t = idx[i];
705                let mut j = i;
706                while j >= gap && self.cmp(idx[j - gap], t) > 0 {
707                    idx[j] = idx[j - gap];
708                    j -= gap;
709                }
710                idx[j] = t;
711            }
712        }
713        let mut prev: Option<usize> = None;
714        let p = |o: u32| String::from_utf8_lossy(pstr(&self.pool, o)).into_owned();
715        let mut faces: Vec<FdFace> = Vec::new();
716        let mut fam: Vec<Family> = Vec::new();
717        for &k in &idx {
718            if let Some(pk) = prev
719                && self.same(pk, k)
720            {
721                continue;
722            }
723            prev = Some(k);
724            let r = self.fi[k];
725            let fdf = FdFace {
726                path: p(self.files[r.file as usize].path),
727                style: p(r.style),
728                full: p(r.full),
729                family: p(r.fam),
730                tfamily: p(r.tfam),
731                face: r.face,
732                weight: r.weight,
733                flags: r.flags,
734            };
735            // family: new when the name differs (case-insensitively) from the previous face
736            match fam.last_mut() {
737                Some(f) if ci_cmp(faces[f.first as usize].family.as_bytes(), fdf.family.as_bytes()) == 0 => f.n += 1,
738                _ => fam.push(Family { first: faces.len() as u32, n: 1 }),
739            }
740            faces.push(fdf);
741        }
742        self.faces = faces;
743        self.fam = fam;
744    }
745
746    // ------------------------------------------------------------------ matching / loading
747
748    /// fontdb_find(family, weight, italic) -> (face index, FS_SYN_* flags to add)
749    pub fn find(&self, family: &str, weight: u32, italic: bool) -> Option<(usize, u32)> {
750        let w = if weight == 0 { 400 } else { weight.min(1000) as i32 };
751        for pass in 0..2 {
752            let mut best = i32::MAX;
753            let mut bi = None;
754            for (i, f) in self.faces.iter().enumerate() {
755                let name = if pass == 0 { &f.family } else { &f.tfamily };
756                if ci_cmp(name.as_bytes(), family.as_bytes()) != 0 {
757                    continue;
758                }
759                // score: italic mismatch 10000, weight distance x 2, direction tie-break
760                let fi = f.flags & (FDFF_ITALIC | FDFF_OBLIQUE) != 0;
761                let mut s = if fi != italic { 10000 } else { 0 };
762                let fw = f.weight as i32;
763                s += (fw - w).abs() * 2;
764                // prefer heavier for w >= 500, lighter otherwise
765                if (w >= 500 && fw < w) || (w < 500 && fw > w) {
766                    s += 1;
767                }
768                if s < best {
769                    best = s;
770                    bi = Some(i);
771                }
772            }
773            if let Some(i) = bi {
774                let f = &self.faces[i];
775                let mut syn = 0;
776                if italic && f.flags & (FDFF_ITALIC | FDFF_OBLIQUE) == 0 {
777                    syn |= FS_SYN_ITALIC;
778                }
779                if w >= 600 && f.weight < 600 {
780                    syn |= FS_SYN_BOLD;
781                }
782                return Some((i, syn));
783            }
784        }
785        None
786    }
787
788    /// the first installed candidate of conf for family in lang (generic families,
789    /// substitutes, the language's choices), else sans-serif's -> (face index, FS_SYN_* flags,
790    /// the family name found).  A substitute without Latin letters (FDFF_NOLATIN) only counts
791    /// when nothing else fits, except for emoji and math: fontconfig ranks the language above
792    /// such a rule's family, and Fedora's rules put Noto Sans Arabic first in sans-serif and
793    /// monospace.  Another script's characters come from fallback.rs
794    pub fn resolve(&self, conf: &FontConf, family: &str, lang: &str, weight: u32, italic: bool) -> Option<(usize, u32, String)> {
795        let mut tried = conf.candidates(family, lang);
796        if !is_generic(family) {
797            tried.extend(conf.candidates("sans-serif", lang));
798        }
799        let any_script = ["emoji", "math"].iter().any(|g| family.trim().eq_ignore_ascii_case(g));
800        let mut spare = None;
801        for f in tried.iter().filter(|f| !is_generic(f)) {
802            if let Some((i, syn)) = self.find(f, weight, italic) {
803                if any_script || self.faces[i].flags & FDFF_NOLATIN == 0 || f.trim().eq_ignore_ascii_case(family.trim()) {
804                    return Some((i, syn, self.faces[i].family.clone()));
805                }
806                spare = spare.or(Some((i, syn)));
807            }
808        }
809        spare.map(|(i, syn)| (i, syn, self.faces[i].family.clone()))
810    }
811
812    /// resolve and load -> (font id / ERR_*, the family found)
813    pub fn load_resolved(&mut self, text: &mut Text, conf: &FontConf, family: &str, lang: &str, weight: u32, italic: bool) -> (i32, String) {
814        match self.resolve(conf, family, lang, weight, italic) {
815            Some((fi, syn, fam)) => (self.load_var(text, fi, syn, weight, italic, None, &[]), fam),
816            None => (ERR_FAIL, String::new()),
817        }
818    }
819
820    /// fontdb_load(text, family, weight, italic) -> font id / ERR_*
821    pub fn load(&mut self, text: &mut Text, family: &str, weight: u32, italic: bool) -> i32 {
822        let Some((fi, syn)) = self.find(family, weight, italic) else { return ERR_FAIL };
823        self.load_var(text, fi, syn, weight, italic, None, &[])
824    }
825
826    /// load face fi with the synthetic styles syn (FS_SYN_*) -> font id / ERR_*; each (face,
827    /// style) once until the next scan
828    pub fn load_face(&mut self, text: &mut Text, fi: usize, syn: u32) -> i32 {
829        if fi >= self.faces.len() {
830            return ERR_FAIL;
831        }
832        // loaded before?
833        for &(f, s, id) in &self.loaded {
834            if f == fi && s == syn && text.valid(id as u32) {
835                return id;
836            }
837        }
838        let face = &self.faces[fi];
839        let mut id = text.load_file(&face.path, face.face);
840        if id < 0 {
841            return id;
842        }
843        if syn != 0 {
844            id = text.style(id as u32, syn);
845            if id < 0 {
846                return id;
847            }
848        }
849        if self.loaded.len() < LOADED_MAX {
850            self.loaded.push((fi, syn, id));
851        }
852        id
853    }
854
855    // ------------------------------------------------------------------ variable faces
856
857    /// the axes and named instances of face i (None: not a variable face, or its file cannot
858    /// be read); read from the file on first use
859    pub fn variations(&mut self, i: usize) -> Option<&FdVar> {
860        let f = self.faces.get(i)?;
861        if f.flags & FDFF_VAR == 0 {
862            return None;
863        }
864        let k = match self.vars.iter().position(|v| v.0 == i) {
865            Some(k) => k,
866            None => {
867                let v = read_var(&f.path, f.face);
868                self.vars.push((i, v));
869                self.vars.len() - 1
870            }
871        };
872        self.vars[k].1.as_ref()
873    }
874
875    /// the axis values of a choice on variable face i (see the module header) -> (one value
876    /// per axis, the FS_SYN_* flags still needed); None: not a variable face
877    pub fn var_values(&mut self, i: usize, weight: u32, italic: bool, size_pt: Option<f32>, axes: &[([u8; 4], f32)]) -> Option<(Vec<f32>, u32)> {
878        let face_weight = self.faces.get(i)?.weight as u32;
879        let face_italic = self.faces.get(i)?.flags & (FDFF_ITALIC | FDFF_OBLIQUE) != 0;
880        let v = self.variations(i)?;
881        let w = if weight == 0 { 400 } else { weight.min(1000) };
882        let mut vals: Vec<f32> = v.axes.iter().map(|a| a.def).collect();
883        let mut syn = 0;
884        let find = |t: &[u8; 4]| v.axes.iter().position(|a| &a.tag == t);
885        // weight: the wght axis, else a synthetic bold like find()
886        match find(b"wght") {
887            Some(k) => {
888                let a = &v.axes[k];
889                vals[k] = (w as f32).clamp(a.min, a.max);
890                if w >= 600 && a.max < 600.0 {
891                    syn |= FS_SYN_BOLD;
892                }
893            }
894            None if w >= 600 && face_weight < 600 => syn |= FS_SYN_BOLD,
895            None => {}
896        }
897        // italic: ital = 1, else the most slanted slnt, else a synthetic italic
898        if italic && !face_italic {
899            match (find(b"ital"), find(b"slnt")) {
900                (Some(k), _) if v.axes[k].max >= 1.0 => vals[k] = 1.0,
901                (_, Some(k)) if v.axes[k].min < 0.0 => vals[k] = v.axes[k].min,
902                _ => syn |= FS_SYN_ITALIC,
903            }
904        }
905        // the optical size follows the size
906        if let (Some(k), Some(pt)) = (find(b"opsz"), size_pt) {
907            vals[k] = pt.clamp(v.axes[k].min, v.axes[k].max);
908        }
909        // the axes named win
910        for &(t, x) in axes {
911            if let Some(k) = find(&t) {
912                vals[k] = if x.is_nan() { v.axes[k].def } else { x.clamp(v.axes[k].min, v.axes[k].max) };
913                if &t == b"wght" {
914                    syn &= !FS_SYN_BOLD;
915                }
916                if &t == b"ital" || &t == b"slnt" {
917                    syn &= !FS_SYN_ITALIC;
918                }
919            }
920        }
921        Some((vals, syn))
922    }
923
924    /// load variable face fi at the axis values of a choice (var_values) with the synthetic
925    /// styles syn asks for that no axis gives -> font id / ERR_*; a face that is not
926    /// variable: load_face(fi, syn)
927    #[allow(clippy::too_many_arguments)]
928    pub fn load_var(&mut self, text: &mut Text, fi: usize, syn: u32, weight: u32, italic: bool, size_pt: Option<f32>, axes: &[([u8; 4], f32)]) -> i32 {
929        let Some((vals, vsyn)) = self.var_values(fi, weight, italic, size_pt, axes) else { return self.load_face(text, fi, syn) };
930        // the synthetic styles the variable face still needs (find asked for them without
931        // knowing the axes)
932        let syn = (syn & !(FS_SYN_BOLD | FS_SYN_ITALIC)) | vsyn;
933        let id = self.load_face(text, fi, 0);
934        if id < 0 {
935            return id;
936        }
937        let tags: Vec<([u8; 4], f32)> = match self.variations(fi) {
938            Some(v) => v.axes.iter().zip(&vals).map(|(a, &x)| (a.tag, x)).collect(),
939            None => Vec::new(),
940        };
941        let id = text.vary(id as u32, &tags);
942        if id < 0 || syn == 0 { id } else { text.style(id as u32, syn) }
943    }
944
945    // ------------------------------------------------------------------ cache
946
947    /// fontdb_save_cache(path) -> 0 / -1
948    pub fn save_cache(&self, path: &str) -> i32 {
949        let mut b = Vec::with_capacity(CACHE_HDR + self.files.len() * 16 + self.fi.len() * 32 + self.pool.len());
950        b.extend_from_slice(&CACHE_MAGIC.to_le_bytes());
951        b.extend_from_slice(&CACHE_VERSION.to_le_bytes());
952        b.extend_from_slice(&(self.files.len() as u32).to_le_bytes());
953        b.extend_from_slice(&(self.fi.len() as u32).to_le_bytes());
954        b.extend_from_slice(&(self.pool.len() as u32).to_le_bytes());
955        b.extend_from_slice(&[0u8; 8]);
956        for f in &self.files {
957            for v in [f.path, f.size, f.first, f.n] {
958                b.extend_from_slice(&v.to_le_bytes());
959            }
960        }
961        for r in &self.fi {
962            b.extend_from_slice(&r.file.to_le_bytes());
963            b.extend_from_slice(&r.face.to_le_bytes());
964            b.extend_from_slice(&r.weight.to_le_bytes());
965            b.extend_from_slice(&r.flags.to_le_bytes());
966            for v in [r.fam, r.style, r.full, r.tfam, 0] {
967                b.extend_from_slice(&v.to_le_bytes());
968            }
969        }
970        b.extend_from_slice(&self.pool);
971        let h = fnv64(&b[CACHE_HDR..]);
972        b[20..28].copy_from_slice(&h.to_le_bytes());
973        if plat::write_file(path, &b) { 0 } else { -1 }
974    }
975
976    /// fontdb_load_cache(path) -> 0 / -1 (a malformed file is rejected as a whole)
977    pub fn load_cache(&mut self, path: &str) -> i32 {
978        self.cache = None;
979        let Some(d) = plat::read_file(path) else { return -1 };
980        if d.len() < CACHE_HDR || le32(&d, 0) != CACHE_MAGIC || le32(&d, 4) != CACHE_VERSION {
981            return -1;
982        }
983        if fnv64(&d[CACHE_HDR..]) != u64::from_le_bytes(d[20..28].try_into().unwrap()) {
984            return -1;
985        }
986        let nf = le32(&d, 8) as usize;
987        let nfa = le32(&d, 12) as usize;
988        let np = le32(&d, 16) as usize;
989        if nf > FILES_MAX || nfa > FILES_MAX * 8 || np == 0 {
990            return -1;
991        }
992        if nf * 16 + nfa * 32 + np + CACHE_HDR > d.len() {
993            return -1;
994        }
995        let fo = CACHE_HDR;
996        let ao = fo + nf * 16;
997        let po = ao + nfa * 32;
998        let pool = &d[po..po + np];
999        if pool[np - 1] != 0 {
1000            return -1;
1001        }
1002        // validate every offset
1003        let mut c = Cache::default();
1004        for i in 0..nf {
1005            let o = fo + i * 16;
1006            let f = FileRec { path: le32(&d, o), size: le32(&d, o + 4), first: le32(&d, o + 8), n: le32(&d, o + 12) };
1007            let (end, carry) = f.first.overflowing_add(f.n);
1008            if f.path as usize >= np || carry || end as usize > nfa {
1009                return -1;
1010            }
1011            c.files.push(f);
1012        }
1013        for i in 0..nfa {
1014            let o = ao + i * 32;
1015            let r = FaceRec {
1016                file: le32(&d, o),
1017                face: le32(&d, o + 4),
1018                weight: u16::from_le_bytes([d[o + 8], d[o + 9]]),
1019                flags: u16::from_le_bytes([d[o + 10], d[o + 11]]),
1020                fam: le32(&d, o + 12),
1021                style: le32(&d, o + 16),
1022                full: le32(&d, o + 20),
1023                tfam: le32(&d, o + 24),
1024            };
1025            if r.file as usize >= nf
1026                || r.fam as usize >= np
1027                || r.style as usize >= np
1028                || r.full as usize >= np
1029                || r.tfam as usize >= np
1030            {
1031                return -1;
1032            }
1033            c.faces.push(r);
1034        }
1035        c.pool = pool.to_vec();
1036        // path hashes
1037        c.hash = c.files.iter().map(|f| fnv64(pstr(&c.pool, f.path))).collect();
1038        c.by_hash = c.hash.iter().enumerate().map(|(i, &h)| (h, i as u32)).collect();
1039        c.by_hash.sort_unstable();
1040        self.cache = Some(c);
1041        0
1042    }
1043}