fix(render): the CFF subfonts read once are kept, and a program with no usable font dict renders as if absent (hayro-interpret patch 93)

- CffFontBlob::new moves the subfonts it validated into the yoke instead
  of reading every font dict a second time.
- a_program_with_no_usable_font_dict_draws_no_square now checks that the
  page renders exactly as it does with no embedded program; the old
  inequality passed with the empty-dict check removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
This commit is contained in:
mtlcl410andClaude Opus 5.5 committed 2026-11-07 18:55:11 +08:00
1 parent ef37d634b2
commit 22d211fa4a
2 files changed
+10 -7

No files matched your search

@@ -100,8 +100,10 @@ fn cid_cff(first_has_private: bool) -> Vec<u8> {
out
}
/// A page showing the hex string `codes` at 60 pt in an Identity-H Type0 font over `program`.
/// A page showing the hex string `codes` at 60 pt in an Identity-H Type0 font over `program`
/// (no `/FontFile3` when `program` is empty).
fn page(program: &[u8], codes: &str) -> Vec<u8> {
let font_file = if program.is_empty() { "" } else { " /FontFile3 8 0 R" };
let body = format!("BT /F1 60 Tf 20 30 Td <{codes}> Tj ET");
let mut stream = format!("<< /Subtype /CIDFontType0C /Length {} >>\nstream\n", program.len()).into_bytes();
stream.extend_from_slice(program);
@@ -113,7 +115,7 @@ fn page(program: &[u8], codes: &str) -> Vec<u8> {
format!("<< /Length {} >>\nstream\n{body}\nendstream", body.len()).into_bytes(),
b"<< /Type /Font /Subtype /Type0 /BaseFont /ABCDEF+Synthetic /Encoding /Identity-H /DescendantFonts [6 0 R] >>".to_vec(),
b"<< /Type /Font /Subtype /CIDFontType0 /BaseFont /ABCDEF+Synthetic /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor 7 0 R /DW 1000 >>".to_vec(),
b"<< /Type /FontDescriptor /FontName /ABCDEF+Synthetic /Flags 4 /FontBBox [0 0 1000 1000] /ItalicAngle 0 /Ascent 1000 /Descent 0 /CapHeight 1000 /StemV 80 /FontFile3 8 0 R >>".to_vec(),
format!("<< /Type /FontDescriptor /FontName /ABCDEF+Synthetic /Flags 4 /FontBBox [0 0 1000 1000] /ItalicAngle 0 /Ascent 1000 /Descent 0 /CapHeight 1000 /StemV 80{font_file} >>").into_bytes(),
stream,
];
@@ -160,4 +162,6 @@ fn a_program_with_no_usable_font_dict_draws_no_square() {
let rgba = pixels(page(&cid_cff(false), "00010002"));
let square = pixels(page(&cid_cff(true), "0001"));
assert!(rgba != square, "a font dict without a Private DICT drew its glyph");
// The page renders exactly as it does with no embedded program at all.
assert!(rgba == pixels(page(&[], "00010002")), "the program was kept although no font dict is usable");
}
+4 -5
View File
@@ -77,14 +77,13 @@ impl CffFontBlob {
return None;
}
let yoke = Yoke::<CFFYoke<'static>, FontData>::attach_to_cart(data.clone(), |data| {
// PdfCraft patch (93): the subfonts read above own their data, so they move into the
// yoke instead of being read again.
let yoke = Yoke::<CFFYoke<'static>, FontData>::attach_to_cart(data.clone(), move |data| {
let bytes = data.as_ref();
let font = CffFontRef::new(bytes, 0, None).unwrap();
let cff = Cff::read(ReadFontData::new(bytes)).unwrap();
let charset = font.charset();
let subfonts = (0..font.num_subfonts())
.map(|index| font.subfont(index, &[]).ok())
.collect();
CFFYoke {
font,
cff,
@@ -93,7 +92,7 @@ impl CffFontBlob {
}
});
let _ = (cff, charset, subfonts);
let _ = (cff, charset);
Some(Self(Arc::new(yoke), Arc::new(OnceLock::new())))
}