Merge mac-font-activation (update): faces without drawable outlines aren't listed; packaged fonts keep a font extension (#443)

This commit is contained in:
aiongg
2026-10-12 12:41:20 +00:00
4 changed files with 88 additions and 3 deletions
+53 -1
View File
@@ -319,7 +319,7 @@ fn copy_font(root: &OutputRoot, face: &FontFace, dir: &Path, copied: &mut Vec<Pa
return String::new();
}
let name = safe_component(&from.file_name().map(|n| n.to_string_lossy()).unwrap_or_default(), "font");
let to = dir.join(unique_name(name, taken));
let to = dir.join(unique_name(font_file_name(name, face.data()), taken));
match root.validate_directory(dir).and_then(|_| root.copy(from, &to)) {
Ok(()) => {
copied.push(from.clone());
@@ -330,6 +330,24 @@ fn copy_font(root: &OutputRoot, face: &FontFace, dir: &Path, copied: &mut Vec<Pa
}
}
/// `name` with a font extension when it has none: font services name their files as they like,
/// and a package's `Document Fonts` load only .ttf, .otf, .ttc and .otc files. The extension
/// comes from the file's first four bytes; a leading dot (a hidden file) is dropped.
fn font_file_name(name: String, data: &[u8]) -> String {
let ext = name.rsplit_once('.').map(|(_, e)| e.to_ascii_lowercase());
if matches!(ext.as_deref(), Some("ttf" | "otf" | "ttc" | "otc")) {
return name;
}
let add = match data.get(..4) {
Some(b"OTTO") => "otf",
Some(b"ttcf") => "ttc",
Some([0, 1, 0, 0] | b"true") => "ttf",
_ => return name,
};
let stem = name.trim_start_matches('.');
format!("{}.{add}", if stem.is_empty() { "font" } else { stem })
}
/// Add the faces that draw the visible glyphs of `cs` (table cells and footnotes included) to
/// `out`, each once.
fn drawn_faces(cs: &ComposedStory, depth: usize, out: &mut Vec<FaceRef>) {
@@ -645,6 +663,40 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn packaged_fonts_keep_a_font_extension() {
use designcraft_fonts::DOCUMENT_FONTS_FOLDER;
use designcraft_fonts::testing::{font_with, installed_face};
// A font service's file without an extension is copied under a name the reopened
// package loads.
let dir = std::env::temp_dir().join(format!("dc-package-font-ext-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let from = dir.join(".12345");
let bytes = font_with("DocFont Package Extensionless", &['E']).unwrap();
std::fs::write(&from, &bytes).unwrap();
let face = installed_face(bytes, from).unwrap();
let root = OutputRoot::new(dir.clone()).unwrap();
let fonts = dir.join("Pkg").join(DOCUMENT_FONTS_FOLDER);
std::fs::create_dir_all(&fonts).unwrap();
let (mut copied, mut taken, mut files) = (Vec::new(), Vec::new(), Vec::new());
assert_eq!(copy_font(&root, &face, &fonts, &mut copied, &mut taken, &mut files), "");
assert_eq!(files, [fonts.join("12345.ttf")]);
let db = designcraft_fonts::FontDb::global();
let loaded = db.load_document_fonts(&fonts);
assert_eq!((loaded.faces, loaded.skipped.len()), (1, 0), "{:?}", loaded.skipped);
db.close_scope(loaded.scope);
// The first four bytes name the kind; a font extension or an unknown kind stays.
assert_eq!(font_file_name("Face".into(), b"OTTO...."), "Face.otf");
assert_eq!(font_file_name("Faces".into(), b"ttcf...."), "Faces.ttc");
assert_eq!(font_file_name("Mac".into(), b"true...."), "Mac.ttf");
assert_eq!(font_file_name("..".into(), &[0, 1, 0, 0]), "font.ttf");
assert_eq!(font_file_name("Face.OTF".into(), b"ttcf"), "Face.OTF");
assert_eq!(font_file_name("Face.dfont".into(), b"\0\0\x01\0"), "Face.dfont");
assert_eq!(font_file_name("short".into(), b"OT"), "short");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn package_uses_portable_names_inside_the_selected_directory() {
assert_eq!(safe_component("Archive/Annual: Review", "Untitled"), "Annual_ Review");
+10 -2
View File
@@ -600,11 +600,15 @@ fn face_names(f: &skrifa::FontRef<'_>) -> Option<(String, String)> {
Some((family, style))
}
/// The tables glyph outlines are drawn from.
#[cfg(not(target_arch = "wasm32"))]
const OUTLINE_TABLES: [&[u8; 4]; 4] = [b"glyf", b"CFF ", b"CFF2", b"VARC"];
/// The names and font menu group of every face in the font file at `path`, reading only its table
/// directories and the tables that name and classify it (`name`, `OS/2`, `meta`; `cmap` only for a
/// font that declares no language or code page): 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.
/// appear once it loads. Faces without glyph outlines (none of [`OUTLINE_TABLES`]) are left out.
#[cfg(not(target_arch = "wasm32"))]
fn file_face_names(path: &std::path::Path) -> Vec<ScannedFace> {
use std::io::{Read, Seek, SeekFrom};
@@ -635,6 +639,10 @@ fn file_face_names(path: &std::path::Path) -> Vec<ScannedFace> {
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)?;
// Faces drawn from other tables (Apple's `hvgl`, bitmaps) would draw nothing.
if !records.as_chunks::<16>().0.iter().any(|r| OUTLINE_TABLES.iter().any(|t| r.starts_with(*t))) {
return None;
}
// Table `tag`, unless missing or over `max` bytes.
let mut table = |tag: &[u8; 4], max: u32| -> Option<([u8; 4], Vec<u8>)> {
let rec = records.as_chunks::<16>().0.iter().find(|r| r.starts_with(tag))?;
@@ -1072,7 +1080,7 @@ impl FontDb {
}
/// The faces of a font file from `source` whose family and style aren't loaded yet.
fn new_faces(&self, bytes: Vec<u8>, source: FontSource) -> Vec<FontFace> {
pub(crate) fn new_faces(&self, bytes: Vec<u8>, source: FontSource) -> Vec<FontFace> {
let data = Arc::new(bytes);
enumerate_faces(&data)
.into_iter()
+6
View File
@@ -292,3 +292,9 @@ pub fn with_meta(font: &[u8], dlng: Option<&str>, slng: Option<&str>) -> Option<
maps.iter().for_each(|(_, v)| meta.extend_from_slice(v.as_bytes()));
with_tables(font, vec![(*b"meta", meta)])
}
/// The first face of the font `bytes` as if installed from `path` (which isn't read).
#[cfg(not(target_arch = "wasm32"))]
pub fn installed_face(bytes: Vec<u8>, path: std::path::PathBuf) -> Option<crate::FontFace> {
crate::FontDb::with_font_dirs(vec![]).new_faces(bytes, crate::FontSource::Installed(path)).into_iter().next()
}
+19
View File
@@ -243,6 +243,25 @@ fn the_scan_reads_names_without_loading_the_font() {
assert!(file_face_names(&dir.join("missing.ttf")).is_empty());
}
#[test]
fn faces_without_outlines_are_not_cataloged() {
// A face whose glyphs live in a table nothing draws from (`hvgl`, say) lists no face; in a
// collection, its drawable sibling is still listed.
let regular = renamed("SourceSans3-Regular.ttf");
let mut hvgl = renamed("SourceSans3-It.ttf");
let tables = u16::from_be_bytes([hvgl[4], hvgl[5]]) as usize;
let glyf = (0..tables).map(|r| 12 + r * 16).find(|&at| &hvgl[at..at + 4] == b"glyf").unwrap();
hvgl[glyf..glyf + 4].copy_from_slice(b"hvgl");
let dir = std::env::temp_dir().join(format!("dc-sysfonts-outlines-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("hvgl.ttf"), &hvgl).unwrap();
std::fs::write(dir.join("mixed.ttc"), collection(&[hvgl, regular])).unwrap();
assert!(file_face_names(&dir.join("hvgl.ttf")).is_empty());
let names: Vec<String> = file_face_names(&dir.join("mixed.ttc")).into_iter().map(|f| f.style).collect();
assert_eq!(names, ["Regular"]);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn font_menus_group_installed_and_loaded_fonts_without_loading_them() {
use crate::testing::{font_with, with_code_pages, with_names};