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:
parent
2548228b1d
commit
5a70838884
|
|
@ -3487,6 +3487,7 @@ dependencies = [
|
|||
name = "printcraft-fonts"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"printcraft-cos",
|
||||
"skrifa 0.44.0",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
];
|
||||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
|
||||
use printcraft_cos::{Dict, Document, ObjRef, Object, Stream};
|
||||
|
||||
mod fonts;
|
||||
mod image;
|
||||
mod interp;
|
||||
pub mod patterns;
|
||||
|
|
|
|||
Loading…
Reference in New Issue