M7: PDF font tables move to the fonts crate with Unicode decoding/encoding (ToUnicode, WinAnsi/MacRoman/Standard, Differences glyph names, subset glyph checks)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-03 00:42:57 -04:00
parent 2548228b1d
commit 5a70838884
7 changed files with 846 additions and 2 deletions

1
Cargo.lock generated
View File

@ -3487,6 +3487,7 @@ dependencies = [
name = "printcraft-fonts"
version = "0.1.0"
dependencies = [
"printcraft-cos",
"skrifa 0.44.0",
]

View File

@ -9,5 +9,6 @@ license.workspace = true
workspace = true
[dependencies]
printcraft-cos = { workspace = true }
# Glyph outlines of the bundled script font (typed signatures); already in the tree via egui.
skrifa = "0.44"

View File

@ -0,0 +1,268 @@
//! Single-byte encodings (ISO 32000-2 Annex D) as code → Unicode tables (0 = undefined).
pub(crate) static WIN_ANSI: [u32; 256] = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79,
80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,
113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 0, 8364, 0, 8218, 402, 8222, 8230, 8224, 8225, 710, 8240, 352, 8249, 338,
0, 381, 0, 0, 8216, 8217, 8220, 8221, 8226, 8211, 8212, 732, 8482, 353, 8250, 339, 0, 382, 376, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169,
170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198,
199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227,
228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255,
];
pub(crate) static MAC_ROMAN: [u32; 256] = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79,
80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,
113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 0, 196, 197, 199, 201, 209, 214, 220, 225, 224, 226, 228, 227, 229, 231,
233, 232, 234, 235, 237, 236, 238, 239, 241, 243, 242, 244, 246, 245, 250, 249, 251, 252, 8224, 176, 162, 163, 167, 8226, 182, 223, 174, 169,
8482, 180, 168, 8800, 198, 216, 8734, 177, 8804, 8805, 165, 181, 8706, 8721, 8719, 960, 8747, 170, 186, 937, 230, 248, 191, 161, 172, 8730, 402,
8776, 8710, 171, 187, 8230, 160, 192, 195, 213, 338, 339, 8211, 8212, 8220, 8221, 8216, 8217, 247, 9674, 255, 376, 8260, 8364, 8249, 8250, 64257,
64258, 8225, 183, 8218, 8222, 8240, 194, 202, 193, 203, 200, 205, 206, 207, 204, 211, 212, 63743, 210, 218, 219, 217, 305, 710, 732, 175, 728,
729, 730, 184, 733, 731, 711,
];
pub(crate) static STANDARD: [u32; 256] = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 33, 34, 35, 36, 37, 38, 8217, 40, 41, 42, 43,
44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79,
80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 8216, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111,
112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 161, 162, 163, 8260, 165, 402, 167, 164, 39, 8220, 171, 8249, 8250, 64257, 64258, 0, 8211, 8224, 8225, 183, 0,
182, 8226, 8218, 8222, 8221, 187, 8230, 8240, 0, 191, 0, 96, 180, 710, 732, 175, 728, 729, 168, 0, 730, 184, 0, 733, 731, 711, 8212, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 198, 0, 170, 0, 0, 0, 0, 321, 216, 338, 186, 0, 0, 0, 0, 0, 230, 0, 0, 0, 305, 0, 0, 322, 248, 339, 223, 0,
0, 0, 0,
];
/// Glyph names of the Latin character set (Annex D) → Unicode.
pub(crate) static NAMES: &[(&str, u32)] = &[
("A", 0x41),
("AE", 0xc6),
("Aacute", 0xc1),
("Acircumflex", 0xc2),
("Adieresis", 0xc4),
("Agrave", 0xc0),
("Aring", 0xc5),
("Atilde", 0xc3),
("B", 0x42),
("C", 0x43),
("Ccedilla", 0xc7),
("D", 0x44),
("E", 0x45),
("Eacute", 0xc9),
("Ecircumflex", 0xca),
("Edieresis", 0xcb),
("Egrave", 0xc8),
("Eth", 0xd0),
("Euro", 0x20ac),
("F", 0x46),
("G", 0x47),
("H", 0x48),
("I", 0x49),
("Iacute", 0xcd),
("Icircumflex", 0xce),
("Idieresis", 0xcf),
("Igrave", 0xcc),
("J", 0x4a),
("K", 0x4b),
("L", 0x4c),
("Lslash", 0x141),
("M", 0x4d),
("N", 0x4e),
("Ntilde", 0xd1),
("O", 0x4f),
("OE", 0x152),
("Oacute", 0xd3),
("Ocircumflex", 0xd4),
("Odieresis", 0xd6),
("Ograve", 0xd2),
("Oslash", 0xd8),
("Otilde", 0xd5),
("P", 0x50),
("Q", 0x51),
("R", 0x52),
("S", 0x53),
("Scaron", 0x160),
("T", 0x54),
("Thorn", 0xde),
("U", 0x55),
("Uacute", 0xda),
("Ucircumflex", 0xdb),
("Udieresis", 0xdc),
("Ugrave", 0xd9),
("V", 0x56),
("W", 0x57),
("X", 0x58),
("Y", 0x59),
("Yacute", 0xdd),
("Ydieresis", 0x178),
("Z", 0x5a),
("Zcaron", 0x17d),
("a", 0x61),
("aacute", 0xe1),
("acircumflex", 0xe2),
("acute", 0xb4),
("adieresis", 0xe4),
("ae", 0xe6),
("agrave", 0xe0),
("ampersand", 0x26),
("aring", 0xe5),
("asciicircum", 0x5e),
("asciitilde", 0x7e),
("asterisk", 0x2a),
("at", 0x40),
("atilde", 0xe3),
("b", 0x62),
("backslash", 0x5c),
("bar", 0x7c),
("braceleft", 0x7b),
("braceright", 0x7d),
("bracketleft", 0x5b),
("bracketright", 0x5d),
("breve", 0x2d8),
("brokenbar", 0xa6),
("bullet", 0x2022),
("c", 0x63),
("caron", 0x2c7),
("ccedilla", 0xe7),
("cedilla", 0xb8),
("cent", 0xa2),
("circumflex", 0x2c6),
("colon", 0x3a),
("comma", 0x2c),
("copyright", 0xa9),
("currency", 0xa4),
("d", 0x64),
("dagger", 0x2020),
("daggerdbl", 0x2021),
("degree", 0xb0),
("dieresis", 0xa8),
("divide", 0xf7),
("dollar", 0x24),
("dotaccent", 0x2d9),
("dotlessi", 0x131),
("e", 0x65),
("eacute", 0xe9),
("ecircumflex", 0xea),
("edieresis", 0xeb),
("egrave", 0xe8),
("eight", 0x38),
("ellipsis", 0x2026),
("emdash", 0x2014),
("endash", 0x2013),
("equal", 0x3d),
("eth", 0xf0),
("exclam", 0x21),
("exclamdown", 0xa1),
("f", 0x66),
("fi", 0xfb01),
("five", 0x35),
("fl", 0xfb02),
("florin", 0x192),
("four", 0x34),
("fraction", 0x2044),
("g", 0x67),
("germandbls", 0xdf),
("grave", 0x60),
("greater", 0x3e),
("guillemotleft", 0xab),
("guillemotright", 0xbb),
("guilsinglleft", 0x2039),
("guilsinglright", 0x203a),
("h", 0x68),
("hungarumlaut", 0x2dd),
("hyphen", 0x2d),
("i", 0x69),
("iacute", 0xed),
("icircumflex", 0xee),
("idieresis", 0xef),
("igrave", 0xec),
("j", 0x6a),
("k", 0x6b),
("l", 0x6c),
("less", 0x3c),
("logicalnot", 0xac),
("lslash", 0x142),
("m", 0x6d),
("macron", 0xaf),
("minus", 0x2212),
("mu", 0xb5),
("multiply", 0xd7),
("n", 0x6e),
("nbspace", 0xa0),
("nine", 0x39),
("ntilde", 0xf1),
("numbersign", 0x23),
("o", 0x6f),
("oacute", 0xf3),
("ocircumflex", 0xf4),
("odieresis", 0xf6),
("oe", 0x153),
("ogonek", 0x2db),
("ograve", 0xf2),
("one", 0x31),
("onehalf", 0xbd),
("onequarter", 0xbc),
("onesuperior", 0xb9),
("ordfeminine", 0xaa),
("ordmasculine", 0xba),
("oslash", 0xf8),
("otilde", 0xf5),
("p", 0x70),
("paragraph", 0xb6),
("parenleft", 0x28),
("parenright", 0x29),
("percent", 0x25),
("period", 0x2e),
("periodcentered", 0xb7),
("perthousand", 0x2030),
("plus", 0x2b),
("plusminus", 0xb1),
("q", 0x71),
("question", 0x3f),
("questiondown", 0xbf),
("quotedbl", 0x22),
("quotedblbase", 0x201e),
("quotedblleft", 0x201c),
("quotedblright", 0x201d),
("quoteleft", 0x2018),
("quoteright", 0x2019),
("quotesinglbase", 0x201a),
("quotesingle", 0x27),
("r", 0x72),
("registered", 0xae),
("ring", 0x2da),
("s", 0x73),
("scaron", 0x161),
("section", 0xa7),
("semicolon", 0x3b),
("seven", 0x37),
("sfthyphen", 0xad),
("six", 0x36),
("slash", 0x2f),
("space", 0x20),
("sterling", 0xa3),
("t", 0x74),
("thorn", 0xfe),
("three", 0x33),
("threequarters", 0xbe),
("threesuperior", 0xb3),
("tilde", 0x2dc),
("trademark", 0x2122),
("two", 0x32),
("twosuperior", 0xb2),
("u", 0x75),
("uacute", 0xfa),
("ucircumflex", 0xfb),
("udieresis", 0xfc),
("ugrave", 0xf9),
("underscore", 0x5f),
("v", 0x76),
("w", 0x77),
("x", 0x78),
("y", 0x79),
("yacute", 0xfd),
("ydieresis", 0xff),
("yen", 0xa5),
("z", 0x7a),
("zcaron", 0x17e),
("zero", 0x30),
];

