mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-02 22:50:27 -06:00
Merge mac-font-activation (update): faces without drawable outlines aren't listed; packaged fonts keep a font extension (#443)
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user