M2: a non-embedded SymbolMT draws Symbol's glyphs, whatever encoding it declares (hayro-interpret patch 98)

A non-embedded TrueType `SymbolMT` (Symbol's TrueType name) with
/WinAnsiEncoding and the nonsymbolic flag was not recognised as the
standard Symbol font and fell back to Helvetica, so code 0x6D drew "m"
where PDFium, MuPDF and Poppler draw "mu". `SymbolMT`, and `Symbol` or
`SymbolMT` with a `,`/`-` style suffix, now select Symbol, which draws
through its built-in encoding like `/Symbol` already does.

ISO 32000-2 §9.6.2.2, §9.6.5.1. PDFBOX-5844 (13069708-exportByExcel.pdf).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
This commit is contained in:
mtlcl410
2026-11-07 17:11:10 +08:00
co-authored by Claude Opus 5.5
parent f42fda7342
commit c420a0edf8
4 changed files with 82 additions and 1 deletions
+1
View File
@@ -14,5 +14,6 @@ mod separation_all;
mod stray_q_in_a_form;
mod stroke_degenerate_cubic;
mod substitute_widths;
mod symbol_mt_builtin_encoding;
mod tiling_cell_placement;
mod type3_text_clip;
@@ -0,0 +1,73 @@
//! A non-embedded font named `SymbolMT` (the name Symbol has as a TrueType font) or a style
//! variant such as `Symbol,Bold` is the standard Symbol font, and a non-embedded Symbol draws
//! through its built-in encoding (ISO 32000-2 §9.6.2.2), even when the font dictionary
//! declares `/WinAnsiEncoding` and the nonsymbolic flag: code 0x6D (`m`) is `mu`, as for
//! `/Symbol` itself (hayro-interpret patch (98)).
use pdfcraft_render::{PageRenderer, RenderConfig, RenderRequest};
use std::sync::Arc;
/// A page showing "abmp" at 40 pt in a non-embedded font `font` (the inside of its dictionary).
fn page(font: &str) -> Vec<u8> {
let body = "BT /F1 40 Tf 10 30 Td (abmp) Tj ET";
let 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 200 100] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>".to_vec(),
format!("<< /Length {} >>\nstream\n{body}\nendstream", body.len()).into_bytes(),
format!("<< /Type /Font {font} >>").into_bytes(),
];
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()
}
/// A TrueType `name` with WinAnsiEncoding, nonsymbolic flags and no font program, as written
/// by spreadsheet exports.
fn win_ansi_true_type(name: &str) -> String {
format!(
"/Subtype /TrueType /BaseFont /{name} /Encoding /WinAnsiEncoding /FontDescriptor << /Type /FontDescriptor /FontName /{name} /Flags 32 /FontBBox [0 -216 1113 693] /ItalicAngle 0 /Ascent 1005 /Descent -216 /CapHeight 693 /StemV 60 >>"
)
}
#[test]
fn symbol_mt_draws_symbols_through_the_built_in_encoding() {
let symbol = pixels(page("/Subtype /Type1 /BaseFont /Symbol"));
let latin = pixels(page("/Subtype /Type1 /BaseFont /Helvetica"));
assert!(symbol != latin, "the Symbol stand-in drew the Latin letters");
for name in ["SymbolMT", "ABCDEF+SymbolMT", "SymbolMT,Bold", "Symbol,Italic", "Symbol-Bold"] {
assert!(pixels(page(&win_ansi_true_type(name))) == symbol, "{name} did not draw as Symbol");
}
let type1 = "/Subtype /Type1 /BaseFont /SymbolMT /Encoding /WinAnsiEncoding";
assert!(pixels(page(type1)) == symbol, "a Type1 SymbolMT did not draw as Symbol");
}
/// Only the Symbol name itself counts: a font whose name merely starts with it keeps its
/// declared encoding and stand-in.
#[test]
fn a_name_that_only_starts_with_symbol_is_not_symbol() {
let symbol = pixels(page("/Subtype /Type1 /BaseFont /Symbol"));
for name in ["Symbolic", "SymbolPS"] {
assert!(pixels(page(&win_ansi_true_type(name))) != symbol, "{name} drew as Symbol");
}
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+7
View File
@@ -233,6 +233,13 @@ pub(crate) fn select_standard_font(
_ => {}
}
// PdfCraft patch (98): `SymbolMT` (the name Symbol has as a TrueType font) and the style
// variants `Symbol,Bold`, `SymbolMT,Italic` or `Symbol-Bold` are the Symbol font, drawn with
// its built-in encoding like `Symbol` itself, whatever `/Encoding` and flags they declare.
if matches!(name.split([',', '-']).next(), Some("Symbol" | "SymbolMT")) {
return Some((StandardFont::Symbol, true));
}
// Now, we bruteforce, trying to determine a suitable font based on the
// keywords that appear in the name.
let lower = name.to_ascii_lowercase();