mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-28 20:37:10 -06:00
#3716: a font file's faces are capped before they are built
Finding 59: enumerate_faces took a collection's member count straight from its header (a TTC can list one table directory millions of times) and font_faces read every fvar named instance (up to 65535, with a linear style dedupe and a name-table lookup each). Every entry became a FontFace kept for the session, and add_fonts then checked each new face against every face in the database. Now a file yields at most 256 faces (MAX_FILE_FACES; user fonts ask for one more so they can still be refused as "more than 256 faces"), named instances are read to the same cap, a face with the family and style of an earlier one in the file is skipped (repeated collection offsets), and add_fonts dedupes against an indexed set. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6c6326486f
commit
ce7e4ea28e
+99
-10
@@ -825,7 +825,8 @@ fn font_faces(f: &skrifa::FontRef<'_>, index: u32) -> Vec<Found> {
|
||||
let east_asian = east_asian_font(f);
|
||||
let axes = f.axes();
|
||||
let mut named: Vec<Found> = Vec::new();
|
||||
for ni in f.named_instances().iter() {
|
||||
// At most one more than a file may add, so a caller can tell there are more (#3716).
|
||||
for ni in f.named_instances().iter().take(MAX_FILE_FACES + 1) {
|
||||
let Some(style) = name(f, &[ni.subfamily_name_id()]) else { continue };
|
||||
if named.iter().any(|n| n.style.eq_ignore_ascii_case(&style)) {
|
||||
continue;
|
||||
@@ -854,15 +855,25 @@ fn font_faces(f: &skrifa::FontRef<'_>, index: u32) -> Vec<Found> {
|
||||
}]
|
||||
}
|
||||
|
||||
/// Parse every face in `data` (a font file or collection); a variable font yields one face per
|
||||
/// named instance.
|
||||
fn enumerate_faces(data: &[u8]) -> Vec<Found> {
|
||||
/// Most faces one font file adds (collection members, variable fonts' named instances).
|
||||
const MAX_FILE_FACES: usize = 256;
|
||||
|
||||
/// Parse the faces in `data` (a font file or collection), at most `limit` of them; a variable
|
||||
/// font yields one face per named instance. A face with the family and style of an earlier one
|
||||
/// is left out (a collection can list one font many times, #3716).
|
||||
fn enumerate_faces(data: &[u8], limit: usize) -> Vec<Found> {
|
||||
let count = match FileRef::new(data) {
|
||||
Ok(FileRef::Font(_)) => 1,
|
||||
Ok(FileRef::Collection(c)) => c.len(),
|
||||
Ok(FileRef::Collection(c)) => c.len().min(u32::try_from(limit).unwrap_or(u32::MAX)),
|
||||
Err(_) => 0,
|
||||
};
|
||||
(0..count).filter_map(|i| skrifa::FontRef::from_index(data, i).ok().map(|f| font_faces(&f, i))).flatten().collect()
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
(0..count)
|
||||
.filter_map(|i| skrifa::FontRef::from_index(data, i).ok().map(|f| font_faces(&f, i)))
|
||||
.flatten()
|
||||
.filter(|f| seen.insert((f.family.to_ascii_lowercase(), f.style.to_ascii_lowercase())))
|
||||
.take(limit)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The compiled-in Source Sans 3 Regular, for a database that somehow has no faces at all.
|
||||
@@ -986,7 +997,7 @@ impl FontDb {
|
||||
let others = crate::CRAFT_FONTS.iter().filter(|f| !f.scripts.contains(&"Jpan"));
|
||||
let craft = crate::japanese_document_fonts().into_iter().chain(others).map(|f| f.bytes);
|
||||
for data in BUNDLED.iter().copied().chain(craft) {
|
||||
for f in enumerate_faces(data) {
|
||||
for f in enumerate_faces(data, MAX_FILE_FACES) {
|
||||
if let Some(mut face) = make_face(FontBytes::Static(data), f.index, f.family, f.style, f.legacy, f.coords) {
|
||||
face.local_names = f.local;
|
||||
faces.push(Arc::new(face));
|
||||
@@ -1173,7 +1184,7 @@ impl FontDb {
|
||||
for (bytes, name) in files {
|
||||
let data = Arc::new(bytes);
|
||||
let name: Option<String> = name.map(|n| n.trim().chars().take(MAX_FAMILY_NAME).collect()).filter(|n: &String| !n.is_empty());
|
||||
for f in enumerate_faces(&data) {
|
||||
for f in enumerate_faces(&data, MAX_FILE_FACES) {
|
||||
if let Some(mut face) = make_face(FontBytes::Owned(data.clone()), f.index, f.family, f.style, f.legacy, f.coords) {
|
||||
face.local_names = f.local;
|
||||
if let Some(n) = name.as_deref().filter(|n| !face.named(n) && !face.local_names.iter().any(|l| l.name.eq_ignore_ascii_case(n))) {
|
||||
@@ -1185,9 +1196,12 @@ impl FontDb {
|
||||
}
|
||||
}
|
||||
let mut faces = self.faces.write().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
// Indexed: a document's fonts may add hundreds of faces to thousands (#3716).
|
||||
let key = |f: &FontFace| (f.family.to_ascii_lowercase(), f.style.to_ascii_lowercase());
|
||||
let mut present: std::collections::HashSet<_> = faces.iter().map(|g| key(g)).collect();
|
||||
let mut added = 0;
|
||||
for f in new {
|
||||
if !faces.iter().any(|g| g.family.eq_ignore_ascii_case(&f.family) && g.style.eq_ignore_ascii_case(&f.style)) {
|
||||
if present.insert(key(&f)) {
|
||||
faces.push(f);
|
||||
added += 1;
|
||||
}
|
||||
@@ -1387,7 +1401,9 @@ impl FontDb {
|
||||
(best.path.clone(), best.family.clone(), best.style.clone())
|
||||
};
|
||||
let data = Arc::new(read_fallback_file(&path)?);
|
||||
let found = enumerate_faces(&data).into_iter().find(|f| f.family.eq_ignore_ascii_case(&fam) && f.style.eq_ignore_ascii_case(&style))?;
|
||||
let found = enumerate_faces(&data, MAX_FILE_FACES)
|
||||
.into_iter()
|
||||
.find(|f| f.family.eq_ignore_ascii_case(&fam) && f.style.eq_ignore_ascii_case(&style))?;
|
||||
let mut face = make_face(FontBytes::Owned(data), found.index, found.family, found.style, found.legacy, found.coords)?;
|
||||
face.local_names = found.local;
|
||||
Some(Arc::new(face))
|
||||
@@ -2127,4 +2143,77 @@ mod tests {
|
||||
assert!(!db.has_family("NotAFont-Bold"));
|
||||
assert_eq!(db.face("Source Serif 4", "Regular").style, "Regular");
|
||||
}
|
||||
|
||||
/// A variable font holding just `name` and `fvar` tables: family "Var Test", one `wght` axis
|
||||
/// and `instances` named instances, each with its own style name.
|
||||
fn variable_font(instances: u16) -> Vec<u8> {
|
||||
let mut names: Vec<(u16, String)> = vec![(1, "Var Test".into()), (2, "Regular".into())];
|
||||
names.extend((0..instances).map(|i| (256 + i, format!("Style {i}"))));
|
||||
let mut name = vec![0, 0];
|
||||
name.extend_from_slice(&(names.len() as u16).to_be_bytes());
|
||||
name.extend_from_slice(&(6 + 12 * names.len() as u16).to_be_bytes());
|
||||
let mut strings = Vec::new();
|
||||
for (id, text) in &names {
|
||||
let utf16: Vec<u8> = text.encode_utf16().flat_map(u16::to_be_bytes).collect();
|
||||
for v in [3, 1, 0x409, *id, utf16.len() as u16, strings.len() as u16] {
|
||||
name.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
strings.extend(utf16);
|
||||
}
|
||||
name.extend(strings);
|
||||
let mut fvar = Vec::new();
|
||||
for v in [1u16, 0, 16, 2, 1, 20, instances, 8] {
|
||||
fvar.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
fvar.extend_from_slice(b"wght");
|
||||
for v in [100u32 << 16, 400 << 16, 900 << 16] {
|
||||
fvar.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
fvar.extend_from_slice(&[0, 0, 0, 2]); // flags, axisNameId
|
||||
for i in 0..instances {
|
||||
fvar.extend_from_slice(&(256 + i).to_be_bytes());
|
||||
fvar.extend_from_slice(&[0, 0]);
|
||||
fvar.extend_from_slice(&(400u32 << 16).to_be_bytes());
|
||||
}
|
||||
mini_font(&[(*b"fvar", fvar), (*b"name", name)])
|
||||
}
|
||||
|
||||
/// A collection listing `font` `members` times.
|
||||
fn collection_of(font: &[u8], members: u32) -> Vec<u8> {
|
||||
let start = 12 + 4 * members;
|
||||
let mut out = b"ttcf\0\x01\0\0".to_vec();
|
||||
out.extend_from_slice(&members.to_be_bytes());
|
||||
for _ in 0..members {
|
||||
out.extend_from_slice(&start.to_be_bytes());
|
||||
}
|
||||
let mut font = font.to_vec();
|
||||
let tables = u16::from_be_bytes([font[4], font[5]]) as usize;
|
||||
for i in 0..tables {
|
||||
let at = 12 + 16 * i + 8;
|
||||
let offset = u32::from_be_bytes(font[at..at + 4].try_into().unwrap()) + start;
|
||||
font[at..at + 4].copy_from_slice(&offset.to_be_bytes());
|
||||
}
|
||||
out.extend(font);
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_instances_and_collection_members_are_capped() {
|
||||
// #3716: a file's faces are capped before they are built, however the file multiplies them.
|
||||
let font = variable_font(300);
|
||||
let faces = font_faces(&skrifa::FontRef::new(&font).unwrap(), 0);
|
||||
assert_eq!(faces.len(), MAX_FILE_FACES + 1, "one more than the cap, so a caller can tell");
|
||||
assert_eq!(faces[0].style, "Style 0");
|
||||
assert_eq!(enumerate_faces(&font, MAX_FILE_FACES).len(), MAX_FILE_FACES);
|
||||
// A million members, all the same font: read once, and no more than the cap.
|
||||
let started = std::time::Instant::now();
|
||||
let ttc = collection_of(BUNDLED[0], 1_000_000);
|
||||
let found = enumerate_faces(&ttc, MAX_FILE_FACES);
|
||||
assert_eq!(found.iter().map(|f| (f.family.as_str(), f.style.as_str())).collect::<Vec<_>>(), [(FALLBACK_FAMILY, "Regular")]);
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
assert_eq!(db.add_font(collection_of(&font, 1_000_000)), MAX_FILE_FACES);
|
||||
// A font the user adds is refused instead.
|
||||
assert!(matches!(db.install_font("v.ttf", &variable_font(300)), Err(user::UserFontError::Unusable(e)) if e.contains("more than 256")));
|
||||
assert!(started.elapsed() < std::time::Duration::from_secs(2), "{:?}", started.elapsed());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,7 +403,7 @@ impl FontDb {
|
||||
if FileRef::new(&sfnt).is_err() {
|
||||
return Err(UserFontError::Unusable("not a valid OpenType file".into()));
|
||||
}
|
||||
let found = enumerate_faces(&sfnt);
|
||||
let found = enumerate_faces(&sfnt, MAX_FACES_PER_FILE + 1);
|
||||
if found.is_empty() {
|
||||
return Err(UserFontError::Unusable("no face with a family name".into()));
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ fn the_scan_lists_a_variable_fonts_named_instances_as_loading_it_does() {
|
||||
return;
|
||||
};
|
||||
let data = std::fs::read(path).unwrap();
|
||||
let loaded: Vec<(String, String, String)> = enumerate_faces(&data).into_iter().map(|f| (f.family, f.style, f.legacy)).collect();
|
||||
let loaded: Vec<(String, String, String)> = enumerate_faces(&data, MAX_FILE_FACES).into_iter().map(|f| (f.family, f.style, f.legacy)).collect();
|
||||
assert_eq!(names(path), loaded);
|
||||
if loaded.len() > 1 {
|
||||
// Instances other than Regular, Bold and Italic have names of their own.
|
||||
|
||||
Reference in New Issue
Block a user