M7: the editor reads no metrics for a composite font it can't adapt

The Resources review of 8eead8ecd:

- `embedded_sfnt_within` read a composite font's metrics within the
  editor's allowance before `composite_glyph_maps` refused any `/Encoding`
  but Identity-H, so an Identity-V or UniJIS font spent its ToUnicode and
  `/CIDToGIDMap` for nothing. Its `/Encoding` is now checked first; a
  program with a cmap of its own is kept as it is, as when the rebuild
  finds nothing.
- The Thai edit's allowance, `MAX_THAI_PROGRAMS`, now also pays for the
  CMaps and glyph maps its subsets' metrics and codes are read from: its
  doc and its refusal say "font data", not "font programs".
- `a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names`
  also reads its font, whose `post` table names its glyphs, within an
  allowance: the program is paid for by the glyph-name reading, by the
  Mac OS Roman one and for the widths, and a byte short of the first the
  names go unread.

Tests: `a_composite_font_that_cant_be_adapted_spends_only_its_program`,
and `adapting_a_programs_cmap_is_paid_for` again expects a composite font
whose codes aren't Identity-H to pay for its program only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
Grzegorz MartensandClaude Opus 5.5 committed 2026-11-09 13:36:44 +01:00
1 parent 063f8629e4
commit 509cf9f9fd
3 files changed
+47 -6

No files matched your search

+4 -3
View File
@@ -116,8 +116,9 @@ struct LegacyFace {
space: f64,
}
/// What one edit may decode, and so keep, of the font programs of its THSarabunPSK subsets
/// (each at most 16 MiB): a page may name many of them.
/// What one edit may decode of its THSarabunPSK subsets' font data: their programs (each at most
/// 16 MiB), which it keeps, and the CMaps and glyph maps their metrics and codes are read from. A
/// page may name many of them.
const MAX_THAI_PROGRAMS: usize = 64 << 20;
struct PreparedCharacters {
@@ -926,7 +927,7 @@ fn legacy_thai_font(
// A subset left unread would count as unusable, silently changing which subset draws the text.
if budget.remaining() == 0 {
return Err(EditError::Invalid(format!(
"the page's {} subsets need more than {} MiB of font programs for one edit; choose a replacement font for this text",
"the page's {} subsets need more than {} MiB of font data for one edit; choose a replacement font for this text",
source.base_font.rsplit('+').next().unwrap_or(&source.base_font),
MAX_THAI_PROGRAMS >> 20
)));
+9
View File
@@ -135,6 +135,15 @@ pub fn embedded_sfnt_within(doc: &Document, font: &Dict, budget: &mut DecodeBudg
if own_cmap && !base_font.rsplit('+').next().is_some_and(|f| f.starts_with("THSarabunPSK")) {
return Some(bytes);
}
// A composite font's cmap is rebuilt only through Identity-H codes (`composite_glyph_maps`):
// with any other `/Encoding` nothing is, so its metrics aren't read, and a program with a
// cmap of its own is kept as it is, as when the rebuild finds nothing.
if font.name(b"Subtype") == Some(b"Type0") && font.name(b"Encoding") != Some(b"Identity-H") {
if own_cmap {
return Some(bytes);
}
continue;
}
// PDF subsets may remove the Unicode cmap entirely. Rebuild an editor-only
// cmap from ToUnicode and the actual CID-to-glyph map; source bytes stay unchanged.
// Metrics read past the allowance, its widths' read of the program again included,
+34 -3
View File
@@ -243,13 +243,13 @@ fn adapting_a_programs_cmap_is_paid_for() {
assert_eq!(skrifa::FontRef::new(&editor).unwrap().charmap().map('Ω'), Some(expected));
assert_eq!(budget.remaining(), 0);
assert!(embedded_sfnt_within(&doc, &source, &mut DecodeBudget::new(cost - 1, 0)).is_none());
// A composite font's metrics are read too, and paid for: the program and its ToUnicode, which
// give no glyph here (its codes aren't Identity-H).
// A composite font whose codes aren't Identity-H can't be adapted, so its metrics aren't
// read: the program is all it pays for.
let mut budget = DecodeBudget::new(cost, 0);
let mut composite = source.clone();
composite.set(b"Subtype".to_vec(), Object::name("Type0"));
assert!(embedded_sfnt_within(&doc, &composite, &mut budget).is_none());
assert_eq!(budget.remaining(), cost - subset.len() - cmap.len());
assert_eq!(budget.remaining(), cost - subset.len());
}
/// A Type 0 subset font whose `/Encoding`, program, `/CIDToGIDMap` and ToUnicode are streams, and
@@ -721,6 +721,21 @@ fn a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names() {
],
);
assert_eq!(Metrics::from_dict(&doc, &f).decode(&codes), "");
// The names are read within the allowance the metrics are read within: the program is paid
// for by the glyph-name reading, by the Mac OS Roman one (which finds the glyphs named and
// reads nothing) and for the widths. A byte short of the first, the names go unread.
let mut doc = Document::new_empty();
let program = doc.add(Object::Stream(Stream::flate(Dict::new(), &subset)));
let mut descriptor = Dict::new();
descriptor.set(b"Flags".to_vec(), Object::Int(4));
descriptor.set(b"FontFile2".to_vec(), Object::Ref(program));
let f = font(&mut doc, vec![("Subtype", Object::name("TrueType")), ("FontDescriptor", Object::Dict(descriptor))]);
let mut budget = DecodeBudget::new(3 * subset.len(), 0);
assert_eq!(Metrics::from_dict_within(&doc, &f, &mut budget), symbolic);
assert_eq!(budget.remaining(), 0);
let short = Metrics::from_dict_within(&doc, &f, &mut DecodeBudget::new(subset.len() - 1, 0));
assert!(short.past_allowance);
assert_ne!(short.decode(&codes), TEXT, "the names unread");
}
/// The glyph list specification's `uni` and `u` forms spell their hex digits in uppercase
@@ -1984,6 +1999,22 @@ fn adapting_a_composite_subsets_cmap_is_paid_for() {
assert!(embedded_sfnt_within(&doc, &source, &mut DecodeBudget::new(subset.len() + cmap - 1, 0)).is_none());
}
/// A composite font whose `/Encoding` isn't Identity-H can't have its cmap rebuilt
/// (`composite_glyph_maps` reads only Identity-H codes), so the editor reads its program and no
/// more: its ToUnicode and `/CIDToGIDMap` are left for what is left of the allowance.
#[test]
fn a_composite_font_that_cant_be_adapted_spends_only_its_program() {
let mut doc = Document::new_empty();
let (source, _, subset, _, [cmap, map]) = cmapless_composite(&mut doc);
for encoding in ["Identity-V", "UniJIS-UCS2-H"] {
let mut f = source.clone();
f.set(b"Encoding".to_vec(), Object::name(encoding));
let mut budget = DecodeBudget::new(subset.len() + cmap + map, 0);
assert!(embedded_sfnt_within(&doc, &f, &mut budget).is_none(), "{encoding}");
assert_eq!(budget.remaining(), cmap + map, "{encoding}");
}
}
/// The `/CIDToGIDMap` a composite subset's cmap is rebuilt from reads as the font's other streams
/// do: a full map, which compresses well, reads behind ASCIIHex and Flate, though the hex stage's
/// output comes on top of the map.