1039 lines
40 KiB
Rust
1039 lines
40 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 (the UI sans).
|
|
pub const FALLBACK_FAMILY: &str = "Source Sans 3";
|
|
|
|
/// The bundled fonts (OFL), as font file bytes. Japanese fonts are not bundled: they come from
|
|
/// the optional craft-fonts build input ([`crate::japanese_document_fonts`]).
|
|
pub fn bundled() -> &'static [&'static [u8]] {
|
|
BUNDLED
|
|
}
|
|
|
|
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/SourceSerif4-It.ttf"),
|
|
include_bytes!("../../../assets/fonts/SourceSerif4-Semibold.ttf"),
|
|
include_bytes!("../../../assets/fonts/SourceSerif4-Bold.ttf"),
|
|
include_bytes!("../../../assets/fonts/SourceSerif4-BoldIt.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"),
|
|
];
|
|
|
|
#[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,
|
|
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]>>,
|
|
}
|
|
|
|
/// 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()
|
|
}
|
|
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)
|
|
}
|
|
|
|
/// GDEF identifies marks even when shaping merges their source cluster with a base.
|
|
pub fn glyph_is_mark(&self, gid: u32) -> bool {
|
|
use skrifa::raw::TableProvider;
|
|
self.skrifa()
|
|
.and_then(|f| f.gdef().ok())
|
|
.and_then(|gdef| gdef.glyph_class_def()?.ok())
|
|
.is_some_and(|classes| classes.get(GlyphId::new(gid)) == 3)
|
|
}
|
|
}
|
|
|
|
#[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>,
|
|
/// CJK chain families already looked up in the catalog (loaded, or not installed).
|
|
chain_tried: std::collections::HashSet<&'static str>,
|
|
}
|
|
|
|
/// 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",
|
|
];
|
|
|
|
/// CJK fallback chains: the families tried first (user-added, else installed) for characters of
|
|
/// each language that the primary font lacks. Each lists macOS, then Windows, then Linux /
|
|
/// cross-platform families, serif first; a platform only has its own installed. Japanese tries
|
|
/// the craft-fonts Japanese faces before these ([`japanese_chain`]).
|
|
const JAPANESE_FALLBACKS: &[&str] = &[
|
|
"Hiragino Mincho ProN",
|
|
"YuMincho",
|
|
"Hiragino Sans",
|
|
"Yu Mincho",
|
|
"MS Mincho",
|
|
"Yu Gothic",
|
|
"Noto Serif CJK JP",
|
|
"Source Han Serif JP",
|
|
"Noto Serif JP",
|
|
"Noto Sans CJK JP",
|
|
"Source Han Sans JP",
|
|
];
|
|
const SIMPLIFIED_CHINESE_FALLBACKS: &[&str] = &[
|
|
"Songti SC",
|
|
"STSong",
|
|
"PingFang SC",
|
|
"SimSun",
|
|
"Microsoft YaHei",
|
|
"Noto Serif CJK SC",
|
|
"Source Han Serif SC",
|
|
"Noto Serif SC",
|
|
"Noto Sans CJK SC",
|
|
"Source Han Sans SC",
|
|
];
|
|
const TRADITIONAL_CHINESE_FALLBACKS: &[&str] = &[
|
|
"Songti TC",
|
|
"PingFang TC",
|
|
"PMingLiU",
|
|
"MingLiU",
|
|
"Microsoft JhengHei",
|
|
"Noto Serif CJK TC",
|
|
"Source Han Serif TC",
|
|
"Noto Serif TC",
|
|
"Noto Sans CJK TC",
|
|
"Source Han Sans TC",
|
|
];
|
|
const KOREAN_FALLBACKS: &[&str] = &[
|
|
"AppleMyungjo",
|
|
"Apple SD Gothic Neo",
|
|
"Batang",
|
|
"Malgun Gothic",
|
|
"Noto Serif CJK KR",
|
|
"Source Han Serif K",
|
|
"Noto Serif KR",
|
|
"Noto Sans CJK KR",
|
|
"Source Han Sans K",
|
|
];
|
|
|
|
/// The Japanese fallback chain: the craft-fonts Japanese faces' families in document order
|
|
/// (Mincho first; none without craft-fonts), then [`JAPANESE_FALLBACKS`].
|
|
fn japanese_chain() -> &'static [&'static str] {
|
|
static CHAIN: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
|
|
CHAIN.get_or_init(|| {
|
|
let mut chain: Vec<&'static str> = Vec::new();
|
|
for f in crate::japanese_document_fonts() {
|
|
if !chain.contains(&f.family) {
|
|
chain.push(f.family);
|
|
}
|
|
}
|
|
chain.extend_from_slice(JAPANESE_FALLBACKS);
|
|
chain
|
|
})
|
|
}
|
|
|
|
/// The CJK fallback chain for `c` in `language` (a BCP 47 tag). The script decides where it is
|
|
/// unambiguous (Hangul is Korean, kana Japanese, Bopomofo Traditional Chinese); ideographs and CJK
|
|
/// punctuation follow the language, and have no chain without a CJK one.
|
|
fn cjk_chain(c: char, language: Option<&str>) -> Option<&'static [&'static str]> {
|
|
match c as u32 {
|
|
0x1100..=0x11FF | 0x3130..=0x318F | 0xA960..=0xA97F | 0xAC00..=0xD7FF | 0xFFA0..=0xFFDC => return Some(KOREAN_FALLBACKS),
|
|
0x3040..=0x30FF | 0x31F0..=0x31FF | 0xFF66..=0xFF9F | 0x1B000..=0x1B16F => return Some(japanese_chain()),
|
|
0x3100..=0x312F | 0x31A0..=0x31BF => return Some(TRADITIONAL_CHINESE_FALLBACKS),
|
|
// Radicals, CJK symbols and punctuation, kanbun, strokes, enclosed and compatibility
|
|
// characters, ideographs, compatibility and vertical forms, full-width forms.
|
|
0x2E80..=0x2FDF
|
|
| 0x3000..=0x303F
|
|
| 0x3190..=0x319F
|
|
| 0x31C0..=0x31EF
|
|
| 0x3200..=0x33FF
|
|
| 0x3400..=0x4DBF
|
|
| 0x4E00..=0x9FFF
|
|
| 0xF900..=0xFAFF
|
|
| 0xFE10..=0xFE1F
|
|
| 0xFE30..=0xFE4F
|
|
| 0xFF00..=0xFFEF
|
|
| 0x20000..=0x3FFFF => {}
|
|
_ => return None,
|
|
}
|
|
match language? {
|
|
"ja" => Some(japanese_chain()),
|
|
"ko" => Some(KOREAN_FALLBACKS),
|
|
"zh" | "zh-Hans" => Some(SIMPLIFIED_CHINESE_FALLBACKS),
|
|
"zh-Hant" => Some(TRADITIONAL_CHINESE_FALLBACKS),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
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 Source Sans 3 Regular, for a database that somehow has no 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(BUNDLED[0]), 0, FALLBACK_FAMILY.into(), "Regular".into(), Vec::new())
|
|
.expect("the compiled-in Source Sans 3 Regular parses");
|
|
Arc::new(face)
|
|
})
|
|
.clone()
|
|
}
|
|
|
|
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),
|
|
shaper,
|
|
coords,
|
|
location,
|
|
instance,
|
|
bytes,
|
|
index,
|
|
bmp: 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();
|
|
let craft = crate::japanese_document_fonts().into_iter().map(|f| f.bytes);
|
|
for data in BUNDLED.iter().copied().chain(craft) {
|
|
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()))
|
|
}
|
|
|
|
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
|
|
/// Source Sans 3 Regular. 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.find(family, style) {
|
|
return f;
|
|
}
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
if self.load_cataloged(family)
|
|
&& let Some(f) = self.find(family, style)
|
|
{
|
|
return f;
|
|
}
|
|
self.find(FALLBACK_FAMILY, style)
|
|
.or_else(|| self.find(FALLBACK_FAMILY, "Regular"))
|
|
.or_else(|| self.read_faces().first().cloned())
|
|
.unwrap_or_else(last_resort_face)
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
|
|
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())
|
|
}
|
|
|
|
/// A face other than `exclude` that covers `c` in `language` (a BCP 47 tag, `None` when
|
|
/// unknown). CJK characters try their language's chain first (see [`cjk_chain`]); then the
|
|
/// 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, language: Option<&str>) -> Option<Arc<FontFace>> {
|
|
if let Some(chain) = cjk_chain(c, language)
|
|
&& let Some(f) = chain.iter().find_map(|family| self.chain_face(family, c, exclude))
|
|
{
|
|
return Some(f);
|
|
}
|
|
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
|
|
}
|
|
|
|
/// The face of `family` (regular first) that covers `c`, loading the installed family the
|
|
/// first time a chain asks for it (native, while the system fallback is on).
|
|
fn chain_face(&self, family: &'static str, c: char, exclude: u32) -> Option<Arc<FontFace>> {
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
if !self.is_loaded(family) {
|
|
// Held while loading, so a parallel lookup waits for the font instead of passing it by.
|
|
let mut sys = self.sys.lock().unwrap_or_else(|e| e.into_inner());
|
|
if sys.enabled && sys.chain_tried.insert(family) {
|
|
self.load_cataloged(family);
|
|
}
|
|
}
|
|
let faces = self.read_faces();
|
|
let mut order: Vec<&Arc<FontFace>> = faces.iter().filter(|f| f.id != exclude && f.family.eq_ignore_ascii_case(family)).collect();
|
|
order.sort_by_key(|f| (f.italic, (f.weight - 400.0).abs() as i32));
|
|
order.into_iter().find(|f| f.covers(c)).cloned()
|
|
}
|
|
|
|
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"))]
|
|
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
|
|
}
|
|
|
|
/// 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();
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
#[path = "tests_sysfonts.rs"]
|
|
mod tests_sysfonts;
|
|
|
|
#[cfg(test)]
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
#[path = "tests_fallback.rs"]
|
|
mod tests_fallback;
|