View File

@ -3,6 +3,8 @@
//! See the README: the metrics are approximations by character class (no vendor metrics files
//! are bundled). The full font subsystem lands in M2.2/M7.
mod encodings;
pub mod pdf;
mod script;
pub use script::{ScriptOutline, script_outline};

573
crates/fonts/src/pdf.rs Normal file
View File

@ -0,0 +1,573 @@
//! What content editing and redaction need from a PDF font: how to split a string into character
//! codes, each code's advance width, the glyph height (ascent, descent), and what each code means
//! (Unicode) — and back, which text the font can show. Widths come from the font dictionary
//! (`/Widths`, `/W`, `/DW`, `/MissingWidth`); the standard 14 fonts without widths use
//! approximations. Meanings come from `/ToUnicode`, else the encoding (`/Encoding` base and
//! `/Differences` glyph names, ISO 32000-2 Annex D).
use std::collections::HashMap;
use printcraft_cos::{Dict, Document, Object};
/// How the bytes of a string map to codes.
#[derive(Clone, Debug, PartialEq)]
enum Codes {
One,
Two,
/// Codespace ranges `(length, low, high)` from an embedded CMap.
Ranges(Vec<(usize, Vec<u8>, Vec<u8>)>),
}
#[derive(Clone, Debug, PartialEq)]
enum Std14 {
Helvetica,
Times,
Courier,
Symbolic,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Metrics {
codes: Codes,
/// Simple fonts: `/FirstChar` and `/Widths`.
first: u32,
widths: Vec<f64>,
/// Composite fonts: CID → width, and CID ranges with one width.
cid_widths: HashMap<u32, f64>,
cid_ranges: Vec<(u32, u32, f64)>,
/// Code → CID for embedded non-identity CMaps (`cidrange` / `cidchar`).
cid_map: Vec<(u32, u32, u32)>,
default: f64,
std14: Option<Std14>,
/// Glyph units → text space (0.001, or `/FontMatrix[0]` for Type 3).
pub scale: f64,
/// Glyph box in text space per unit of font size.
pub ascent: f64,
pub descent: f64,
pub composite: bool,
/// Code → Unicode text.
unicode: HashMap<u32, String>,
/// `/BaseFont`, and whether it is a subset (`ABCDEF+Name`: other glyphs are missing).
pub base_font: String,
pub subset: bool,
/// Bytes per code for writing new text (1 for simple fonts, else the codespace length).
code_len: usize,
}
fn nums(doc: &Document, o: Option<&Object>) -> Vec<f64> {
o.map(|o| doc.resolve(o))
.and_then(|a| a.as_array().map(|a| a.iter().map(|x| doc.resolve(x).as_f64().unwrap_or(0.0)).collect()))
.unwrap_or_default()
}
fn dict(doc: &Document, o: Option<&Object>) -> Option<Dict> {
o.and_then(|o| doc.resolve(o).as_dict().cloned())
}
fn hex_bytes(tok: &str) -> Option<Vec<u8>> {
let t = tok.trim_start_matches('<').trim_end_matches('>');
if !t.len().is_multiple_of(2) || t.is_empty() {
return None;
}
(0..t.len()).step_by(2).map(|i| u8::from_str_radix(&t[i..i + 2], 16).ok()).collect()
}
fn be(b: &[u8]) -> u32 {
b.iter().fold(0u32, |a, x| (a << 8) | u32::from(*x))
}
/// Codespace ranges and CID mappings from an embedded CMap stream.
#[allow(clippy::type_complexity)]
fn parse_cmap(data: &[u8]) -> (Vec<(usize, Vec<u8>, Vec<u8>)>, Vec<(u32, u32, u32)>) {
let text = String::from_utf8_lossy(data);
let toks: Vec<&str> = text.split_whitespace().collect();
let (mut spaces, mut cids) = (Vec::new(), Vec::new());
let mut i = 0;
while i < toks.len() {
match toks[i] {
"begincodespacerange" => {
i += 1;
while i + 1 < toks.len() && toks[i] != "endcodespacerange" {
if let (Some(lo), Some(hi)) = (hex_bytes(toks[i]), hex_bytes(toks[i + 1]))
&& lo.len() == hi.len()
{
spaces.push((lo.len(), lo, hi));
}
i += 2;
}
}
"begincidrange" => {
i += 1;
while i + 2 < toks.len() && toks[i] != "endcidrange" {
if let (Some(lo), Some(hi), Ok(c)) = (hex_bytes(toks[i]), hex_bytes(toks[i + 1]), toks[i + 2].parse::<u32>()) {
cids.push((be(&lo), be(&hi), c));
}
i += 3;
}
}
"begincidchar" => {
i += 1;
while i + 1 < toks.len() && toks[i] != "endcidchar" {
if let (Some(code), Ok(c)) = (hex_bytes(toks[i]), toks[i + 1].parse::<u32>()) {
cids.push((be(&code), be(&code), c));
}
i += 2;
}
}
_ => {}
}
i += 1;
}
spaces.sort_by_key(|s| s.0);
(spaces, cids)
}
impl Metrics {
/// Metrics for text without a usable font (Helvetica-like).
pub fn fallback() -> Self {
Metrics {
codes: Codes::One,
first: 0,
widths: Vec::new(),
cid_widths: HashMap::new(),
cid_ranges: Vec::new(),
cid_map: Vec::new(),
default: 500.0,
std14: Some(Std14::Helvetica),
scale: 0.001,
ascent: 0.9,
descent: -0.25,
composite: false,
unicode: (32..127u32).map(|c| (c, char::from_u32(c).expect("ascii").to_string())).collect(),
base_font: "Helvetica".into(),
subset: false,
code_len: 1,
}
}
pub fn from_dict(doc: &Document, font: &Dict) -> Self {
let mut m = Self::read_metrics(doc, font);
let base = String::from_utf8_lossy(font.name(b"BaseFont").unwrap_or(b"")).into_owned();
m.subset = base.len() > 7 && base.as_bytes()[6] == b'+' && base[..6].bytes().all(|b| b.is_ascii_uppercase());
m.base_font = base;
m.code_len = match &m.codes {
Codes::One => 1,
Codes::Two => 2,
Codes::Ranges(r) => r.last().map_or(2, |x| x.0),
};
m.unicode = unicode_map(doc, font, m.composite);
m
}
fn read_metrics(doc: &Document, font: &Dict) -> Self {
let mut m = Metrics::fallback();
m.std14 = None;
let subtype = font.name(b"Subtype").unwrap_or(b"Type1").to_vec();
let descriptor;
if subtype == b"Type0" {
m.composite = true;
m.default = 1000.0;
m.codes = Codes::Two;
if let Some(e) = font.get(b"Encoding").map(|e| doc.resolve(e))
&& let Object::Stream(s) = &*e
&& let Ok(data) = s.decoded()
{
let (spaces, cids) = parse_cmap(&data);
if !spaces.is_empty() {
m.codes = Codes::Ranges(spaces);
}
m.cid_map = cids;
}
let desc = font
.get(b"DescendantFonts")
.map(|d| doc.resolve(d))
.and_then(|a| a.as_array().and_then(|a| a.first().cloned()))
.and_then(|d| doc.resolve(&d).as_dict().cloned())
.unwrap_or_default();
if let Some(dw) = desc.get(b"DW").and_then(|d| doc.resolve(d).as_f64()) {
m.default = dw;
}
if let Some(w) = desc.get(b"W").map(|w| doc.resolve(w)).and_then(|w| w.as_array().cloned()) {
let mut i = 0;
while i < w.len() {
let Some(c0) = doc.resolve(&w[i]).as_f64().map(|v| v as u32) else { break };
match w.get(i + 1).map(|o| doc.resolve(o)) {
Some(a) if a.as_array().is_some() => {
for (k, x) in a.as_array().into_iter().flatten().enumerate() {
if let Some(v) = doc.resolve(x).as_f64() {
m.cid_widths.insert(c0 + k as u32, v);
}
}
i += 2;
}
Some(c1) => {
if let (Some(c1), Some(v)) = (c1.as_f64(), w.get(i + 2).and_then(|x| doc.resolve(x).as_f64())) {
m.cid_ranges.push((c0, c1 as u32, v));
}
i += 3;
}
None => break,
}
}
}
descriptor = dict(doc, desc.get(b"FontDescriptor"));
} else {
m.first = font.get(b"FirstChar").and_then(|f| doc.resolve(f).as_f64()).unwrap_or(0.0).max(0.0) as u32;
m.widths = nums(doc, font.get(b"Widths"));
descriptor = dict(doc, font.get(b"FontDescriptor"));
m.default = descriptor.as_ref().and_then(|d| d.get(b"MissingWidth")).and_then(|w| doc.resolve(w).as_f64()).unwrap_or(0.0);
if subtype == b"Type3" {
let fm = nums(doc, font.get(b"FontMatrix"));
if fm.len() == 6 {
m.scale = fm[0];
let bbox = nums(doc, font.get(b"FontBBox"));
if bbox.len() == 4 && fm[3] != 0.0 {
m.ascent = (bbox[3] * fm[3]).max(0.5);
m.descent = (bbox[1] * fm[3]).min(-0.1);
}
}
return m;
}
if m.widths.is_empty() {
let base = String::from_utf8_lossy(font.name(b"BaseFont").unwrap_or(b"")).to_ascii_lowercase();
m.std14 = Some(if base.contains("courier") {
Std14::Courier
} else if base.contains("times") {
Std14::Times
} else if base.contains("symbol") || base.contains("dingbats") {
Std14::Symbolic
} else {
Std14::Helvetica
});
}
}
if let Some(d) = descriptor {
let a = d.get(b"Ascent").and_then(|v| doc.resolve(v).as_f64()).unwrap_or(0.0) / 1000.0;
let de = d.get(b"Descent").and_then(|v| doc.resolve(v).as_f64()).unwrap_or(0.0) / 1000.0;
// Fonts often claim 0; never shrink the glyph box below a sensible minimum.
m.ascent = if a > 0.3 { a.min(1.5) } else { 0.9 };
m.descent = if de < -0.05 { de.max(-0.6) } else { -0.25 };
}
m
}
/// Split a string into `(code, byte length)`.
pub fn codes(&self, s: &[u8]) -> Vec<(u32, usize)> {
let mut out = Vec::with_capacity(s.len());
let mut i = 0;
while i < s.len() {
let len = match &self.codes {
Codes::One => 1,
Codes::Two => 2,
Codes::Ranges(r) => r
.iter()
.find(|(len, lo, hi)| s.get(i..i + len).is_some_and(|b| b.iter().zip(lo.iter().zip(hi)).all(|(x, (l, h))| x >= l && x <= h)))
.map_or(r.first().map_or(1, |x| x.0), |x| x.0),
};
let len = len.min(s.len() - i).max(1);
out.push((be(&s[i..i + len]), len));
i += len;
}
out
}
fn cid(&self, code: u32) -> u32 {
if self.cid_map.is_empty() {
return code;
}
self.cid_map.iter().find(|(lo, hi, _)| code >= *lo && code <= *hi).map_or(0, |(lo, _, c)| c + (code - lo))
}
/// The advance of `code` in text space per unit of font size.
pub fn width(&self, code: u32) -> f64 {
if self.composite {
let cid = self.cid(code);
let w = self.cid_widths.get(&cid).copied().or_else(|| self.cid_ranges.iter().find(|(a, b, _)| cid >= *a && cid <= *b).map(|r| r.2));
return w.unwrap_or(self.default) * self.scale;
}
if let Some(w) = code.checked_sub(self.first).and_then(|i| self.widths.get(i as usize)) {
return w * self.scale;
}
match &self.std14 {
Some(Std14::Courier) => 0.6,
Some(Std14::Symbolic) => 0.75,
Some(f) => {
let c = char::from_u32(code).filter(|c| !c.is_control()).unwrap_or('n');
let w = crate::helvetica_width(&c.to_string(), 1.0);
if *f == Std14::Times { w * 0.92 } else { w }
}
None => self.default * self.scale,
}
}
/// Word spacing applies to the single-byte code 32 (§9.3.3).
pub fn is_space(&self, code: u32, len: usize) -> bool {
code == 32 && len == 1
}
}
impl Metrics {
/// The text a string shows (codes without a known meaning are left out).
pub fn decode(&self, s: &[u8]) -> String {
self.codes(s).into_iter().filter_map(|(c, _)| self.unicode.get(&c).cloned()).collect()
}
/// The Unicode text of one code, if known.
pub fn text_of(&self, code: u32) -> Option<&str> {
self.unicode.get(&code).map(String::as_str)
}
/// Whether the font has a glyph for `code` (subset fonts lack the glyphs they don't use).
fn has_glyph(&self, code: u32) -> bool {
if self.composite {
let cid = self.cid(code);
return self.cid_widths.contains_key(&cid) || self.cid_ranges.iter().any(|(a, b, _)| cid >= *a && cid <= *b) || !self.subset;
}
match code.checked_sub(self.first).and_then(|i| self.widths.get(i as usize)) {
Some(w) => *w > 0.0 || code == 32,
None => self.widths.is_empty() && !self.subset,
}
}
/// The bytes that show `text` in this font, or `None` if some character has no code or
/// no glyph in it (the caller then substitutes another font).
pub fn encode(&self, text: &str) -> Option<Vec<u8>> {
let mut reverse: HashMap<&str, u32> = HashMap::new();
for (code, t) in &self.unicode {
if self.has_glyph(*code) {
reverse.entry(t.as_str()).and_modify(|c| *c = (*c).min(*code)).or_insert(*code);
}
}
let mut out = Vec::with_capacity(text.len() * self.code_len);
let mut buf = [0u8; 4];
for ch in text.chars() {
let code = *reverse.get(ch.encode_utf8(&mut buf) as &str)?;
let bytes = code.to_be_bytes();
out.extend_from_slice(&bytes[4 - self.code_len.clamp(1, 4)..]);
}
Some(out)
}
}
/// UTF-16BE (with surrogates) from CMap hex bytes.
fn utf16(b: &[u8]) -> String {
let units: Vec<u16> = b.chunks(2).map(|c| u16::from_be_bytes([c[0], *c.get(1).unwrap_or(&0)])).collect();
String::from_utf16_lossy(&units)
}
/// `bfchar`/`bfrange` entries of a ToUnicode CMap.
fn parse_to_unicode(data: &[u8], out: &mut HashMap<u32, String>) {
let text = String::from_utf8_lossy(data);
// Tokenise, keeping arrays' brackets as tokens.
let spaced = text.replace('[', " [ ").replace(']', " ] ");
let toks: Vec<&str> = spaced.split_whitespace().collect();
let mut i = 0;
while i < toks.len() {
match toks[i] {
"beginbfchar" => {
i += 1;
while i + 1 < toks.len() && toks[i] != "endbfchar" {
if let (Some(c), Some(u)) = (hex_bytes(toks[i]), hex_bytes(toks[i + 1])) {
out.insert(be(&c), utf16(&u));
}
i += 2;
}
}
"beginbfrange" => {
i += 1;
while i + 2 < toks.len() && toks[i] != "endbfrange" {
let (Some(lo), Some(hi)) = (hex_bytes(toks[i]), hex_bytes(toks[i + 1])) else { break };
let (lo, hi) = (be(&lo), be(&hi));
if toks[i + 2] == "[" {
let mut j = i + 3;
let mut code = lo;
while j < toks.len() && toks[j] != "]" {
if let Some(u) = hex_bytes(toks[j]) {
out.insert(code, utf16(&u));
}
code += 1;
j += 1;
}
i = j + 1;
} else {
if let Some(u) = hex_bytes(toks[i + 2])
&& hi >= lo
&& hi - lo < 65536
{
let mut units: Vec<u16> = u.chunks(2).map(|c| u16::from_be_bytes([c[0], *c.get(1).unwrap_or(&0)])).collect();
for code in lo..=hi {
out.insert(code, String::from_utf16_lossy(&units));
if let Some(last) = units.last_mut() {
*last = last.wrapping_add(1);
}
}
}
i += 3;
}
}
}
_ => {}
}
i += 1;
}
}
/// A glyph name's Unicode (Annex D names, `uniXXXX`, `uXXXX[XX]`, single letters).
pub fn glyph_unicode(name: &str) -> Option<char> {
if let Ok(i) = crate::encodings::NAMES.binary_search_by(|(n, _)| (*n).cmp(name)) {
return char::from_u32(crate::encodings::NAMES[i].1);
}
let hex = name.strip_prefix("uni").filter(|h| h.len() == 4).or_else(|| name.strip_prefix('u').filter(|h| (4..=6).contains(&h.len())))?;
u32::from_str_radix(hex, 16).ok().and_then(char::from_u32)
}
/// Code → Unicode for a font: its ToUnicode CMap, else (simple fonts) its encoding.
fn unicode_map(doc: &Document, font: &Dict, composite: bool) -> HashMap<u32, String> {
let mut out = HashMap::new();
if !composite {
let enc = font.get(b"Encoding").map(|e| doc.resolve(e));
let base_name = match enc.as_deref() {
Some(Object::Name(n)) => Some(n.clone()),
Some(Object::Dict(d)) => d.name(b"BaseEncoding").map(<[u8]>::to_vec),
_ => None,
};
let table: &[u32; 256] = match base_name.as_deref() {
Some(b"WinAnsiEncoding") => &crate::encodings::WIN_ANSI,
Some(b"MacRomanEncoding") => &crate::encodings::MAC_ROMAN,
_ => &crate::encodings::STANDARD,
};
for (code, u) in table.iter().enumerate() {
if let Some(c) = char::from_u32(*u).filter(|_| *u != 0) {
out.insert(code as u32, c.to_string());
}
}
if let Some(Object::Dict(d)) = enc.as_deref()
&& let Some(diffs) = d.get(b"Differences").map(|x| doc.resolve(x)).and_then(|x| x.as_array().cloned())
{
let mut code = 0u32;
for item in diffs {
match &item {
Object::Int(n) => code = (*n).max(0) as u32,
Object::Name(n) => {
match glyph_unicode(&String::from_utf8_lossy(n)) {
Some(c) => out.insert(code, c.to_string()),
None => out.remove(&code),
};
code += 1;
}
_ => {}
}
}
}
}
if let Some(Object::Stream(s)) = font.get(b"ToUnicode").map(|t| doc.resolve(t)).as_deref()
&& let Ok(data) = s.decoded()
{
parse_to_unicode(&data, &mut out);
}
out
}
#[cfg(test)]
mod tests {
use printcraft_cos::{Dict, Document, Object, Stream};
use super::*;
fn font(doc: &mut Document, entries: Vec<(&str, Object)>) -> Dict {
let mut d = Dict::new();
d.set(b"Type".to_vec(), Object::name("Font"));
for (k, v) in entries {
d.set(k.as_bytes().to_vec(), v);
}
let _ = doc;
d
}
#[test]
fn simple_fonts_decode_and_encode_through_their_encoding() {
let mut doc = Document::new_empty();
let f = font(
&mut doc,
vec![("Subtype", Object::name("Type1")), ("BaseFont", Object::name("Helvetica")), ("Encoding", Object::name("WinAnsiEncoding"))],
);
let m = Metrics::from_dict(&doc, &f);
assert_eq!(m.decode(b"Caf\xe9 \x80 \x93x\x94"), "Café € “x”");
assert_eq!(m.encode("Café €").as_deref(), Some(&b"Caf\xe9 \x80"[..]));
assert_eq!(m.encode("Ω"), None, "not in WinAnsi");
// Differences rename codes by glyph name.
let mut enc = Dict::new();
enc.set(b"Differences".to_vec(), Object::Array(vec![Object::Int(65), Object::name("eacute"), Object::name("uni03A9")]));
let f = font(&mut doc, vec![("Subtype", Object::name("Type1")), ("BaseFont", Object::name("Custom")), ("Encoding", Object::Dict(enc))]);
let m = Metrics::from_dict(&doc, &f);
assert_eq!(m.decode(b"AB"), "éΩ");
assert_eq!(m.encode("Ω").as_deref(), Some(&b"B"[..]));
}
#[test]
fn subset_fonts_only_encode_the_glyphs_they_have() {
let mut doc = Document::new_empty();
// Widths for a, b only (97, 98); c (99) is 0.
let widths = Object::Array(vec![Object::Int(500), Object::Int(520), Object::Int(0)]);
let f = font(
&mut doc,
vec![
("Subtype", Object::name("TrueType")),
("BaseFont", Object::name("ABCDEF+Arial")),
("FirstChar", Object::Int(97)),
("Widths", widths),
("Encoding", Object::name("WinAnsiEncoding")),
],
);
let m = Metrics::from_dict(&doc, &f);
assert!(m.subset);
assert_eq!(m.encode("ab").as_deref(), Some(&b"ab"[..]));
assert_eq!(m.encode("abc"), None);
}
#[test]
fn composite_fonts_use_to_unicode() {
let mut doc = Document::new_empty();
let cmap = b"/CIDInit /ProcSet findresource begin 1 begincodespacerange <0000> <FFFF> endcodespacerange \
2 beginbfchar <0003> <0020> <0010> <00E9> endbfchar 1 beginbfrange <0024> <0026> <0041> endbfrange \
1 beginbfrange <0030> <0031> [<0048> <0069>] endbfrange end";
let tu = doc.add(Object::Stream(Stream::from_raw(Dict::new(), cmap.to_vec())));
let mut desc = Dict::new();
desc.set(b"Subtype".to_vec(), Object::name("CIDFontType2"));
desc.set(
b"W".to_vec(),
Object::Array(vec![
Object::Int(3),
Object::Array(vec![Object::Int(250)]),
Object::Int(16),
Object::Int(16),
Object::Int(500),
Object::Int(36),
Object::Int(49),
Object::Int(600),
]),
);
let f = font(
&mut doc,
vec![
("Subtype", Object::name("Type0")),
("BaseFont", Object::name("QWERTY+Noto")),
("Encoding", Object::name("Identity-H")),
("DescendantFonts", Object::Array(vec![Object::Dict(desc)])),
("ToUnicode", Object::Ref(tu)),
],
);
let m = Metrics::from_dict(&doc, &f);
assert_eq!(m.decode(&[0, 0x24, 0, 0x25, 0, 3, 0, 0x30, 0, 0x31, 0, 0x10]), "AB Hié");
assert_eq!(m.encode("Hi A").as_deref(), Some(&[0, 0x30, 0, 0x31, 0, 3, 0, 0x24][..]));
assert_eq!(m.encode("Z"), None);
}
#[test]
fn glyph_names() {
assert_eq!(glyph_unicode("quotedblleft"), Some('“'));
assert_eq!(glyph_unicode("uni20AC"), Some('€'));
assert_eq!(glyph_unicode("u1F600"), Some('😀'));
assert_eq!(glyph_unicode("nonsense"), None);
}
}

View File

@ -23,8 +23,8 @@ use std::rc::Rc;
use printcraft_content::{Matrix, Op, contains, num, overlaps, parse, serialize_ops, string};
use printcraft_cos::{Dict, Document, ObjRef, Object, Stream};
use crate::fonts::Metrics;
use crate::{Report, image};
use printcraft_fonts::pdf::Metrics;
const MAX_DEPTH: usize = 12;

View File

@ -14,7 +14,6 @@
use printcraft_cos::{Dict, Document, ObjRef, Object, Stream};
mod fonts;
mod image;
mod interp;
pub mod patterns;