From de14f93c05cb77c1399dc6f5cd8af8fc7d09c627 Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 26 Oct 2026 21:00:04 +0000 Subject: [PATCH] refactor(text): move fontdb.rs items to fontdb/{build,cache,db,dirs,face,glyph_pen,lookup,names,scan,traits,wanted}.rs --- crates/text/src/fontdb.rs | 2643 +-------------------------- crates/text/src/fontdb/build.rs | 140 ++ crates/text/src/fontdb/cache.rs | 101 + crates/text/src/fontdb/db.rs | 395 ++++ crates/text/src/fontdb/dirs.rs | 220 +++ crates/text/src/fontdb/face.rs | 376 ++++ crates/text/src/fontdb/glyph_pen.rs | 29 + crates/text/src/fontdb/lookup.rs | 545 ++++++ crates/text/src/fontdb/names.rs | 121 ++ crates/text/src/fontdb/scan.rs | 235 +++ crates/text/src/fontdb/traits.rs | 363 ++++ crates/text/src/fontdb/wanted.rs | 141 ++ 12 files changed, 2712 insertions(+), 2597 deletions(-) create mode 100644 crates/text/src/fontdb/build.rs create mode 100644 crates/text/src/fontdb/cache.rs create mode 100644 crates/text/src/fontdb/db.rs create mode 100644 crates/text/src/fontdb/dirs.rs create mode 100644 crates/text/src/fontdb/face.rs create mode 100644 crates/text/src/fontdb/glyph_pen.rs create mode 100644 crates/text/src/fontdb/lookup.rs create mode 100644 crates/text/src/fontdb/names.rs create mode 100644 crates/text/src/fontdb/scan.rs create mode 100644 crates/text/src/fontdb/traits.rs create mode 100644 crates/text/src/fontdb/wanted.rs diff --git a/crates/text/src/fontdb.rs b/crates/text/src/fontdb.rs index 33b645d53..6eca651bb 100644 --- a/crates/text/src/fontdb.rs +++ b/crates/text/src/fontdb.rs @@ -17,472 +17,53 @@ use crate::GlyphImage; use crate::colr::ColourContent; use vectorcraft_doc::color::Color; -/// The family used when a requested family is unknown (Illustrator's Myriad Pro analogue). -pub const FALLBACK_FAMILY: &str = "Source Sans 3"; +mod build; +mod cache; +mod db; +mod dirs; +mod face; +mod glyph_pen; +mod lookup; +mod names; +mod scan; +mod traits; +mod wanted; -/// The fonts every database starts with (feature `bundled-fonts`, on by default). -#[cfg(feature = "bundled-fonts")] -static BUNDLED: &[&[u8]] = &[ - include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf"), - include_bytes!("../../../assets/fonts/SourceSans3-Semibold.ttf"), - include_bytes!("../../../assets/fonts/SourceSans3-Bold.ttf"), - include_bytes!("../../../assets/fonts/SourceSans3-It.ttf"), - include_bytes!("../../../assets/fonts/SourceSerif4-Regular.ttf"), - include_bytes!("../../../assets/fonts/Inter-Regular.ttf"), - include_bytes!("../../../assets/fonts/Inter-Medium.ttf"), - include_bytes!("../../../assets/fonts/Inter-SemiBold.ttf"), - include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf"), -]; -/// Without `bundled-fonts` a database starts with none: the embedder adds its fonts -/// ([`FontDb::add_font`]); until then text has no glyphs. -#[cfg(not(feature = "bundled-fonts"))] -static BUNDLED: &[&[u8]] = &[]; - -/// An axis setting of a variable font: the axis tag (`wght`) and its value in user units (700). -pub type Variation = ([u8; 4], f32); - -/// Caps on what a (possibly damaged) variable font can make the database read: axes per font and -/// named instances listed per face. -const MAX_AXES: usize = 64; -const MAX_INSTANCES: usize = 512; - -/// One axis of a variable font's design space, as its `fvar` table lists it: the tag (`wght`), its -/// name in the font's name table (`Weight`; the tag when it has none) and its range and default, -/// in the axis's own units. -#[derive(Clone, Debug, PartialEq)] -pub struct VariationAxis { - pub tag: [u8; 4], - pub name: String, - pub min: f32, - pub default: f32, - pub max: f32, - /// The font asks for the axis not to be shown to users (the `fvar` hidden flag). - pub hidden: bool, -} - -/// Faces of variable fonts instanced at axis settings their named instances don't have -/// ([`FontDb::instance`]). Each setting of a face keeps its id for the life of the process, so a -/// laid-out glyph's face can be found again ([`FontDb::face_by_id`]) after the built face was -/// dropped to bound the memory the cache holds. -#[derive(Default)] -struct Instances { - /// The id of each face and setting. - ids: HashMap, - /// What each id is: the face it was instanced from and its axis settings. - recipes: HashMap)>, - /// The instanced faces built, by id; dropped all at once when [`MAX_LIVE_INSTANCES`] are held. - live: HashMap>, -} - -/// A face instanced at axis settings: the id of the face, and the settings by tag, as bits. -type InstanceKey = (u32, Vec<([u8; 4], u32)>); - -/// How many instanced faces [`Instances::live`] holds at most. -pub(crate) const MAX_LIVE_INSTANCES: usize = 256; - -#[derive(Clone)] -enum FontBytes { - Static(&'static [u8]), - Owned(Arc>), -} - -/// One loaded font face. -pub struct FontFace { - /// Every name the face answers to (normalized), in every language its name table has. - keys: FaceKeys, - /// Its kind, for font menus' filters. - pub traits: FontTraits, - id: u32, - /// Typographic family name (e.g. "Source Sans 3"). - pub family: String, - /// Non-English family name from the name table (`ヒラギノ角ゴシック`), when the font has one. - /// Used when Preferences › Type › Show Font Names in English is off. - pub native_family: Option, - /// Typographic style name (e.g. "Semibold", "Italic"). - pub style: String, - /// The font's version string (name ID 5, e.g. "Version 7.200"; empty when it has none). Two - /// installed versions of one family and style are told apart by it (see [`FontDb::face_version`]). - pub version: String, - /// usWeightClass-style weight (400 = regular). - pub weight: f32, - pub italic: bool, - bytes: FontBytes, - index: u32, - /// The file the face was read from (cataloged system fonts); `None` for the bundled fonts and - /// fonts added as bytes. - path: Option, - pub(crate) upem: f64, - /// Ascender in font units (positive = up). - pub(crate) ascent: f64, - /// Descender in font units (positive = down). - pub(crate) descent: f64, - /// Cap height and x height in font units (estimated from the ascent when the font has no OS/2 - /// values). - pub(crate) cap_height: f64, - pub(crate) x_height: f64, - /// Underline and strikeout: (offset from the baseline up to the line's top, thickness), in - /// font units (the `post` and OS/2 tables', else a common placement). - pub(crate) underline: (f64, f64), - pub(crate) strikeout: (f64, f64), - pub(crate) shaper: harfrust::ShaperData, - /// The axis settings of the variable font's named instance this face is (user units: `wght` - /// 700); empty for a static face or a variable font's default instance. - variations: Vec, - /// [`Self::variations`] in the font's normalized design space (outlines and metrics). - location: Location, - /// [`Self::variations`] for the shaper (advances, kerning, feature variations). - pub(crate) instance: Option, - /// [`Self::ideographic_centre`], read once: layout asks for it per glyph. - ideographic_centre: std::sync::OnceLock, - /// [`Self::icf_margins`], read once. - icf_margins: std::sync::OnceLock, - /// Has `sbix` or `CBDT` strikes: colour bitmap glyphs ([`FontDb::glyph_image`]). - colour_bitmaps: bool, - /// Has `COLR` colour glyphs ([`FontDb::glyph_colour`]). - colour_layers: bool, - /// Has an `SVG ` table: OpenType SVG glyphs ([`FontDb::glyph_colour`]). - svg_glyphs: bool, -} - -/// Where the ideographic character face (ICF) lies inside the ideographic em box: its distance -/// from each edge of the em box, in ems. `top` and `bottom` along a horizontal line, `right` and -/// `left` across a vertical one. -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub struct IcfMargins { - pub top: f64, - pub bottom: f64, - pub right: f64, - pub left: f64, -} - -impl std::fmt::Debug for FontFace { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "FontFace({} {})", self.family, self.style) - } -} - -impl FontFace { - pub(crate) fn data(&self) -> &[u8] { - match &self.bytes { - FontBytes::Static(b) => b, - FontBytes::Owned(v) => v.as_slice(), - } - } - pub(crate) fn skrifa(&self) -> Option> { - skrifa::FontRef::from_index(self.data(), self.index).ok() - } - pub(crate) fn hb(&self) -> Option> { - harfrust::FontRef::from_index(self.data(), self.index).ok() - } - /// The face's index in its font file (collections hold several). - pub(crate) fn index(&self) -> u32 { - self.index - } - /// Where in a variable font's design space the face's outlines and metrics are taken (the - /// default location for a static face). - pub(crate) fn location(&self) -> LocationRef<'_> { - LocationRef::from(&self.location) - } - /// The axis settings of the variable font's named instance this face is (`wght` 700 for - /// Bold), in user units; empty for a static face and a variable font's default instance. - /// The face's file holds the whole variable font: exporters that embed it pass these on. - pub fn variations(&self) -> &[Variation] { - &self.variations - } - /// The variation axes of the face's font (its `fvar` table, at most 64); none for a static - /// face. - pub fn axes(&self) -> Vec { - let Some(f) = self.skrifa() else { return vec![] }; - f.axes() - .iter() - .take(MAX_AXES) - .map(|a| { - let tag = a.tag().to_be_bytes(); - let fallback = || String::from_utf8_lossy(&tag).trim().to_string(); - let name = name(&f, &[a.name_id()]).unwrap_or_else(fallback); - VariationAxis { tag, name, min: a.min_value(), default: a.default_value(), max: a.max_value(), hidden: a.is_hidden() } - }) - .collect() - } - /// The face's setting of axis `tag`: its named instance's ([`Self::variations`]), else the - /// axis default. `None` when the font has no such axis. - pub fn axis_value(&self, tag: [u8; 4]) -> Option { - let axis = self.axes().into_iter().find(|a| a.tag == tag)?; - Some(self.variations.iter().find(|(t, _)| *t == tag).map_or(axis.default, |(_, v)| *v)) - } - /// Unique id of this face within the process. - pub fn id(&self) -> u32 { - self.id - } - /// Does the face map `c` to a glyph? - pub fn covers(&self, c: char) -> bool { - self.skrifa().is_some_and(|f| f.charmap().map(c).is_some()) - } - /// Units per em. - pub fn units_per_em(&self) -> f64 { - self.upem - } - /// (ascent, descent) in font units, both positive. - pub fn vertical_metrics(&self) -> (f64, f64) { - (self.ascent, self.descent) - } - /// The line gap the font asks for between lines, in font units (from the same table as - /// [`vertical_metrics`](Self::vertical_metrics); 0 when it has none). - pub fn line_gap(&self) -> f64 { - self.skrifa().map_or(0.0, |f| f64::from(f.metrics(Size::unscaled(), self.location()).leading)).max(0.0) - } - /// Every mapped character and its glyph id, sorted by code point (the Glyphs panel). - pub fn chars(&self) -> Vec<(char, u32)> { - let Some(f) = self.skrifa() else { return vec![] }; - let mut v: Vec<(char, u32)> = f.charmap().mappings().filter_map(|(cp, g)| char::from_u32(cp).map(|c| (c, g.to_u32()))).collect(); - v.sort_unstable_by_key(|x| x.0); - v.dedup_by_key(|x| x.0); - v - } - /// Advance width of glyph `gid` in font units. - pub fn advance(&self, gid: u32) -> f64 { - self.skrifa() - .and_then(|f| f.glyph_metrics(Size::unscaled(), self.location()).advance_width(GlyphId::new(gid))) - .map(|a| a as f64) - .unwrap_or(self.upem * 0.5) - } - /// Glyph `gid` set upright in vertical type, from the font's vertical metrics: (its advance down - /// the column, the height above the baseline of its vertical origin, the top of its cell), in - /// font units. The origin is the `VORG` table's, else the glyph's top plus its top side bearing. - /// `None` when the font has no vertical metrics (`vhea`/`vmtx`), or they make no sense. - pub fn vertical_glyph(&self, gid: u32) -> Option<(f64, f64)> { - use skrifa::raw::TableProvider; - let f = self.skrifa()?; - let vmtx = f.vmtx().ok()?; - let g = GlyphId::new(gid); - let advance = f64::from(vmtx.advance(g)?); - let origin = match f.vorg() { - Ok(vorg) => f64::from(vorg.vertical_origin_y(g)), - Err(_) => { - let top = f.glyph_metrics(Size::unscaled(), self.location()).bounds(g).map_or(self.ascent, |b| f64::from(b.y_max)); - top + f64::from(vmtx.side_bearing(g)?) - } - }; - let em = self.upem; - // A glyph's cell is somewhere between a tenth of an em and a few ems, its top within a few - // ems of the baseline: anything else is a damaged table. - (advance > em * 0.1 && advance < em * 4.0 && origin.abs() < em * 4.0).then_some((advance, origin)) - } - /// Height above the baseline of the centre of the face's ideographic em box, in ems: from its - /// vertical metrics (an ideograph's cell), else the usual 0.38 (the box running from 0.12 em - /// below the baseline to 0.88 em above it). - pub fn ideographic_centre(&self) -> f64 { - *self.ideographic_centre.get_or_init(|| { - let gid = ['国', 'あ', '一'].into_iter().map(|c| self.glyph_for(c)).find(|g| *g != 0); - gid.and_then(|g| self.vertical_glyph(g)).map_or(0.38, |(advance, origin)| (origin - advance * 0.5) / self.upem) - }) - } - /// The ideographic character face as margins inside the em box ([`IcfMargins`]): from the - /// font's BASE table as the OpenType baseline tags define it (`icfb`, `icft` against `ideo`, - /// `idtp`), else, as that definition allows, from the bounds of some ideographs and kana - /// averaged, else none (the ICF is the em box). - /// - pub fn icf_margins(&self) -> IcfMargins { - *self.icf_margins.get_or_init(|| self.icf_from_base().or_else(|| self.icf_from_glyphs()).unwrap_or_default()) - } - - /// [`Self::icf_margins`] from the BASE table: the ICF's bottom edge `icfb` (top edge `icft`, else - /// as far below the em box top) against the em box `ideo` .. `idtp` (else the OS/2 typographic - /// descender, and one em above it); across vertical lines, `icfb` and `icft` of the vertical - /// axis against 0 .. `idtp` (else the horizontal margin, and one em). - pub(crate) fn icf_from_base(&self) -> Option { - use skrifa::raw::TableProvider; - let f = self.skrifa()?; - let base = f.base().ok()?; - let em = self.upem; - let h = base_values(base.horiz_axis()?.ok()?)?; - let icfb = *h.get(b"icfb")?; - let em_bottom = match h.get(b"ideo") { - Some(v) => *v, - None => f64::from(f.os2().ok()?.s_typo_descender()), - }; - let em_top = h.get(b"idtp").copied().unwrap_or(em_bottom + em); - let margin = icfb - em_bottom; - let icf_top = h.get(b"icft").copied().unwrap_or(em_top - margin); - let v = base.vert_axis().and_then(Result::ok).and_then(base_values).unwrap_or_default(); - let icf_left = v.get(b"icfb").copied().unwrap_or(margin); - let em_right = v.get(b"idtp").copied().unwrap_or(em); - let icf_right = v.get(b"icft").copied().unwrap_or(em_right - icf_left); - let m = IcfMargins { top: (em_top - icf_top) / em, bottom: margin / em, right: (em_right - icf_right) / em, left: icf_left / em }; - // Inside the em box, short of its middle: anything else is a damaged table. - [m.top, m.bottom, m.right, m.left].iter().all(|x| (0.0..0.5).contains(x)).then_some(m) - } - - /// [`Self::icf_margins`] from the ink of some ideographs and kana: each edge's margin to the em - /// box (its centre [`Self::ideographic_centre`], one em high and wide), averaged. - pub(crate) fn icf_from_glyphs(&self) -> Option { - let f = self.skrifa()?; - let metrics = f.glyph_metrics(Size::unscaled(), self.location()); - let em = self.upem; - let (em_bottom, em_top) = ((self.ideographic_centre() - 0.5) * em, (self.ideographic_centre() + 0.5) * em); - let boxes: Vec<_> = ['国', '東', '永', '書', 'あ', 'ア'] - .into_iter() - .map(|c| self.glyph_for(c)) - .filter(|g| *g != 0) - .filter_map(|g| metrics.bounds(GlyphId::new(g))) - .collect(); - if boxes.is_empty() { - return None; - } - let n = boxes.len() as f64; - let mean = |v: &dyn Fn(&skrifa::metrics::BoundingBox) -> f32| boxes.iter().map(|b| f64::from(v(b))).sum::() / n; - let m = IcfMargins { - top: (em_top - mean(&|b| b.y_max)) / em, - bottom: (mean(&|b| b.y_min) - em_bottom) / em, - right: (em - mean(&|b| b.x_max)) / em, - left: mean(&|b| b.x_min) / em, - }; - [m.top, m.bottom, m.right, m.left].iter().all(|x| (0.0..0.5).contains(x)).then_some(m) - } - - /// Glyph id for `c` (0 = .notdef). - pub fn glyph_for(&self, c: char) -> u32 { - self.skrifa().and_then(|f| f.charmap().map(c)).map(|g| g.to_u32()).unwrap_or(0) - } - /// The file the face was read from (cataloged system fonts). - pub fn path(&self) -> Option<&std::path::Path> { - self.path.as_deref() - } - /// The bytes of the face's font file (a collection holds several faces). - pub fn file_data(&self) -> &[u8] { - self.data() - } - /// The embedding permissions of the OS/2 table (`fsType`; 0, installable, when it has none). - pub fn fs_type(&self) -> u16 { - use skrifa::raw::TableProvider; - self.skrifa().and_then(|f| f.os2().ok()).map_or(0, |t| t.fs_type()) - } - /// May the font file be copied along with a document? Its license allows embedding its - /// outlines, as exports check it ([`Self::embedding`]). - pub fn embeddable(&self) -> bool { - self.embedding() != crate::embed::Embedding::Forbidden - } - /// The face's index in its font file ([`Self::file_data`]; collections hold several). - pub fn face_index(&self) -> u32 { - self.index - } - /// The face's PostScript name, as PostScript files name fonts (`SourceSans3-Bold`): name ID 6; - /// for a named instance of a variable font (whose name ID 6 is the default instance's), its - /// prefix and style (`SourceSans3-Semibold`). Without one, its family and style. - pub fn postscript_name(&self) -> String { - let squeeze = |s: &str| -> String { s.chars().filter(|c| c.is_ascii_graphic() && !"[](){}<>/%".contains(*c)).collect() }; - let own = self.skrifa().and_then(|f| localized(&f, StringId::POSTSCRIPT_NAME).into_iter().map(|(_, s)| s).next()).map(|s| squeeze(&s)); - match own.filter(|s| !s.is_empty()) { - Some(name) if self.variations.is_empty() => name, - own => { - let prefix = own.as_deref().and_then(|n| n.split('-').next()).map_or_else(|| squeeze(&self.family), str::to_string); - format!("{prefix}-{}", squeeze(&self.style)) - } - } - } - /// Are the face's outlines TrueType (`glyf`), not PostScript (`CFF `)? - pub fn is_truetype(&self) -> bool { - use skrifa::raw::TableProvider; - self.skrifa().is_some_and(|f| f.glyf().is_ok()) - } -} - -/// One installed family found by the system font scan: its name and the file of each style. -#[derive(Debug, Default)] -struct CatalogFamily { - name: String, - /// Non-English family name from the name table, when the scan found one. - native: Option, - faces: Vec, - /// The family's kind (from its first face found); variable when any face found is. - traits: Option, -} - -/// One style of an installed family: a face, or a named instance of a variable font. -#[derive(Debug)] -#[cfg_attr(target_arch = "wasm32", allow(dead_code))] -struct CatalogFace { - style: String, - path: PathBuf, - /// Weight and italic as the style lists them (by its name, or a named instance's axes). - weight: f32, - italic: bool, -} - -/// The installed fonts by ASCII-lowercased family name (lookups ignore ASCII case, as -/// [`FontDb::face`] does). Always empty on wasm, which has no system fonts. -type Catalog = HashMap; - -/// The installed faces' (family, style) by normalized PostScript name ([`norm`]). -type PostScriptNames = HashMap; - -/// Maps the family and style [`FontDb::canonical`] was asked for to its answer, for names that -/// are neither a loaded nor an installed family. -type CanonicalNames = HashMap<(String, String), (String, String)>; - -/// [`FontDb::glyph_image`]'s answers by face and glyph. -type GlyphImages = GlyphCache<(u32, u32), Arc>; -/// [`FontDb::glyph_colour`]'s answers by face and glyph, and the text's fill and stroke colours for -/// glyphs that paint with them. -type GlyphColours = GlyphCache<(u32, u32, Option), ColourContent>; -/// The text's fill and stroke colours, as a cache key ([`Color::key`]). -type TextColours = ([u32; 5], Option<[u32; 5]>); - -/// What a glyph's colour content (an image, layers) takes of memory, in bytes. -trait Weight { - fn bytes(&self) -> usize; -} - -impl Weight for Arc { - fn bytes(&self) -> usize { - self.blob.bytes.len() - } -} - -impl Weight for ColourContent { - fn bytes(&self) -> usize { - match self { - ColourContent::Image(i) => i.bytes(), - // Roughly what a path node with its outline and fill takes. - ColourContent::Art(art) => { - let mut nodes = 0; - art.walk(&mut |_| nodes += 1); - nodes * 512 - } - } - } -} - -/// The colour content of glyphs ([`FontDb::glyph_image`], [`FontDb::glyph_colour`]) kept by -/// face, glyph and what else it depends on, and its bytes in all. -struct GlyphCache { - kept: HashMap>, - bytes: usize, -} - -impl Default for GlyphCache { - fn default() -> Self { - Self { kept: HashMap::new(), bytes: 0 } - } -} - -impl GlyphCache { - /// Keeps `value` (or that there is none), first letting go of all it keeps when that would - /// take more than [`IMAGE_CACHE_MAX`] glyphs or [`IMAGE_CACHE_BYTES`]. A glyph it keeps - /// already, which another thread looked up meanwhile, stays as it is. - fn keep(&mut self, key: K, value: Option) { - if self.kept.contains_key(&key) { - return; - } - let bytes = value.as_ref().map_or(0, Weight::bytes); - if self.kept.len() >= IMAGE_CACHE_MAX || self.bytes.saturating_add(bytes) > IMAGE_CACHE_BYTES { - self.kept.clear(); - self.bytes = 0; - } - self.bytes += bytes; - self.kept.insert(key, value); - } -} +pub use build::style_weight; +#[cfg(test)] +use cache::GlyphCache; +use cache::{CanonicalNames, Catalog, GlyphColours, GlyphImages, PostScriptNames}; +#[cfg(all(not(target_arch = "wasm32"), test))] +pub(crate) use dirs::fonts_outside; +#[cfg(test)] +pub(crate) use dirs::sfnt_of; +pub use dirs::{ + PlatformFontFiles, app_font_dir, set_app_font_dir, set_platform_font_files, set_user_font_dirs, set_user_font_dirs_confined, system_font_dirs, + user_font_dirs, +}; +use face::Instances; +#[cfg(test)] +pub(crate) use face::MAX_LIVE_INSTANCES; +pub use face::{FALLBACK_FAMILY, FontFace, IcfMargins}; +pub(crate) use glyph_pen::FlipPen; +#[cfg(not(target_arch = "wasm32"))] +use names::SysFallback; +#[cfg(test)] +pub use names::THAI_FAMILIES; +#[cfg(test)] +use names::{native_family_name, norm, pick_native_label}; +#[cfg(all(not(target_arch = "wasm32"), test))] +pub(crate) use scan::file_face_names_within; +#[cfg(test)] +use scan::{MAX_FACES, enumerate_faces}; +pub use scan::{is_font_file, is_suitcase}; +use traits::Alias; +#[cfg(test)] +use traits::{FaceKeys, face_names}; +pub use traits::{FontClass, FontMatch, FontTraits}; +#[cfg(test)] +pub(crate) use traits::{alias_lists, face_styles, on_faces_published}; +pub use wanted::{WantedFont, WantedFonts}; /// Process-wide font database. pub struct FontDb { @@ -556,75 +137,6 @@ fn modified(path: &Path) -> Option { std::fs::metadata(path).and_then(|m| m.modified()).ok() } -#[cfg(not(target_arch = "wasm32"))] -#[derive(Default)] -struct SysFallback { - enabled: bool, - misses: std::collections::HashSet, - /// Counts the scans of the font folders: a search that a scan overtook doesn't record its miss. - scans: u64, -} - -/// Fonts made for Thai, tried first (when installed) for Thai the font in use lacks: Windows -/// (Leelawadee UI; Tahoma before Windows 8), macOS (Thonburi, Sukhumvit Set) and Linux (Noto, -/// TLWG). -pub const THAI_FAMILIES: &[&str] = &[ - "Leelawadee UI", - "Leelawadee", - "Tahoma", - "Thonburi", - "Sukhumvit Set", - "Noto Sans Thai", - "Noto Sans Thai UI", - "Noto Sans Thai Looped", - "Waree", - "Loma", - "Garuda", -]; - -/// The families tried first for character `c`, in a script whose text only fonts made for it set -/// well (Thai, Lao, Khmer, Myanmar); none for other characters. -fn script_families(c: char) -> &'static [&'static str] { - use unicode_script::{Script, UnicodeScript}; - match c.script() { - Script::Thai => THAI_FAMILIES, - Script::Lao => &["Lao UI", "Leelawadee UI", "Lao Sangam MN", "Noto Sans Lao", "Noto Sans Lao UI", "Phetsarath OT"], - Script::Khmer => &["Khmer UI", "Khmer Sangam MN", "Noto Sans Khmer", "Noto Sans Khmer UI", "Khmer OS"], - Script::Myanmar => &["Myanmar Text", "Myanmar Sangam MN", "Noto Sans Myanmar", "Noto Sans Myanmar UI", "Padauk"], - _ => &[], - } -} - -/// Families tried (when installed) for characters the loaded fonts lack: CJK, symbols, emoji. -#[cfg(not(target_arch = "wasm32"))] -const SYSTEM_FALLBACKS: &[&str] = &[ - "Helvetica Neue", - "Arial", - "Segoe UI", - "Noto Sans", - "DejaVu Sans", - "PingFang SC", - "Hiragino Sans", - "Hiragino Kaku Gothic ProN", - "Apple SD Gothic Neo", - "Heiti SC", - "STHeiti", - "Microsoft YaHei", - "Yu Gothic", - "Malgun Gothic", - "Noto Sans CJK SC", - "Noto Sans CJK JP", - "Arial Unicode MS", - "Apple Symbols", - "Segoe UI Symbol", - "Noto Sans Symbols", - "Noto Sans Symbols2", - "Noto Emoji", - "Segoe UI Emoji", - "Apple Color Emoji", - "Noto Color Emoji", -]; - static NEXT_ID: AtomicU32 = AtomicU32::new(1); const OUTLINE_CACHE_MAX: usize = 50_000; /// The most glyph images [`FontDb::glyph_image`] keeps (each a PNG of some 10–60 KB). @@ -635,2072 +147,9 @@ const IMAGE_CACHE_BYTES: usize = 64 << 20; /// them before it keeps the next one. const CANONICAL_CACHE_MAX: usize = 4_096; -fn name(font: &skrifa::FontRef<'_>, ids: &[StringId]) -> Option { - ids.iter().find_map(|id| font.localized_strings(*id).english_or_first().map(|s| s.to_string()).filter(|s| !s.is_empty())) -} - -/// The most distinct legacy family (and style) names of a font used as keys. -const MAX_LEGACY_NAMES: usize = 64; - -/// Every localized string of `id`, with its language. -fn localized(font: &skrifa::FontRef<'_>, id: StringId) -> Vec<(Option, String)> { - font.localized_strings(id).map(|s| (s.language().map(str::to_owned), s.to_string())).filter(|(_, s)| !s.is_empty()).collect() -} - -/// Whether a name-table language tag is English (Illustrator's "Show Font Names in English"). -fn is_english_lang(lang: Option<&str>) -> bool { - lang.is_some_and(|l| { - let l = l.to_ascii_lowercase(); - l == "en" || l == "eng" || l.starts_with("en-") || l.starts_with("en_") - }) -} - -/// A non-English family label from the name table, for Preferences › Type › Show Font Names in -/// English (off). Prefers a record whose language isn't English; else any label that differs from -/// `english` and isn't plain ASCII. `None` when the font has no other name. -fn pick_native_label(english: &str, pairs: &[(Option, String)]) -> Option { - pairs - .iter() - .find(|(lang, s)| !s.eq_ignore_ascii_case(english) && !is_english_lang(lang.as_deref())) - .or_else(|| pairs.iter().find(|(_, s)| !s.eq_ignore_ascii_case(english) && !s.is_ascii())) - .map(|(_, s)| s.clone()) -} - -/// The family's native-language name from the font (typographic, else legacy), when it has one. -fn native_family_name(font: &skrifa::FontRef<'_>, english: &str) -> Option { - pick_native_label(english, &localized(font, StringId::TYPOGRAPHIC_FAMILY_NAME)) - .or_else(|| pick_native_label(english, &localized(font, StringId::FAMILY_NAME))) -} - -/// Every localized string of `id`. -fn all_names(font: &skrifa::FontRef<'_>, id: StringId) -> Vec { - localized(font, id).into_iter().map(|(_, s)| s).collect() -} - -/// The names a face answers to besides its (English) family and style, normalized ([`norm`]): -/// its family in every language (typographic, else legacy: `ヒラギノ角ゴシック`), its style likewise -/// (`ミディアム`), its legacy family names (`Hiragino Sans W3`) with their legacy styles, paired per -/// language record (the Mac and Windows records of a face differ), its full names (name ID 4), -/// and its PostScript names (`HiraginoSans-W3`, as PDF and `.ai` files name fonts). -#[derive(Clone, Debug, Default)] -struct FaceKeys { - families: Vec, - styles: Vec, - legacy: Vec<(String, String)>, - full_names: Vec, - postscript: Vec, -} - -impl FaceKeys { - fn of(font: &skrifa::FontRef<'_>, family: &str, style: &str) -> Self { - let (typo_family, typo_style) = (all_names(font, StringId::TYPOGRAPHIC_FAMILY_NAME), all_names(font, StringId::TYPOGRAPHIC_SUBFAMILY_NAME)); - let (mut legacy_family, mut legacy_style) = (localized(font, StringId::FAMILY_NAME), localized(font, StringId::SUBFAMILY_NAME)); - // Records repeat (platforms, encodings) and are paired below: once each, and no more than a - // font has any use for, or the pairs multiply. - for names in [&mut legacy_family, &mut legacy_style] { - names.sort(); - names.dedup(); - names.truncate(MAX_LEGACY_NAMES); - } - // Without typographic names, the legacy ones are the family's. - let family_names: Vec<&str> = if typo_family.is_empty() { - legacy_family.iter().map(|(_, f)| f.as_str()).collect() - } else { - typo_family.iter().map(String::as_str).collect() - }; - let mut families: Vec = std::iter::once(family).chain(family_names).map(norm).collect(); - // A legacy style only names the face beside its legacy family when the font has a - // typographic style: every weight of `Hiragino Sans` is `Regular` in its own legacy family. - let style_names: Vec<&str> = if typo_style.is_empty() { - legacy_style.iter().map(|(_, s)| s.as_str()).collect() - } else { - typo_style.iter().map(String::as_str).collect() - }; - let mut styles: Vec = std::iter::once(style).chain(style_names).map(norm).collect(); - let mut legacy: Vec<(String, String)> = if typo_family.is_empty() { - vec![] - } else { - legacy_family - .iter() - .flat_map(|(lf, f)| legacy_style.iter().filter(move |(ls, _)| ls == lf).map(move |(_, s)| (norm(f), norm(s)))) - .filter(|(f, s)| !f.is_empty() && !s.is_empty()) - .collect() - }; - let mut postscript: Vec = all_names(font, StringId::POSTSCRIPT_NAME).iter().map(|n| norm(n)).collect(); - let mut full_names: Vec = all_names(font, StringId::FULL_NAME).iter().map(|n| norm(n)).collect(); - for v in [&mut families, &mut styles, &mut full_names, &mut postscript] { - v.retain(|k| !k.is_empty()); - v.sort(); - v.dedup(); - } - legacy.sort(); - legacy.dedup(); - FaceKeys { families, styles, legacy, full_names, postscript } - } -} - -/// A font's kind, for filtering font menus: the classification of its OS/2 table (IBM family -/// class, PANOSE) and, for CJK fonts whose tables seldom say, its names (明朝, ゴシック, 丸…). -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] -pub enum FontClass { - /// Serif (Latin) or Mincho (CJK). - Serif, - /// Sans serif (Latin) or Gothic (CJK). - Sans, - /// Rounded sans / Maru Gothic. - Rounded, - /// Script, handwriting, brush (楷書・行書・教科書…). - Script, - Monospaced, - /// Decorative, display, symbol and pictorial fonts. - Decorative, - #[default] - Other, -} - -impl FontClass { - /// The kinds a font menu filters by (all but `Other`). - pub const ALL: [FontClass; 6] = - [FontClass::Serif, FontClass::Sans, FontClass::Rounded, FontClass::Script, FontClass::Monospaced, FontClass::Decorative]; -} - -/// What a font menu filters on: whether a family sets Japanese or Thai, its kind, and whether it -/// is a variable font. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct FontTraits { - pub japanese: bool, - /// Its character map has Thai (ก and า). - pub thai: bool, - pub class: FontClass, - /// The face has variation axes (an `fvar` table of at most 64 axes); for a family, one of - /// its faces has. - pub variable: bool, -} - -impl FontTraits { - /// From a face's OS/2 and `fvar` tables (when it has them) and names. - fn of(font: &skrifa::FontRef<'_>, keys: &FaceKeys) -> Self { - use skrifa::raw::TableProvider; - let os2 = font.os2().ok(); - let names: Vec<&str> = keys.families.iter().chain(&keys.postscript).map(String::as_str).collect(); - let named = |words: &[&str]| names.iter().any(|n| words.iter().any(|w| n.contains(w))); - let has_japanese_name = keys.families.iter().any(|k| k.chars().any(|c| matches!(c as u32, 0x3040..=0x30FF | 0x4E00..=0x9FFF))); - let (class_id, panose, ranges2, codepages) = match &os2 { - Some(o) => (o.s_family_class(), o.panose_10().to_vec(), o.ul_unicode_range_2(), o.ul_code_page_range_1().unwrap_or(0)), - None => (0, vec![], 0, 0), - }; - // Hiragana (bit 49) or Katakana (bit 50) in the Unicode ranges, or the JIS code page. - let japanese = ranges2 & (0b11 << 17) != 0 || codepages & (1 << 17) != 0 || has_japanese_name; - let thai = ['\u{0E01}', '\u{0E32}'].iter().all(|c| font.charmap().map(*c).is_some()); - let family_type = panose.first().copied().unwrap_or(0); - let serif_style = panose.get(1).copied().unwrap_or(0); - let proportion = panose.get(3).copied().unwrap_or(0); - let ibm = (class_id >> 8) as u8; - let class = if named(&["丸", "maru", "rounded", "round"]) || japanese && named(&["jun"]) { - FontClass::Rounded - } else if named(&[ - "楷書", - "行書", - "草書", - "隷書", - "教科書", - "毛筆", - "筆", - "kaisho", - "gyosho", - "sosho", - "reisho", - "kyokasho", - "brush", - "script", - "hand", - "kaiti", - "xingkai", - "libian", - ]) { - FontClass::Script - } else if named(&["mono", "code", "courier", "menlo", "consol"]) { - FontClass::Monospaced - } else if named(&[ - "明朝", - "mincho", - "ryumin", - "hiramin", - "yumin", - "kozmin", - "serif", - "songti", - "simsun", - "stsong", - "fangsong", - "myungjo", - "times", - "georgia", - "garamond", - "baskerville", - "bodoni", - "caslon", - "palatino", - "didot", - "century", - "cambria", - "minion", - "charter", - "hoefler", - ]) && !named(&["sansserif", "sans"]) - { - FontClass::Serif - } else if named(&[ - "ゴシック", - "gothic", - "goth", - "sans", - "kakugo", - "kaku", - "heiti", - "helvetica", - "arial", - "grotesk", - "grotesque", - "futura", - "gill", - "verdana", - "tahoma", - "frutiger", - "avenir", - "geneva", - "lucidagrande", - "inter", - "roboto", - "optima", - "pingfang", - ]) { - FontClass::Sans - } else if family_type == 2 && proportion == 9 { - FontClass::Monospaced - } else if family_type == 3 || ibm == 10 { - FontClass::Script - } else if family_type == 4 || family_type == 5 || ibm == 9 || ibm == 12 { - FontClass::Decorative - } else if matches!(ibm, 1..=5 | 7) || family_type == 2 && (2..=10).contains(&serif_style) { - FontClass::Serif - } else if ibm == 8 || family_type == 2 && (11..=13).contains(&serif_style) { - FontClass::Sans - } else { - FontClass::Other - }; - let variable = (1..=MAX_AXES).contains(&font.axes().len()); - FontTraits { japanese, thai, class, variable } - } -} - -/// What a name other than a family's own one stands for ([`FontDb::canonical`]). -#[derive(Clone, Debug)] -struct Alias { - /// The family's own name. - family: String, - /// The face the name names, for legacy family names, full names and PostScript names. - style: Option, - /// A legacy family name's legacy style (`Regular` beside `Hiragino Sans W3`). - paired: Option, -} - -impl Alias { - /// The aliases of a face `family` / `style` with `keys`, by normalized name. - fn of(family: &str, style: &str, keys: &FaceKeys) -> Vec<(String, Alias)> { - #[cfg(test)] - ALIAS_LISTS.with(|c| c.set(c.get() + 1)); - let mut v: Vec<(String, Alias)> = - keys.families.iter().map(|k| (k.clone(), Alias { family: family.to_string(), style: None, paired: None })).collect(); - v.extend( - keys.legacy - .iter() - .map(|(f, s)| (f.clone(), Alias { family: family.to_string(), style: Some(style.to_string()), paired: Some(s.clone()) })), - ); - v.extend( - keys.postscript - .iter() - .chain(&keys.full_names) - .map(|k| (k.clone(), Alias { family: family.to_string(), style: Some(style.to_string()), paired: None })), - ); - v - } -} - -#[cfg(test)] -thread_local! { - /// Tests: counts the faces whose aliases ([`Alias::of`]) were listed on this thread. - static ALIAS_LISTS: std::cell::Cell = const { std::cell::Cell::new(0) }; -} - -/// Tests: returns how many faces' aliases were listed on this thread. -#[cfg(test)] -pub(crate) fn alias_lists() -> u64 { - ALIAS_LISTS.with(std::cell::Cell::get) -} - -#[cfg(test)] -thread_local! { - /// Tests: run on this thread each time it publishes faces to a database, to look the database - /// up at exactly that point (as another thread could). - static ON_FACES_PUBLISHED: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }; -} - -/// Tests: run `hook` each time this thread publishes faces to a database. -#[cfg(test)] -pub(crate) fn on_faces_published(hook: Option>) { - ON_FACES_PUBLISHED.with(|h| *h.borrow_mut() = hook); -} - -#[cfg(test)] -fn faces_published() { - // Taken out while it runs, so a hook that adds faces itself doesn't run again. - if let Some(mut hook) = ON_FACES_PUBLISHED.with(|h| h.borrow_mut().take()) { - hook(); - ON_FACES_PUBLISHED.with(|h| { - h.borrow_mut().get_or_insert(hook); - }); - } -} - -/// How a requested family and style resolved ([`FontDb::resolve`]). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum FontMatch { - /// The face of that family and style (by any of its names, in any language). - Exact, - /// The family is there but not the style: its closest style stands in. - Style, - /// The family is unknown: the fallback family stands in. - Missing, -} - -impl FontMatch { - pub fn as_str(self) -> &'static str { - match self { - FontMatch::Exact => "exact", - FontMatch::Style => "substitute", - FontMatch::Missing => "missing", - } - } -} - -/// A face's (family, style) names. -fn face_names(f: &skrifa::FontRef<'_>) -> Option<(String, String)> { - let family = name(f, &[StringId::TYPOGRAPHIC_FAMILY_NAME, StringId::FAMILY_NAME])?; - let style = name(f, &[StringId::TYPOGRAPHIC_SUBFAMILY_NAME, StringId::SUBFAMILY_NAME]).unwrap_or_else(|| "Regular".into()); - Some((family, style)) -} - -/// One style a font file offers: one of its faces, or a named instance of a variable font -/// (Figtree's `Figtree[wght].ttf` holds Light, the default, and Regular to Black as instances). -#[derive(Clone, Debug)] -pub(crate) struct FaceStyle { - family: String, - /// See [`FontFace::native_family`]. - native_family: Option, - style: String, - keys: FaceKeys, - /// A named instance's axis settings (user units); empty for the face itself. - variations: Vec, - /// A named instance's weight (its `wght` setting); `None` for the face itself (its OS/2 table - /// says) or an instance of a font without a weight axis. - weight: Option, - /// A named instance is italic (by its `ital` or `slnt` setting, or its name). - italic: bool, - /// Its kind, for font menus' filters (the face's, shared by its named instances). - traits: FontTraits, -} - -/// The styles of a face: the face itself, then its named instances when it is a variable font. -pub(crate) fn face_styles(f: &skrifa::FontRef<'_>) -> Vec { - let Some((family, style)) = face_names(f) else { return vec![] }; - let native_family = native_family_name(f, &family); - let mut keys = FaceKeys::of(f, &family, &style); - let traits = FontTraits::of(f, &keys); - let (mut instances, renamed) = named_instances(f, &family, &style, &mut keys, native_family.clone()); - for i in &mut instances { - i.traits = traits; - } - // A variable font's default face renders at its `wght` default, whatever its name table and - // OS/2 table say (Noto Sans Hebrew: named `Light`, weight class 300, default instance Regular - // at 400): weigh it so, and name it by its default instance when that is another weight. - let weight = f - .axes() - .iter() - .find(|a| a.tag().to_be_bytes() == *b"wght") - .map(|a| a.default_value()) - .filter(|w| w.is_finite()) - .map(|w| w.clamp(1.0, 1000.0)); - let style = renamed.unwrap_or(style); - let face = FaceStyle { family, native_family, style, keys, variations: vec![], weight, italic: false, traits }; - std::iter::once(face).chain(instances).collect() -} - -/// The named instances of a variable font face (its `fvar` table) as styles of the face's family, -/// named by their subfamily names, without the default instance (the face itself) and without -/// names already taken. The default instance's name, when it isn't the face's style name (a face -/// `Regular` whose default instance is `Book`), names the face too (`keys`). Damaged tables give -/// fewer instances or none; the counts read are capped. -fn named_instances( - f: &skrifa::FontRef<'_>, - family: &str, - style: &str, - keys: &mut FaceKeys, - native_family: Option, -) -> (Vec, Option) { - let axes = f.axes(); - if axes.is_empty() || axes.len() > MAX_AXES { - return (vec![], None); - } - let tags: Vec = axes.iter().map(|a| a.tag()).collect(); - let defaults: Vec = axes.iter().map(|a| a.default_value()).collect(); - let is_default = |coords: &[f32]| coords.len() == defaults.len() && coords.iter().zip(&defaults).all(|(c, d)| (c - d).abs() < 1e-3); - // The face renders as its default instance. When that instance is another weight than the - // name table says (`Light` face, Regular default instance), the instance names the face. - let renamed = tags.iter().any(|t| t.to_be_bytes() == *b"wght").then(|| { - f.named_instances() - .iter() - .take(MAX_INSTANCES) - .filter(|inst| is_default(&inst.user_coords().take(MAX_AXES).collect::>())) - .find_map(|inst| name(f, &[inst.subfamily_name_id()])) - .filter(|d| style_weight(d) != style_weight(style)) - }); - let renamed = renamed.flatten(); - // Instances without a PostScript name of their own are named `-