Files
pdfcraft/crates/render/tests/it/symbol_mt_builtin_encoding.rs
T
mtlcl410andClaude Opus 5.5 cd9b6b8a0f fix(render): only Symbol's style variants are Symbol, and SymbolMT keeps its differences and embedded program (hayro-interpret patch 98)
- is_symbol_name: after `,` or `-` only a style name (SYMBOL_STYLES) keeps
  the Symbol font, so `Symbol-Helvetica` keeps its declared encoding and
  Latin stand-in.
- Tests: the negative names draw the same letters as Helvetica; a SymbolMT
  with /Differences [109 /alpha] draws alpha; an embedded SymbolMT draws
  its own program.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-07 19:01:05 +08:00

114 lines
5.7 KiB
Rust

//! 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> {
page_with(font, None)
}
/// [`page`] with `program` as object 6, a `/FontFile2` stream the font descriptor can point at.
fn page_with(font: &str, program: Option<&[u8]>) -> Vec<u8> {
let body = "BT /F1 40 Tf 10 30 Td (abmp) 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 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(),
];
if let Some(program) = program {
objects.push([format!("<< /Length {} >>\nstream\n", program.len()).into_bytes(), program.to_vec(), b"\nendstream".to_vec()].concat());
}
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");
}
/// The number of dark pixels on a page.
fn inked(rgba: &[u8]) -> usize {
rgba.chunks(4).filter(|p| p.first().is_some_and(|r| *r < 128)).count()
}
/// Only the Symbol name itself, alone or with a style, counts: a font whose name merely starts
/// with it, or names another font after the separator, keeps its declared encoding and its
/// Latin stand-in, drawing the same letters as Helvetica.
#[test]
fn a_name_that_only_starts_with_symbol_is_not_symbol() {
let latin = pixels(page("/Subtype /Type1 /BaseFont /Helvetica"));
assert!(inked(&latin) > 100, "the Latin reference drew no text");
for name in ["Symbolic", "SymbolPS", "Symbol-Helvetica", "SymbolMT,Helvetica"] {
assert!(pixels(page(&win_ansi_true_type(name))) == latin, "{name} did not draw the Latin letters");
}
}
/// A `/Differences` array still applies to a SymbolMT: code 0x6D named `alpha` draws alpha, as
/// for `/Symbol` with the same differences, not the built-in encoding's `mu`.
#[test]
fn symbol_mt_applies_its_differences() {
let symbol = pixels(page("/Subtype /Type1 /BaseFont /Symbol"));
let alpha = pixels(page("/Subtype /Type1 /BaseFont /Symbol /Encoding << /Differences [109 /alpha] >>"));
assert!(alpha != symbol, "the differences changed nothing");
let font = win_ansi_true_type("SymbolMT")
.replace("/Encoding /WinAnsiEncoding", "/Encoding << /BaseEncoding /WinAnsiEncoding /Differences [109 /alpha] >>");
assert!(pixels(page(&font)) == alpha, "SymbolMT did not draw the alpha its differences name");
}
/// An embedded font named SymbolMT draws its own program, not the Symbol stand-in.
#[test]
fn an_embedded_symbol_mt_draws_its_own_program() {
let program = include_bytes!("../../../../assets/fonts/JetBrainsMono-Regular.ttf");
let embedded = |name: &str| {
let font = win_ansi_true_type(name).replace(" >>", " /FontFile2 6 0 R >>");
pixels(page_with(&font, Some(program)))
};
let own = embedded("ABCDEF+JetBrainsMono");
assert!(inked(&own) > 100, "the embedded program drew no text");
assert!(embedded("ABCDEF+SymbolMT") == own, "an embedded SymbolMT did not draw its own glyphs");
}