mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 13:54:09 +00:00
- 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
114 lines
5.7 KiB
Rust
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");
|
|
}
|