mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
* Emoji: colour glyphs, and emoji take the colour emoji font Emoji were drawn as black outlines from a symbol font, with ZWJ sequences (families, flags) falling apart into their parts. Now: - an emoji cluster (emoji presentation, U+FE0F, ZWJ between pictographs, skin tones, regional indicators, keycaps) takes an installed colour font (COLR, CBDT, sbix or SVG tables) before the document's fonts, so its sequences ligate as in PowerPoint; U+FE0E keeps a plain face; - COLR v0 and v1 glyphs are painted in their CPAL colours (solid fills, linear, radial and sweep gradients, transforms, composite modes), with palette index 0xFFFF in the text colour. A tiny COLRv0 test font (two squares, CC0, regenerated by make_colr_font.py) backs the render test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Render: lock skrifa for colour glyphs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1016 lines
41 KiB
Rust
1016 lines
41 KiB
Rust
//! Font database: bundled OFL fonts, user fonts, the installed system fonts (cataloged the first
|
|
//! time a lookup needs them, native only), outline cache.
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::atomic::{AtomicU32, 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};
|
|
|
|
/// The family used when a requested family is unknown and has no substitute.
|
|
pub const FALLBACK_FAMILY: &str = "Inter";
|
|
|
|
/// The always-available last-resort face (Ubuntu Light, compiled in through
|
|
/// `epaint_default_fonts`; licence in ATTRIBUTION.md).
|
|
pub const LAST_RESORT_FAMILY: &str = "Ubuntu";
|
|
|
|
/// Compiled-in font files: the craft-fonts faces (Latin presentation fonts, then Japanese;
|
|
/// empty without `CRAFT_FONTS_DIR`), then the last-resort face.
|
|
pub fn bundled() -> Vec<&'static [u8]> {
|
|
let mut v: Vec<&'static [u8]> = crate::CRAFT_FONTS.iter().map(|f| f.bytes).collect();
|
|
v.push(epaint_default_fonts::UBUNTU_LIGHT);
|
|
v
|
|
}
|
|
|
|
/// Open substitutes for families documents often ask for but we can't ship (metric-compatible
|
|
/// where one exists), tried in order after the family itself.
|
|
pub fn substitutes(family: &str) -> &'static [&'static str] {
|
|
match family.to_ascii_lowercase().as_str() {
|
|
"calibri" | "calibri light" => &["Carlito", "Liberation Sans", "Arial", "Inter"],
|
|
"cambria" | "cambria math" => &["Caladea", "Liberation Serif", "Times New Roman", "Merriweather"],
|
|
"arial" | "helvetica" | "helvetica neue" | "arial nova" => &["Liberation Sans", "Arial", "Helvetica Neue", "Helvetica", "Inter"],
|
|
"times new roman" | "times" => &["Liberation Serif", "Times New Roman", "Times", "Merriweather"],
|
|
"courier new" | "courier" | "consolas" | "menlo" => &["Liberation Mono", "Courier New", "Menlo", "Courier"],
|
|
"aptos" | "aptos display" | "aptos narrow" | "segoe ui" | "segoe ui light" | "segoe ui semibold" | "tahoma" | "verdana" => {
|
|
&["Inter", "Helvetica Neue", "Arial", "Liberation Sans"]
|
|
}
|
|
"georgia" | "garamond" | "book antiqua" | "palatino" | "palatino linotype" | "constantia" => &["Merriweather", "Liberation Serif", "Georgia"],
|
|
"century gothic" | "avenir" | "avenir next" | "futura" | "gill sans" | "gill sans mt" | "trebuchet ms" | "corbel" | "candara" => {
|
|
&["Montserrat", "Poppins", "Avenir Next", "Inter"]
|
|
}
|
|
"franklin gothic" | "franklin gothic book" | "franklin gothic medium" | "univers" => &["Open Sans", "Inter"],
|
|
"open sans" | "lato" | "roboto" | "montserrat" | "poppins" | "nunito sans" => &["Inter"],
|
|
"playfair display" | "merriweather" | "source serif 4" | "source serif pro" => &["Merriweather", "Liberation Serif", "Georgia"],
|
|
"source sans 3" | "source sans pro" => &["Open Sans", "Inter"],
|
|
"impact" | "arial black" => &["Montserrat", "Inter"],
|
|
_ => &[],
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
enum FontBytes {
|
|
Static(&'static [u8]),
|
|
Owned(Arc<Vec<u8>>),
|
|
}
|
|
|
|
/// One loaded font face.
|
|
pub struct FontFace {
|
|
id: u32,
|
|
/// Typographic family name (e.g. "Source Sans 3").
|
|
pub family: String,
|
|
/// Typographic style name (e.g. "Semibold", "Italic").
|
|
pub style: String,
|
|
/// usWeightClass-style weight (400 = regular).
|
|
pub weight: f32,
|
|
pub italic: bool,
|
|
bytes: FontBytes,
|
|
index: u32,
|
|
pub upem: f64,
|
|
/// Ascender in font units (positive = up).
|
|
pub ascent: f64,
|
|
/// Descender in font units (positive = down).
|
|
pub descent: f64,
|
|
/// Cap height and x height in font units (estimated from the ascent when the font has no OS/2
|
|
/// values).
|
|
pub cap_height: f64,
|
|
pub x_height: f64,
|
|
/// Where the baseline sits in a line, as a share of the line's height from its top: the
|
|
/// Windows ascent over the Windows ascent plus descent (OS/2 `usWinAscent`/`usWinDescent`),
|
|
/// the split PowerPoint uses (computed by `line_split`).
|
|
pub line_split: f64,
|
|
pub shaper: harfrust::ShaperData,
|
|
/// Variable fonts: the named instance's axis settings (user units; empty = default instance),
|
|
/// the normalised location and the shaper's view of it.
|
|
pub coords: Vec<([u8; 4], f32)>,
|
|
location: Location,
|
|
pub instance: Option<harfrust::ShaperInstance>,
|
|
/// Basic Multilingual Plane coverage bitset, built on first use.
|
|
bmp: std::sync::OnceLock<Box<[u64]>>,
|
|
/// Colour glyph tables present (`is_color`), looked up on first use.
|
|
color: std::sync::OnceLock<bool>,
|
|
}
|
|
|
|
/// A cheap `Copy` handle to a face. Faces are never unloaded, so the handle lives for the rest of
|
|
/// the process; unlike cloning an `Arc`, copying it touches no shared reference count (which made
|
|
/// parallel composition scale negatively).
|
|
#[derive(Clone, Copy)]
|
|
pub struct FaceRef(&'static FontFace);
|
|
|
|
impl FaceRef {
|
|
/// The handle for a loaded face (memoised per face).
|
|
pub fn of(face: &Arc<FontFace>) -> FaceRef {
|
|
static LEAKED: RwLock<Vec<Option<&'static FontFace>>> = RwLock::new(Vec::new());
|
|
let id = face.id as usize;
|
|
if let Some(Some(f)) = LEAKED.read().unwrap_or_else(|e| e.into_inner()).get(id) {
|
|
return FaceRef(f);
|
|
}
|
|
let mut w = LEAKED.write().unwrap_or_else(|e| e.into_inner());
|
|
if w.len() <= id {
|
|
w.resize(id + 1, None);
|
|
}
|
|
let f: &'static FontFace = w[id].unwrap_or_else(|| {
|
|
let keep: &'static Arc<FontFace> = Box::leak(Box::new(face.clone()));
|
|
keep
|
|
});
|
|
w[id] = Some(f);
|
|
FaceRef(f)
|
|
}
|
|
pub fn get(self) -> &'static FontFace {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl std::ops::Deref for FaceRef {
|
|
type Target = FontFace;
|
|
fn deref(&self) -> &FontFace {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for FaceRef {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
self.0.fmt(f)
|
|
}
|
|
}
|
|
|
|
impl PartialEq for FaceRef {
|
|
fn eq(&self, o: &FaceRef) -> bool {
|
|
self.0.id == o.0.id
|
|
}
|
|
}
|
|
|
|
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 fn data(&self) -> &[u8] {
|
|
match &self.bytes {
|
|
FontBytes::Static(b) => b,
|
|
FontBytes::Owned(v) => v.as_slice(),
|
|
}
|
|
}
|
|
pub fn skrifa(&self) -> Option<skrifa::FontRef<'_>> {
|
|
skrifa::FontRef::from_index(self.data(), self.index).ok()
|
|
}
|
|
/// Does the face draw colour glyphs (a `COLR`, `CBDT`, `sbix` or `SVG ` table, as colour emoji
|
|
/// fonts have)?
|
|
pub fn is_color(&self) -> bool {
|
|
*self.color.get_or_init(|| has_color_tables(self.data(), self.index))
|
|
}
|
|
pub fn hb(&self) -> Option<harfrust::FontRef<'_>> {
|
|
harfrust::FontRef::from_index(self.data(), self.index).ok()
|
|
}
|
|
/// Where in the design space glyphs, metrics and outlines come from.
|
|
pub fn location(&self) -> LocationRef<'_> {
|
|
(&self.location).into()
|
|
}
|
|
/// Is this a named instance of a variable font?
|
|
pub fn is_variable(&self) -> bool {
|
|
!self.coords.is_empty()
|
|
}
|
|
/// Face index within the font file (collections); 0 for plain fonts.
|
|
pub fn index(&self) -> u32 {
|
|
self.index
|
|
}
|
|
/// 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 {
|
|
let cp = c as u32;
|
|
if cp < 0x1_0000 {
|
|
let bits = self.bmp.get_or_init(|| {
|
|
let mut b = vec![0u64; 1024].into_boxed_slice();
|
|
if let Some(f) = self.skrifa() {
|
|
for (cp, _) in f.charmap().mappings() {
|
|
if cp < 0x1_0000 {
|
|
b[(cp / 64) as usize] |= 1 << (cp % 64);
|
|
}
|
|
}
|
|
}
|
|
b
|
|
});
|
|
return bits[(cp / 64) as usize] & (1 << (cp % 64)) != 0;
|
|
}
|
|
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 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)
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
#[derive(Clone, Debug)]
|
|
struct CatalogEntry {
|
|
family: String,
|
|
style: String,
|
|
path: std::path::PathBuf,
|
|
}
|
|
|
|
/// Process-wide font database.
|
|
pub struct FontDb {
|
|
faces: RwLock<Vec<Arc<FontFace>>>,
|
|
outlines: Mutex<HashMap<(u32, u32), Arc<BezPath>>>,
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
catalog: RwLock<Vec<CatalogEntry>>,
|
|
/// The folders the system font scan reads (the platform's font folders for [`FontDb::global`]).
|
|
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
|
font_dirs: Vec<std::path::PathBuf>,
|
|
/// Set once the font folders have been scanned. Lookups by family name wait for the first
|
|
/// scan, so what they find doesn't depend on what ran before them.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
cataloged: std::sync::OnceLock<()>,
|
|
/// System fallback state: enabled, and characters no system font covers.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
sys: Mutex<SysFallback>,
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
#[derive(Default)]
|
|
struct SysFallback {
|
|
enabled: bool,
|
|
misses: std::collections::HashSet<char>,
|
|
}
|
|
|
|
/// Does face `index` of `data` carry colour glyph tables?
|
|
fn has_color_tables(data: &[u8], index: u32) -> bool {
|
|
use skrifa::raw::types::Tag;
|
|
let Ok(f) = skrifa::FontRef::from_index(data, index) else { return false };
|
|
[b"COLR", b"CBDT", b"sbix", b"SVG "].iter().any(|t| f.table_data(Tag::new(t)).is_some())
|
|
}
|
|
|
|
/// 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<String> {
|
|
ids.iter().find_map(|id| font.localized_strings(*id).english_or_first().map(|s| s.to_string()).filter(|s| !s.is_empty()))
|
|
}
|
|
|
|
/// A face's family and style names (the default instance's style for a variable font).
|
|
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))
|
|
}
|
|
|
|
/// (family, style) of every face in the font file at `path`, reading only its table directories
|
|
/// and `name` tables: a scan opens hundreds of font files, many of them megabytes long. A
|
|
/// variable font is cataloged under its default style; its named instances appear once it loads.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn file_face_names(path: &std::path::Path) -> Vec<(String, String)> {
|
|
use std::io::{Read, Seek, SeekFrom};
|
|
/// Caps on what a (possibly damaged) file can make the scan read.
|
|
const MAX_FACES: u32 = 256;
|
|
const MAX_NAME_TABLE: u32 = 1 << 20;
|
|
let Ok(mut file) = std::fs::File::open(path) else { return vec![] };
|
|
let mut read_at = |offset: u64, len: usize| -> Option<Vec<u8>> {
|
|
let mut buf = vec![0; len];
|
|
file.seek(SeekFrom::Start(offset)).ok()?;
|
|
file.read_exact(&mut buf).ok()?;
|
|
Some(buf)
|
|
};
|
|
let be32 = |b: &[u8], at: usize| b.get(at..at + 4).and_then(|s| s.try_into().ok()).map(u32::from_be_bytes);
|
|
let Some(head) = read_at(0, 12) else { return vec![] };
|
|
// A collection lists where each face's table directory starts.
|
|
let starts: Vec<u32> = if head.starts_with(b"ttcf") {
|
|
let n = be32(&head, 8).unwrap_or(0).min(MAX_FACES) as usize;
|
|
read_at(12, n * 4).map(|b| b.as_chunks::<4>().0.iter().map(|c| u32::from_be_bytes(*c)).collect()).unwrap_or_default()
|
|
} else {
|
|
vec![0]
|
|
};
|
|
starts
|
|
.into_iter()
|
|
.filter_map(|start| {
|
|
let dir = read_at(start.into(), 12)?;
|
|
let tables = u16::from_be_bytes(dir.get(4..6)?.try_into().ok()?) as usize;
|
|
let records = read_at(u64::from(start) + 12, tables * 16)?;
|
|
let rec = records.as_chunks::<16>().0.iter().find(|r| r.starts_with(b"name"))?;
|
|
let (offset, len) = (be32(rec, 8)?, be32(rec, 12)?);
|
|
if len > MAX_NAME_TABLE {
|
|
return None;
|
|
}
|
|
let table = read_at(offset.into(), len as usize)?;
|
|
// A one-table font holding just the `name` table, to read it as the font itself would.
|
|
let mut font = Vec::with_capacity(28 + table.len());
|
|
font.extend_from_slice(&0x0001_0000_u32.to_be_bytes());
|
|
font.extend_from_slice(&[0, 1, 0, 16, 0, 0, 0, 0]);
|
|
font.extend_from_slice(b"name");
|
|
for v in [0, 28, len] {
|
|
font.extend_from_slice(&u32::to_be_bytes(v));
|
|
}
|
|
font.extend_from_slice(&table);
|
|
face_names(&skrifa::FontRef::new(&font).ok()?)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// The platform's font folders (the system's and the user's), scanned by [`FontDb::global`].
|
|
/// Empty on wasm.
|
|
pub fn system_font_dirs() -> Vec<std::path::PathBuf> {
|
|
let mut dirs: Vec<std::path::PathBuf> = Vec::new();
|
|
if cfg!(target_arch = "wasm32") {
|
|
return dirs;
|
|
}
|
|
let home = std::env::var_os("HOME").map(std::path::PathBuf::from);
|
|
if cfg!(target_os = "macos") {
|
|
dirs.extend(["/System/Library/Fonts", "/Library/Fonts"].map(Into::into));
|
|
if let Some(h) = &home {
|
|
dirs.push(h.join("Library/Fonts"));
|
|
}
|
|
// Fonts macOS ships as assets (PingFang, Yu Mincho, …): com_apple_MobileAsset_Font<N>.
|
|
for parent in ["/System/Library/AssetsV2", "/System/Library/AssetsV2/PreinstalledAssetsV2/InstallWithOs"] {
|
|
let Ok(entries) = std::fs::read_dir(parent) else { continue };
|
|
for e in entries.flatten() {
|
|
if e.file_name().to_string_lossy().starts_with("com_apple_MobileAsset_Font") {
|
|
dirs.push(e.path());
|
|
}
|
|
}
|
|
}
|
|
} else if cfg!(windows) {
|
|
let root = std::env::var_os("WINDIR").map(std::path::PathBuf::from).unwrap_or_else(|| "C:\\Windows".into());
|
|
dirs.push(root.join("Fonts"));
|
|
if let Some(l) = std::env::var_os("LOCALAPPDATA") {
|
|
dirs.push(std::path::PathBuf::from(l).join("Microsoft\\Windows\\Fonts"));
|
|
}
|
|
} else {
|
|
dirs.extend(["/usr/share/fonts", "/usr/local/share/fonts"].map(Into::into));
|
|
if let Some(h) = &home {
|
|
dirs.push(h.join(".fonts"));
|
|
dirs.push(h.join(".local/share/fonts"));
|
|
}
|
|
}
|
|
dirs
|
|
}
|
|
|
|
/// One face found in a font file: index, family, style and (variable fonts) the named instance's
|
|
/// axis settings.
|
|
type Found = (u32, String, String, Vec<([u8; 4], f32)>);
|
|
|
|
/// Parse every face in `data` (a font file or collection); a variable font yields one face per
|
|
/// named instance (InDesign lists them as styles).
|
|
fn enumerate_faces(data: &[u8]) -> Vec<Found> {
|
|
let count = match FileRef::new(data) {
|
|
Ok(FileRef::Font(_)) => 1,
|
|
Ok(FileRef::Collection(c)) => c.len(),
|
|
Err(_) => 0,
|
|
};
|
|
let mut out = Vec::new();
|
|
for i in 0..count {
|
|
let Ok(f) = skrifa::FontRef::from_index(data, i) else { continue };
|
|
let Some((family, style)) = face_names(&f) else { continue };
|
|
let axes = f.axes();
|
|
let mut named = Vec::new();
|
|
for ni in f.named_instances().iter() {
|
|
let Some(style) = name(&f, &[ni.subfamily_name_id()]) else { continue };
|
|
if named.iter().any(|(_, s, _): &(u32, String, _)| s.eq_ignore_ascii_case(&style)) {
|
|
continue;
|
|
}
|
|
let coords: Vec<([u8; 4], f32)> = axes.iter().zip(ni.user_coords()).map(|(a, v)| (a.tag().to_be_bytes(), v)).collect();
|
|
named.push((i, style, coords));
|
|
}
|
|
if named.is_empty() {
|
|
out.push((i, family, style, Vec::new()));
|
|
} else {
|
|
out.extend(named.into_iter().map(|(i, s, c)| (i, family.clone(), s, c)));
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// The compiled-in last-resort face, for a database that somehow has no usable faces at all.
|
|
pub(crate) fn last_resort_face() -> Arc<FontFace> {
|
|
static FACE: std::sync::OnceLock<Arc<FontFace>> = std::sync::OnceLock::new();
|
|
FACE.get_or_init(|| {
|
|
// The font is compiled in (`include_bytes!`), so parsing it can't depend on input; the
|
|
// `last_resort_face_parses` test proves it on every run.
|
|
#[allow(clippy::expect_used)]
|
|
let face = make_face(FontBytes::Static(epaint_default_fonts::UBUNTU_LIGHT), 0, LAST_RESORT_FAMILY.into(), "Regular".into(), Vec::new())
|
|
.expect("the compiled-in last-resort face parses");
|
|
Arc::new(face)
|
|
})
|
|
.clone()
|
|
}
|
|
|
|
/// The share of a line above the baseline (`FontFace::line_split`): OS/2 `usWinAscent` over
|
|
/// `usWinAscent + usWinDescent`, else the ascent over ascent plus descent. Fonts whose Windows
|
|
/// extent is huge (Gabriola, Cambria Math: over 1.5 em) get 0.81, where PowerPoint puts their
|
|
/// baseline (observed).
|
|
fn line_split(f: &skrifa::FontRef, ascent: f64, descent: f64, upem: f64) -> f64 {
|
|
use skrifa::raw::TableProvider;
|
|
let (a, d) = match f.os2() {
|
|
Ok(os2) if os2.us_win_ascent() > 0 => (os2.us_win_ascent() as f64, os2.us_win_descent() as f64),
|
|
_ => (ascent, descent),
|
|
};
|
|
let total = a + d;
|
|
if !total.is_finite() || total <= 0.0 || a < 0.0 || d < 0.0 {
|
|
return 0.8;
|
|
}
|
|
if total > upem * 1.5 { 0.81 } else { (a / total).clamp(0.5, 1.0) }
|
|
}
|
|
|
|
fn make_face(bytes: FontBytes, index: u32, family: String, style: String, coords: Vec<([u8; 4], f32)>) -> Option<FontFace> {
|
|
let data: &[u8] = match &bytes {
|
|
FontBytes::Static(b) => b,
|
|
FontBytes::Owned(v) => v.as_slice(),
|
|
};
|
|
let f = skrifa::FontRef::from_index(data, index).ok()?;
|
|
let settings: Vec<(skrifa::Tag, f32)> = coords.iter().map(|(t, v)| (skrifa::Tag::new(t), *v)).collect();
|
|
let location = if coords.is_empty() { Location::default() } else { f.axes().location(settings.iter().copied()) };
|
|
let m = f.metrics(Size::unscaled(), &location);
|
|
let a = f.attributes();
|
|
let hb = harfrust::FontRef::from_index(data, index).ok()?;
|
|
let shaper = harfrust::ShaperData::new(&hb);
|
|
let instance = (!coords.is_empty())
|
|
.then(|| harfrust::ShaperInstance::from_variations(&hb, settings.iter().map(|(t, v)| harfrust::Variation { tag: *t, value: *v })));
|
|
let axis = |tag: &[u8; 4]| coords.iter().find(|(t, _)| t == tag).map(|(_, v)| *v);
|
|
let weight = axis(b"wght").unwrap_or(if coords.is_empty() { a.weight.value() } else { style_weight(&style) });
|
|
let italic = axis(b"ital")
|
|
.map(|v| v >= 0.5)
|
|
.or(axis(b"slnt").map(|v| v.abs() > 0.1))
|
|
.unwrap_or(!matches!(a.style, skrifa::attribute::Style::Normal) || style_italic(&style));
|
|
Some(FontFace {
|
|
id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
|
|
family,
|
|
style,
|
|
weight,
|
|
italic,
|
|
upem: m.units_per_em.max(1) as f64,
|
|
ascent: m.ascent as f64,
|
|
descent: -(m.descent as f64),
|
|
cap_height: m.cap_height.map(|v| v as f64).filter(|v| *v > 0.0).unwrap_or(m.ascent as f64 * 0.72),
|
|
x_height: m.x_height.map(|v| v as f64).filter(|v| *v > 0.0).unwrap_or(m.ascent as f64 * 0.5),
|
|
line_split: line_split(&f, m.ascent as f64, -(m.descent as f64), m.units_per_em.max(1) as f64),
|
|
shaper,
|
|
coords,
|
|
location,
|
|
instance,
|
|
bytes,
|
|
index,
|
|
bmp: std::sync::OnceLock::new(),
|
|
color: std::sync::OnceLock::new(),
|
|
})
|
|
}
|
|
|
|
/// The named style part of a style with axis settings (`Bold {wght:650}` → `Bold`).
|
|
pub fn base_style(style: &str) -> &str {
|
|
style.split('{').next().unwrap_or(style).trim()
|
|
}
|
|
|
|
fn norm(s: &str) -> String {
|
|
s.chars().filter(|c| c.is_alphanumeric()).flat_map(|c| c.to_lowercase()).collect()
|
|
}
|
|
|
|
/// Weight implied by a style name.
|
|
fn style_weight(style: &str) -> f32 {
|
|
let s = norm(style);
|
|
const TABLE: &[(&str, f32)] = &[
|
|
("extralight", 200.0),
|
|
("ultralight", 200.0),
|
|
("semibold", 600.0),
|
|
("demibold", 600.0),
|
|
("extrabold", 800.0),
|
|
("ultrabold", 800.0),
|
|
("hairline", 100.0),
|
|
("thin", 100.0),
|
|
("light", 300.0),
|
|
("medium", 500.0),
|
|
("bold", 700.0),
|
|
("black", 900.0),
|
|
("heavy", 900.0),
|
|
];
|
|
TABLE.iter().find(|(k, _)| s.contains(k)).map(|(_, w)| *w).unwrap_or(400.0)
|
|
}
|
|
|
|
fn style_italic(style: &str) -> bool {
|
|
let s = norm(style);
|
|
s.contains("italic") || s.contains("oblique") || s == "it"
|
|
}
|
|
|
|
impl FontDb {
|
|
/// A database holding the bundled fonts, then the craft-fonts Japanese faces (Mincho first,
|
|
/// so they are the fallback for Japanese text after the requested and bundled fonts; empty
|
|
/// without `CRAFT_FONTS_DIR`), whose system font scan reads `font_dirs`.
|
|
pub fn with_font_dirs(font_dirs: Vec<std::path::PathBuf>) -> Self {
|
|
let mut faces = Vec::new();
|
|
for data in bundled() {
|
|
for (i, family, style, coords) in enumerate_faces(data) {
|
|
if let Some(f) = make_face(FontBytes::Static(data), i, family, style, coords) {
|
|
faces.push(Arc::new(f));
|
|
}
|
|
}
|
|
}
|
|
Self {
|
|
faces: RwLock::new(faces),
|
|
outlines: Mutex::new(HashMap::new()),
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
catalog: RwLock::new(Vec::new()),
|
|
font_dirs,
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
cataloged: std::sync::OnceLock::new(),
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
sys: Mutex::new(SysFallback { enabled: true, ..Default::default() }),
|
|
}
|
|
}
|
|
|
|
/// Enable or disable the lazy system-font fallback for characters the loaded fonts lack
|
|
/// (native only; on by default).
|
|
pub fn set_system_fallback(&self, on: bool) {
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
{
|
|
self.sys.lock().unwrap_or_else(|e| e.into_inner()).enabled = on;
|
|
}
|
|
#[cfg(target_arch = "wasm32")]
|
|
let _ = on;
|
|
}
|
|
|
|
/// Process-wide database: the bundled fonts, then the installed system fonts, cataloged the
|
|
/// first time a lookup by family name needs them (or ahead of time by
|
|
/// [`FontDb::scan_in_background`]).
|
|
pub fn global() -> &'static FontDb {
|
|
static DB: std::sync::OnceLock<FontDb> = std::sync::OnceLock::new();
|
|
DB.get_or_init(|| FontDb::with_font_dirs(system_font_dirs()))
|
|
}
|
|
|
|
pub(crate) fn read_faces(&self) -> std::sync::RwLockReadGuard<'_, Vec<Arc<FontFace>>> {
|
|
self.faces.read().unwrap_or_else(|e| e.into_inner())
|
|
}
|
|
|
|
/// The system font catalog, scanned first if it hasn't been yet.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn read_catalog(&self) -> std::sync::RwLockReadGuard<'_, Vec<CatalogEntry>> {
|
|
self.ensure_catalog();
|
|
self.catalog.read().unwrap_or_else(|e| e.into_inner())
|
|
}
|
|
|
|
/// Family names available (loaded plus cataloged system fonts), sorted and deduplicated.
|
|
pub fn families(&self) -> Vec<String> {
|
|
let mut v: Vec<String> = self.read_faces().iter().map(|f| f.family.clone()).collect();
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
v.extend(self.read_catalog().iter().map(|c| c.family.clone()));
|
|
v.sort_by_key(|a| a.to_lowercase());
|
|
v.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
|
|
v
|
|
}
|
|
|
|
/// Style names available for `family` (Regular first, then by weight).
|
|
pub fn styles(&self, family: &str) -> Vec<String> {
|
|
let mut v: Vec<(bool, f32, String)> = self
|
|
.read_faces()
|
|
.iter()
|
|
.filter(|f| f.family.eq_ignore_ascii_case(family) && !f.style.contains('{'))
|
|
.map(|f| (f.italic, f.weight, f.style.clone()))
|
|
.collect();
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
for c in self.read_catalog().iter() {
|
|
if c.family.eq_ignore_ascii_case(family) && !v.iter().any(|(_, _, s)| s.eq_ignore_ascii_case(&c.style)) {
|
|
v.push((style_italic(&c.style), style_weight(&c.style), c.style.clone()));
|
|
}
|
|
}
|
|
v.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.total_cmp(&b.1)).then(a.2.cmp(&b.2)));
|
|
v.dedup_by(|a, b| a.2 == b.2);
|
|
v.into_iter().map(|t| t.2).collect()
|
|
}
|
|
|
|
/// Add a user font (TTF/OTF/TTC bytes). Returns the number of faces added (0 if unparseable or
|
|
/// every face was already present).
|
|
pub fn add_font(&self, bytes: Vec<u8>) -> usize {
|
|
let data = Arc::new(bytes);
|
|
let mut added = 0;
|
|
for (i, family, style, coords) in enumerate_faces(&data) {
|
|
if self.read_faces().iter().any(|f| f.family.eq_ignore_ascii_case(&family) && f.style.eq_ignore_ascii_case(&style)) {
|
|
continue;
|
|
}
|
|
if let Some(f) = make_face(FontBytes::Owned(data.clone()), i, family, style, coords) {
|
|
self.faces.write().unwrap_or_else(|e| e.into_inner()).push(Arc::new(f));
|
|
added += 1;
|
|
}
|
|
}
|
|
added
|
|
}
|
|
|
|
/// Catalog the installed fonts unless that has been done: the first caller scans the font
|
|
/// folders, any other waits for that scan to finish.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn ensure_catalog(&self) {
|
|
self.cataloged.get_or_init(|| {
|
|
self.scan_font_dirs();
|
|
});
|
|
}
|
|
|
|
/// Catalog the installed fonts on a background thread, so the first lookup by family name
|
|
/// (opening a file, the font menus) doesn't wait for the scan. A no-op once they are
|
|
/// cataloged, and on wasm.
|
|
pub fn scan_in_background(&'static self) {
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
if self.cataloged.get().is_none() {
|
|
// A failed spawn leaves the scan to the first lookup that needs it.
|
|
let _ = std::thread::Builder::new().name("font-scan".into()).spawn(move || self.ensure_catalog());
|
|
}
|
|
}
|
|
|
|
/// Scan the font folders again (fonts installed or removed since), cataloging the faces
|
|
/// found (native only). Font data is loaded when a cataloged family is first resolved.
|
|
/// Returns the number of faces cataloged.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub fn load_system_fonts(&self) -> usize {
|
|
// The first scan, or a rescan once it is done (never both at once).
|
|
let mut first = None;
|
|
self.cataloged.get_or_init(|| first = Some(self.scan_font_dirs()));
|
|
first.unwrap_or_else(|| self.scan_font_dirs())
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn scan_font_dirs(&self) -> usize {
|
|
let mut found = Vec::new();
|
|
let mut stack = self.font_dirs.clone();
|
|
// Each folder once, however links lead back to it.
|
|
let mut visited = std::collections::HashSet::new();
|
|
while let Some(d) = stack.pop() {
|
|
if !visited.insert(std::fs::canonicalize(&d).unwrap_or_else(|_| d.clone())) {
|
|
continue;
|
|
}
|
|
let Ok(rd) = std::fs::read_dir(&d) else { continue };
|
|
for e in rd.flatten() {
|
|
let p = e.path();
|
|
if p.is_dir() {
|
|
stack.push(p);
|
|
continue;
|
|
}
|
|
let ext = p.extension().and_then(|e| e.to_str()).map(|e| e.to_ascii_lowercase());
|
|
if !matches!(ext.as_deref(), Some("ttf" | "otf" | "ttc" | "otc")) {
|
|
continue;
|
|
}
|
|
for (family, style) in file_face_names(&p) {
|
|
found.push(CatalogEntry { family, style, path: p.clone() });
|
|
}
|
|
}
|
|
}
|
|
let n = found.len();
|
|
log::debug!("cataloged {n} system font faces");
|
|
*self.catalog.write().unwrap_or_else(|e| e.into_inner()) = found;
|
|
n
|
|
}
|
|
|
|
/// Load the files of the installed `family`. Returns whether any face was added.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn load_cataloged(&self, family: &str) -> bool {
|
|
let paths: Vec<std::path::PathBuf> = {
|
|
let cat = self.read_catalog();
|
|
let mut p: Vec<_> = cat.iter().filter(|c| c.family.eq_ignore_ascii_case(family)).map(|c| c.path.clone()).collect();
|
|
p.sort();
|
|
p.dedup();
|
|
p
|
|
};
|
|
let mut any = false;
|
|
for p in paths {
|
|
if let Ok(data) = std::fs::read(&p) {
|
|
any |= self.add_font(data) > 0;
|
|
}
|
|
}
|
|
any
|
|
}
|
|
|
|
/// Resolve a family + style to a face, falling back to the closest style of the family, then to
|
|
/// an open substitute ([`substitutes`]), then to Inter. Installed system fonts are found by name
|
|
/// whatever ran before.
|
|
pub fn face(&self, family: &str, style: &str) -> Arc<FontFace> {
|
|
if let Some(f) = self.try_family(family, style) {
|
|
return f;
|
|
}
|
|
for sub in substitutes(family) {
|
|
if let Some(f) = self.try_family(sub, style) {
|
|
return f;
|
|
}
|
|
}
|
|
for sub in [FALLBACK_FAMILY, "Helvetica Neue", "Arial", "Liberation Sans", "DejaVu Sans", "Segoe UI"] {
|
|
if let Some(f) = self.try_family(sub, style) {
|
|
return f;
|
|
}
|
|
}
|
|
self.find(LAST_RESORT_FAMILY, "Regular")
|
|
.or_else(|| self.find(FALLBACK_FAMILY, "Regular"))
|
|
.or_else(|| self.read_faces().first().cloned())
|
|
.unwrap_or_else(last_resort_face)
|
|
}
|
|
|
|
fn try_family(&self, family: &str, style: &str) -> Option<Arc<FontFace>> {
|
|
if let Some(f) = self.find(family, style) {
|
|
return Some(f);
|
|
}
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
if self.load_cataloged(family)
|
|
&& let Some(f) = self.find(family, style)
|
|
{
|
|
return Some(f);
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Is `family` available (loaded, or installed on the system)?
|
|
pub fn has_family(&self, family: &str) -> bool {
|
|
if self.is_loaded(family) {
|
|
return true;
|
|
}
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
if self.read_catalog().iter().any(|c| c.family.eq_ignore_ascii_case(family)) {
|
|
return true;
|
|
}
|
|
false
|
|
}
|
|
|
|
pub(crate) fn is_loaded(&self, family: &str) -> bool {
|
|
self.read_faces().iter().any(|f| f.family.eq_ignore_ascii_case(family))
|
|
}
|
|
|
|
/// A variable font's axes: (tag, name, min, default, max), empty for static fonts.
|
|
pub fn axes(&self, family: &str, style: &str) -> Vec<(String, String, f32, f32, f32)> {
|
|
let face = self.face(family, base_style(style));
|
|
let Some(f) = face.skrifa() else { return vec![] };
|
|
f.axes()
|
|
.iter()
|
|
.map(|a| {
|
|
let tag = String::from_utf8_lossy(&a.tag().to_be_bytes()).to_string();
|
|
let name = f.localized_strings(a.name_id()).english_or_first().map(|s| s.to_string()).unwrap_or_else(|| tag.clone());
|
|
(tag, name, a.min_value(), a.default_value(), a.max_value())
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// `Style {wght:650,wdth:90}`: the named style's font at those axis values, made on first use.
|
|
fn instance(&self, family: &str, style: &str) -> Option<Arc<FontFace>> {
|
|
let open = style.find('{')?;
|
|
let base = self.find(family, style[..open].trim())?;
|
|
if base.skrifa().is_none_or(|f| f.axes().is_empty()) {
|
|
return Some(base);
|
|
}
|
|
let mut coords = base.coords.clone();
|
|
for kv in style[open + 1..].trim_end_matches('}').split(',') {
|
|
let (k, v) = kv.split_once(':')?;
|
|
let k = k.trim().as_bytes();
|
|
let v: f32 = v.trim().parse().ok()?;
|
|
if k.len() != 4 {
|
|
return None;
|
|
}
|
|
let tag = [k[0], k[1], k[2], k[3]];
|
|
match coords.iter_mut().find(|(t, _)| *t == tag) {
|
|
Some(c) => c.1 = v,
|
|
None => coords.push((tag, v)),
|
|
}
|
|
}
|
|
let f = make_face(base.bytes.clone(), base.index, base.family.clone(), style.to_string(), coords)?;
|
|
let f = Arc::new(f);
|
|
self.faces.write().unwrap_or_else(|e| e.into_inner()).push(f.clone());
|
|
Some(f)
|
|
}
|
|
|
|
fn find(&self, family: &str, style: &str) -> Option<Arc<FontFace>> {
|
|
if style.contains('{') {
|
|
{
|
|
let faces = self.read_faces();
|
|
if let Some(f) = faces.iter().find(|f| f.family.eq_ignore_ascii_case(family) && f.style == style) {
|
|
return Some(f.clone());
|
|
}
|
|
}
|
|
return self.instance(family, style);
|
|
}
|
|
let faces = self.read_faces();
|
|
let cands: Vec<&Arc<FontFace>> = faces.iter().filter(|f| f.family.eq_ignore_ascii_case(family)).collect();
|
|
if cands.is_empty() {
|
|
return None;
|
|
}
|
|
let ns = norm(style);
|
|
if let Some(f) = cands.iter().find(|f| norm(&f.style) == ns) {
|
|
return Some((*f).clone());
|
|
}
|
|
let (tw, ti) = (style_weight(style), style_italic(style));
|
|
cands
|
|
.iter()
|
|
.min_by(|a, b| {
|
|
let sa = (a.weight - tw).abs() + if a.italic != ti { 1000.0 } else { 0.0 };
|
|
let sb = (b.weight - tw).abs() + if b.italic != ti { 1000.0 } else { 0.0 };
|
|
sa.total_cmp(&sb)
|
|
})
|
|
.map(|f| (*f).clone())
|
|
}
|
|
|
|
/// First face (fallback family first, then load order) that covers `c`; on native, system
|
|
/// fonts are cataloged and loaded lazily the first time no loaded face covers a character.
|
|
pub fn fallback_for(&self, c: char, exclude: u32) -> Option<Arc<FontFace>> {
|
|
if let Some(f) = self.loaded_fallback(c, exclude) {
|
|
return Some(f);
|
|
}
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
if self.system_fallback(c) {
|
|
return self.loaded_fallback(c, exclude);
|
|
}
|
|
None
|
|
}
|
|
|
|
fn loaded_fallback(&self, c: char, exclude: u32) -> Option<Arc<FontFace>> {
|
|
let faces = self.read_faces();
|
|
let mut order: Vec<&Arc<FontFace>> = faces.iter().filter(|f| f.id != exclude).collect();
|
|
order.sort_by_key(|f| (!f.family.eq_ignore_ascii_case(FALLBACK_FAMILY), f.italic, (f.weight - 400.0).abs() as i32));
|
|
order.into_iter().find(|f| f.covers(c)).cloned()
|
|
}
|
|
|
|
/// Load a system font covering `c` (preferred fallback families first, then any cataloged
|
|
/// file under 40 MB). Returns true if one was loaded. Misses are remembered.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub(crate) fn system_fallback(&self, c: char) -> bool {
|
|
if c.is_control() || c.is_whitespace() {
|
|
return false;
|
|
}
|
|
{
|
|
let sys = self.sys.lock().unwrap_or_else(|e| e.into_inner());
|
|
if !sys.enabled || sys.misses.contains(&c) {
|
|
return false;
|
|
}
|
|
}
|
|
let covered = |db: &FontDb| db.read_faces().iter().any(|f| f.covers(c));
|
|
let cataloged: Vec<String> = self.read_catalog().iter().map(|e| e.family.clone()).collect();
|
|
for fam in SYSTEM_FALLBACKS {
|
|
if !self.is_loaded(fam) && cataloged.iter().any(|f| f.eq_ignore_ascii_case(fam)) {
|
|
self.load_cataloged(fam);
|
|
if covered(self) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
let mut paths: Vec<std::path::PathBuf> = self.catalog.read().unwrap_or_else(|e| e.into_inner()).iter().map(|e| e.path.clone()).collect();
|
|
paths.sort();
|
|
paths.dedup();
|
|
for p in paths {
|
|
if std::fs::metadata(&p).map(|m| m.len() > 40 << 20).unwrap_or(true) {
|
|
continue;
|
|
}
|
|
let Ok(data) = std::fs::read(&p) else { continue };
|
|
let hit =
|
|
enumerate_faces(&data).iter().any(|(i, _, _, _)| skrifa::FontRef::from_index(&data, *i).is_ok_and(|f| f.charmap().map(c).is_some()));
|
|
if hit && self.add_font(data) > 0 && covered(self) {
|
|
return true;
|
|
}
|
|
}
|
|
self.sys.lock().unwrap_or_else(|e| e.into_inner()).misses.insert(c);
|
|
false
|
|
}
|
|
|
|
/// A loaded colour face drawing all of `cluster`, loading an installed colour font that covers
|
|
/// `key` if none is loaded yet. Misses are remembered.
|
|
pub(crate) fn color_face_for(&self, key: char, cluster: &[char]) -> Option<Arc<FontFace>> {
|
|
let pick = |db: &FontDb| {
|
|
let faces = db.read_faces();
|
|
let mut v: Vec<&Arc<FontFace>> = faces.iter().filter(|f| crate::covers_cluster(f, cluster) && f.is_color()).collect();
|
|
v.sort_by_key(|f| (f.italic, (f.weight - 400.0).abs() as i32));
|
|
v.first().map(|f| (*f).clone())
|
|
};
|
|
if let Some(f) = pick(self) {
|
|
return Some(f);
|
|
}
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
if self.system_color_fallback(key) {
|
|
return pick(self);
|
|
}
|
|
let _ = key;
|
|
None
|
|
}
|
|
|
|
/// Load an installed colour font covering `c`. True if one was loaded.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn system_color_fallback(&self, c: char) -> bool {
|
|
static MISSES: Mutex<Option<std::collections::HashSet<char>>> = Mutex::new(None);
|
|
if c.is_control() || c.is_whitespace() {
|
|
return false;
|
|
}
|
|
{
|
|
let sys = self.sys.lock().unwrap_or_else(|e| e.into_inner());
|
|
if !sys.enabled {
|
|
return false;
|
|
}
|
|
}
|
|
if MISSES.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(Default::default).contains(&c) {
|
|
return false;
|
|
}
|
|
let mut paths: Vec<std::path::PathBuf> = self.read_catalog().iter().map(|e| e.path.clone()).collect();
|
|
paths.sort();
|
|
paths.dedup();
|
|
for p in paths {
|
|
if std::fs::metadata(&p).map(|m| m.len() > 64 << 20).unwrap_or(true) {
|
|
continue;
|
|
}
|
|
let Ok(data) = std::fs::read(&p) else { continue };
|
|
let hit = enumerate_faces(&data).iter().any(|(i, _, _, _)| {
|
|
has_color_tables(&data, *i) && skrifa::FontRef::from_index(&data, *i).is_ok_and(|f| f.charmap().map(c).is_some())
|
|
});
|
|
if hit && self.add_font(data) > 0 {
|
|
return true;
|
|
}
|
|
}
|
|
MISSES.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(Default::default).insert(c);
|
|
false
|
|
}
|
|
|
|
/// Glyph outline in font units, y-down (flipped), cached per (face, glyph).
|
|
pub fn outline(&self, face: &FontFace, gid: u32) -> Arc<BezPath> {
|
|
let key = (face.id, gid);
|
|
if let Some(p) = self.outlines.lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
|
|
return p.clone();
|
|
}
|
|
let mut pen = FlipPen(BezPath::new());
|
|
if let Some(f) = face.skrifa()
|
|
&& let Some(g) = f.outline_glyphs().get(GlyphId::new(gid))
|
|
{
|
|
let _ = g.draw(DrawSettings::unhinted(Size::unscaled(), face.location()), &mut pen);
|
|
}
|
|
let p = Arc::new(pen.0);
|
|
let mut cache = self.outlines.lock().unwrap_or_else(|e| e.into_inner());
|
|
if cache.len() >= OUTLINE_CACHE_MAX {
|
|
cache.clear();
|
|
}
|
|
cache.insert(key, p.clone());
|
|
p
|
|
}
|
|
}
|
|
|
|
struct FlipPen(BezPath);
|
|
|
|
impl OutlinePen for FlipPen {
|
|
fn move_to(&mut self, x: f32, y: f32) {
|
|
self.0.move_to((x as f64, -y as f64));
|
|
}
|
|
fn line_to(&mut self, x: f32, y: f32) {
|
|
self.0.line_to((x as f64, -y as f64));
|
|
}
|
|
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
|
|
self.0.quad_to((cx0 as f64, -cy0 as f64), (x as f64, -y as f64));
|
|
}
|
|
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
|
|
self.0.curve_to((cx0 as f64, -cy0 as f64), (cx1 as f64, -cy1 as f64), (x as f64, -y as f64));
|
|
}
|
|
fn close(&mut self) {
|
|
self.0.close_path();
|
|
}
|
|
}
|