M7: Type1 fonts with an OpenType-wrapped CFF program draw their own face

A Type1 font whose FontFile3 has subtype /OpenType (ISO 32000-2 §9.9)
keeps its CFF program in the `CFF ` table of an OpenType wrapper.
CffFontBlob::new rejected the wrapper, so the font fell back to the
standard-font stand-in and the text drew in the wrong face and weight.
The CFF table is now taken from the wrapper, as composite fonts already
do (hayro-interpret patch (69)).

The test wraps the renderer's bundled CFF program in a one-table OTTO
directory at run time (no font file added) and requires the wrapped and
bare programs to give identical pixels; it fails with the fix reverted.
A wrapper without a `CFF ` table still draws, with the stand-in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
This commit is contained in:
KenM76
2026-10-28 11:37:10 -04:00
co-authored by Claude Opus 5.5
parent 20eebc1c33
commit aad8a3841c
5 changed files with 101 additions and 2 deletions
+1
View File
@@ -5,6 +5,7 @@ mod inline_image_ei;
mod jbig2_invariants;
mod luminosity_group_space;
mod named_form_properties;
mod open_type_cff_simple_font;
mod search;
mod separation_all;
mod substitute_widths;
@@ -0,0 +1,85 @@
//! A `Type1` font whose `FontFile3` has subtype `OpenType` (ISO 32000-2 §9.9) keeps its CFF
//! program inside an OpenType table directory; it is drawn with that program, not with a
//! standard-font stand-in (hayro-interpret patch (69)).
//!
//! The program is the renderer's own bundled fixed-pitch CFF face, so no font file is added.
//! Drawn right, the bare program and the same program in an OpenType wrapper give the same
//! pixels; the stand-in a serif font falls back to is a different face.
use pdfcraft_render::{PageRenderer, RenderConfig, RenderRequest};
use std::sync::Arc;
const CFF: &[u8] = include_bytes!("../../../../vendor/hayro-interpret/assets/FoxitFixed.pfb");
/// `cff` in the smallest OpenType wrapper: an `OTTO` table directory with one `CFF ` table.
fn open_type(cff: &[u8]) -> Vec<u8> {
let mut out = b"OTTO".to_vec();
out.extend_from_slice(&[0, 1, 0, 16, 0, 0, 0, 0]);
out.extend_from_slice(b"CFF ");
out.extend_from_slice(&0u32.to_be_bytes());
out.extend_from_slice(&28u32.to_be_bytes());
out.extend_from_slice(&u32::try_from(cff.len()).unwrap().to_be_bytes());
out.extend_from_slice(cff);
out
}
/// A page showing "AgW" at 60 pt in a serif `Type1` font whose `FontFile3` is `program`.
fn page(program: &[u8], subtype: &str) -> Vec<u8> {
let body = "BT /F1 60 Tf 10 30 Td (AgW) Tj ET";
let mut objects: Vec<Vec<u8>> = vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 120] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>".to_vec(),
format!("<< /Length {} >>\nstream\n{body}\nendstream", body.len()).into_bytes(),
b"<< /Type /Font /Subtype /Type1 /BaseFont /Synthetic-Semibold /Encoding /WinAnsiEncoding /FirstChar 65 /LastChar 103 /Widths [600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600] /FontDescriptor 6 0 R >>".to_vec(),
b"<< /Type /FontDescriptor /FontName /Synthetic-Semibold /Flags 34 /FontBBox [0 -200 1000 900] /ItalicAngle 0 /Ascent 800 /Descent -200 /CapHeight 700 /StemV 120 /FontFile3 7 0 R >>".to_vec(),
];
let mut stream = format!("<< /Subtype /{subtype} /Length {} >>\nstream\n", program.len()).into_bytes();
stream.extend_from_slice(program);
stream.extend_from_slice(b"\nendstream");
objects.push(stream);
let mut bytes = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new();
for (i, object) in objects.iter().enumerate() {
offsets.push(bytes.len());
bytes.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
bytes.extend_from_slice(object);
bytes.extend_from_slice(b"\nendobj\n");
}
let xref = bytes.len();
bytes.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).as_bytes());
for offset in offsets {
bytes.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
}
bytes.extend_from_slice(format!("trailer << /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).as_bytes());
bytes
}
fn pixels(bytes: Vec<u8>) -> Vec<u8> {
let mut renderer = PageRenderer::new(Arc::new(bytes), RenderConfig::default());
let image = renderer.render(RenderRequest { page: 0, scale: 1.0, ..Default::default() });
assert!(image.error.is_none(), "{:?}", image.error);
image.rgba.to_vec()
}
fn inked(rgba: &[u8]) -> usize {
rgba.chunks(4).filter(|p| p.first().is_some_and(|r| *r < 128)).count()
}
#[test]
fn an_open_type_wrapped_cff_program_draws_like_the_bare_program() {
let bare = pixels(page(CFF, "Type1C"));
assert!(inked(&bare) > 200, "the bare CFF program drew no text");
let wrapped = pixels(page(&open_type(CFF), "OpenType"));
assert!(wrapped == bare, "the OpenType-wrapped program drew a different face: the font fell back");
}
/// A wrapper with no `CFF ` table still draws, with the stand-in, rather than failing the page.
#[test]
fn a_wrapper_without_a_cff_table_falls_back() {
let mut broken = open_type(CFF);
broken.get_mut(12..16).unwrap().copy_from_slice(b"glyf");
let rgba = pixels(page(&broken, "OpenType"));
assert!(inked(&rgba) > 200, "the stand-in drew no text");
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+7
View File
@@ -170,6 +170,13 @@ impl CffFontBlob {
self.font().num_glyphs()
}
/// PdfCraft patch (69): the `CFF ` table of an OpenType font, for a `FontFile3` of
/// subtype `OpenType` (ISO 32000-2 §9.9) under a simple font.
pub(crate) fn from_open_type(data: &[u8]) -> Option<Self> {
let cff = FontRef::new(data).ok()?.cff().ok()?;
Self::new(Arc::new(cff.offset_data().as_ref().to_vec()))
}
pub(crate) fn is_cid(&self) -> bool {
self.font().is_cid()
}
+7 -1
View File
@@ -282,7 +282,13 @@ impl CffKind {
fn new(dict: &Dict<'_>) -> Option<Self> {
let descriptor = dict.get::<Dict<'_>>(FONT_DESC)?;
let data = descriptor.get::<Stream<'_>>(FONT_FILE3)?;
let font = CffFontBlob::new(Arc::new(data.decoded().ok()?.to_vec()))?;
let bytes = data.decoded().ok()?.to_vec();
// PdfCraft patch (69): an OpenType-wrapped program carries its CFF in a table.
let font = if bytes.starts_with(b"OTTO") {
CffFontBlob::from_open_type(&bytes)?
} else {
CffFontBlob::new(Arc::new(bytes))?
};
let (encoding, encodings) = read_encoding(dict);
let (widths, missing_width) = read_widths(dict, &descriptor)?;