From 22d211fa4af60ac4998765f4cf6eb9bdd34d1555 Mon Sep 17 00:00:00 2001 From: mtlcl410 Date: Sat, 7 Nov 2026 18:55:11 +0800 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA --- crates/render/tests/it/cid_cff_unusable_font_dict.rs | 8 ++++++-- vendor/hayro-interpret/src/font/blob.rs | 9 ++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/render/tests/it/cid_cff_unusable_font_dict.rs b/crates/render/tests/it/cid_cff_unusable_font_dict.rs index 05e7b0a796..9b40b5277c 100644 --- a/crates/render/tests/it/cid_cff_unusable_font_dict.rs +++ b/crates/render/tests/it/cid_cff_unusable_font_dict.rs @@ -100,8 +100,10 @@ fn cid_cff(first_has_private: bool) -> Vec { 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 { + 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 { 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"); } diff --git a/vendor/hayro-interpret/src/font/blob.rs b/vendor/hayro-interpret/src/font/blob.rs index 53131c1f12..879050a5b3 100644 --- a/vendor/hayro-interpret/src/font/blob.rs +++ b/vendor/hayro-interpret/src/font/blob.rs @@ -77,14 +77,13 @@ impl CffFontBlob { return None; } - let yoke = Yoke::, 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::, 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()))) }