M10: a swapped post 2.0 table's glyph names serve /Differences (#2946)

Review follow-up on #2949.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
This commit is contained in:
mtlcl410
2026-10-22 08:40:08 +08:00
co-authored by Claude Opus 5.5
parent b67a8bf268
commit 09668ce885
3 changed files with 30 additions and 3 deletions
+8 -2
View File
@@ -4708,7 +4708,7 @@ trailer << /Root 1 0 R >>
.find(|e| &e[..4] == b"post")
.map(|e| u32::from_be_bytes([e[8], e[9], e[10], e[11]]) as usize)
.expect("the test font has a post table");
let render = |version: [u8; 4]| -> Vec<u8> {
let render_with = |version: [u8; 4], encoding: &str| -> Vec<u8> {
let mut program = font.to_vec();
program[post..post + 4].copy_from_slice(&version);
let content = b"BT /F1 28 Tf 10 40 Td (Hamburg 0123) Tj ET";
@@ -4718,7 +4718,7 @@ trailer << /Root 1 0 R >>
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 80] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>".to_vec(),
[format!("<< /Length {} >>\nstream\n", content.len()).into_bytes(), content.to_vec(), b"\nendstream".to_vec()].concat(),
format!("<< /Type /Font /Subtype /TrueType /BaseFont /JetBrainsMono-Regular /FirstChar 32 /LastChar 126 /Widths [{widths}] /FontDescriptor 6 0 R /Encoding /WinAnsiEncoding >>").into_bytes(),
format!("<< /Type /Font /Subtype /TrueType /BaseFont /JetBrainsMono-Regular /FirstChar 32 /LastChar 126 /Widths [{widths}] /FontDescriptor 6 0 R /Encoding {encoding} >>").into_bytes(),
b"<< /Type /FontDescriptor /FontName /JetBrainsMono-Regular /Flags 32 /FontBBox [-1000 -300 1000 1000] /ItalicAngle 0 /Ascent 1020 /Descent -300 /CapHeight 730 /StemV 80 /FontFile2 7 0 R >>".to_vec(),
[format!("<< /Length {} >>\nstream\n", program.len()).into_bytes(), program, b"\nendstream".to_vec()].concat(),
];
@@ -4747,10 +4747,16 @@ trailer << /Root 1 0 R >>
assert!(page.error.is_none(), "{:?}", page.error);
page.rgba.to_vec()
};
let render = |version: [u8; 4]| render_with(version, "/WinAnsiEncoding");
let correct = render([0, 3, 0, 0]);
assert_eq!(render([0, 0, 0, 3]), correct, "3.0 with swapped halves draws with the embedded font");
assert_eq!(render([0, 0, 0, 2]), render([0, 2, 0, 0]), "2.0 with swapped halves too");
assert_ne!(render([0, 9, 0, 0]), correct, "an unknown version is still refused");
// A /Differences name only the font's own post table knows (W's alternate, `W.cv07`)
// is found through a swapped 2.0 table too (review on #2949).
let alternate = |version: [u8; 4]| render_with(version, "<< /BaseEncoding /WinAnsiEncoding /Differences [72 /W.cv07] >>");
assert_ne!(alternate([0, 2, 0, 0]), render([0, 2, 0, 0]), "the alternate W draws instead of H");
assert_eq!(alternate([0, 0, 0, 2]), alternate([0, 2, 0, 0]), "and also with swapped halves");
}
/// A page whose content expands far beyond its compressed stream stops at the expansion budget,
+1 -1
View File
File diff suppressed because one or more lines are too long
+21
View File
@@ -256,6 +256,27 @@ impl OpenTypeFontBlob {
return glyph_names;
};
// PdfCraft patch: a version written with its halves swapped (`00 00 00 02`, accepted in
// `new`) is read as the version it means, from a copy of the table with the version
// written correctly: read-fonts reads a 2.0 table's name index only under version 2.0,
// so otherwise its glyph names were never found (#2946).
let repaired = match post.version().to_be_bytes() {
[0, 0, 0, n @ 1..=3] => self
.font_ref()
.table_data(skrifa::raw::types::Tag::new(b"post"))
.map(|data| {
let mut bytes = data.as_bytes().to_vec();
if let Some(version) = bytes.get_mut(..4) {
version.copy_from_slice(&[0, n, 0, 0]);
}
bytes
}),
_ => None,
};
let repaired_post = repaired.as_deref().and_then(|b| {
skrifa::raw::tables::post::Post::read(ReadFontData::new(b)).ok()
});
let post = repaired_post.as_ref().unwrap_or(&post);
match post.version().to_major_minor() {
(1, 0) => {
for (gid, name) in DEFAULT_GLYPH_NAMES.iter().enumerate() {