//! Font database: bundled OFL fonts, user fonts, the installed system fonts (cataloged once, loaded //! on demand), outline cache. use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; use std::sync::{Arc, Mutex, RwLock}; use kurbo::BezPath; use skrifa::instance::{Location, LocationRef, Size}; use skrifa::outline::{DrawSettings, OutlinePen}; use skrifa::raw::FileRef; use skrifa::string::StringId; use skrifa::{GlyphId, MetadataProvider, Tag}; /// The family used when a requested family is unknown (Illustrator's Myriad Pro analogue). pub const FALLBACK_FAMILY: &str = "Source Sans 3"; 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"), ]; /// 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; 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, } /// 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 } /// 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) } /// 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 } } /// 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). 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; /// Process-wide font database. pub struct FontDb { faces: RwLock>>, outlines: Mutex>>, catalog: RwLock, /// The installed faces by PostScript name (documents name fonts so: PDF, EPS, .ai). postscript: RwLock, /// Where the system font scan looks. #[cfg_attr(target_arch = "wasm32", allow(dead_code))] font_paths: FontPaths, /// The installed fonts' other names (localized, legacy, full, PostScript) by normalized name. aliases: RwLock>>, /// Set once the font folders have been scanned. Lookups by family name wait for the first /// scan, so what they find never depends on what ran before them. #[cfg(not(target_arch = "wasm32"))] cataloged: std::sync::OnceLock<()>, /// Families (lowercased) whose installed files were all loaded to find other versions of their /// faces (see [`FontDb::versions`]); a rescan forgets them. #[cfg(not(target_arch = "wasm32"))] version_scanned: RwLock>, /// [`FontDb::family_list`], built on demand and dropped when fonts are added or rescanned. family_cache: Mutex>>, menu_cache: Mutex>, generation: AtomicU64, /// System fallback state: characters no system font covers. #[cfg(not(target_arch = "wasm32"))] sys: Mutex, /// What the last scan read, to tell when fonts were installed or removed since. #[cfg(not(target_arch = "wasm32"))] stamp: Mutex>, } /// Where a database's system font scan looks: folders (read with their subfolders) and font files. #[cfg_attr(target_arch = "wasm32", allow(dead_code))] enum FontPaths { Fixed(Vec), /// The platform's ([`system_font_dirs`]), looked up again by each scan: fonts registered and /// font folders added since count too. System, } #[cfg_attr(target_arch = "wasm32", allow(dead_code))] impl FontPaths { fn get(&self) -> Vec { match self { FontPaths::Fixed(paths) => paths.clone(), FontPaths::System => system_font_dirs(), } } } /// What a system font scan read: the paths it started from, and when each folder it listed (and /// font file named by itself) last changed. Installing or removing a font changes its folder's. #[cfg(not(target_arch = "wasm32"))] struct ScanStamp { paths: Vec, modified: Vec<(PathBuf, Option)>, } #[cfg(not(target_arch = "wasm32"))] 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; 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())) } /// 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 { let pairs = { let t = localized(font, StringId::TYPOGRAPHIC_FAMILY_NAME); if t.is_empty() { localized(font, StringId::FAMILY_NAME) } else { t } }; pick_native_label(english, &pairs) } /// 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 (legacy_family, legacy_style) = (localized(font, StringId::FAMILY_NAME), localized(font, StringId::SUBFAMILY_NAME)); // 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, and its kind. #[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, } impl FontTraits { /// From a face's OS/2 table (when it has one) 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 }; FontTraits { japanese, thai, class } } } /// 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)> { 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 } } /// 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 = named_instances(f, &family, &style, &mut keys, native_family.clone()); for i in &mut instances { i.traits = traits; } let face = FaceStyle { family, native_family, style, keys, variations: vec![], weight: None, 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 { let axes = f.axes(); if axes.is_empty() || axes.len() > MAX_AXES { return vec![]; } let tags: Vec = axes.iter().map(|a| a.tag()).collect(); let defaults: Vec = axes.iter().map(|a| a.default_value()).collect(); // Instances without a PostScript name of their own are named `-