Files
pdfcraft/crates/fonts/src/pdf.rs
T

2261 lines
105 KiB
Rust

//! 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`); a simple font with no `/Widths` uses its embedded
//! sfnt program's advances, else the exact published standard-14 metrics ([`crate::Std14`]).
//!
//! Meanings come from `/ToUnicode`, else the encoding (`/Encoding` base and `/Differences` glyph
//! names, ISO 32000-2 Annex D). A code may deliberately have **no** meaning: where the font's own
//! built-in encoding governs and we hold no table for it — Symbol, ZapfDingbats, MacExpert — this
//! module reports nothing rather than the Latin letter at that code. Callers must treat a missing
//! meaning as "unknown", never as "absent": `redact` matches on this map, and a wrong character
//! there is worse than no character.
use std::borrow::Cow;
use std::collections::{BinaryHeap, HashMap, HashSet};
use pdfcraft_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)]
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: Spans<f64>,
/// Code → CID for embedded non-identity CMaps (`cidrange` / `cidchar`).
cid_map: Spans<u32>,
default: f64,
/// Composite fonts with an explicit `/DW`: `/W` may leave out the glyphs that have that
/// width (Chrome and Skia do), so a CID missing from `/W` is not a missing glyph.
explicit_default: bool,
/// The standard-14 face to measure with when the dictionary carries no `/Widths`.
std14: Option<crate::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: Unicode,
/// `/BaseFont` (or Type 3 descriptor's `/FontName`), and whether it is a subset (`ABCDEF+Name`: other glyphs are missing).
pub base_font: String,
pub subset: bool,
/// Whether the PDF identifies this face as bold or italic. These come from the
/// font name and, when present, `/FontDescriptor` flags/weight/angle.
pub bold: bool,
pub italic: bool,
/// Whether this font draws Indic text in visual order (pre-base vowel signs before their
/// consonant): a known legacy Indic font whose Unicode mapping is Indic. See [`legacy_indic_font`].
pub visual_order: bool,
/// Bytes per code for writing new text (1 for simple fonts, else the codespace length).
code_len: usize,
/// Valid single-character explicit mappings, preindexed once for large full CJK fonts.
explicit_reverse: HashMap<char, u32>,
/// Subset CIDFontType2 only: one bit per CID (below 65 536) whose glyph has an outline in the
/// embedded program. See [`cids_with_outlines`].
outlined_cids: Vec<u64>,
}
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())
}
/// Whether `base_font` (subset tag allowed) names a legacy Indic font family that is laid out in
/// visual order (Kruti Dev, DevLys, Chanakya, Shusha, Shivaji, Walkman, Akruti, APS-DV, DV-TT…).
fn legacy_indic_font(base_font: &str) -> bool {
let name =
base_font.split_once('+').filter(|(tag, _)| tag.len() == 6 && tag.bytes().all(|b| b.is_ascii_uppercase())).map_or(base_font, |(_, n)| n);
let name = name.to_ascii_lowercase();
["kruti", "devlys", "chanakya", "shusha", "shivaji", "walkman", "akruti"].iter().any(|n| name.contains(n))
|| ["aps-", "dv-"].iter().any(|p| name.starts_with(p))
}
/// A source PDF's sfnt program for an in-memory editor. Never a bundled font or an export
/// fallback. Type 1/Type 3 programs cannot be consumed by an sfnt text renderer.
pub fn embedded_sfnt(doc: &Document, font: &Dict) -> Option<Vec<u8>> {
use skrifa::{MetadataProvider as _, raw::TableProvider as _};
let descendant = dict(doc, font.get(b"DescendantFonts").and_then(|o| doc.resolve(o).as_array().and_then(|a| a.first()).cloned()).as_ref());
let descriptor = dict(doc, descendant.as_ref().unwrap_or(font).get(b"FontDescriptor"))?;
for key in [b"FontFile2".as_slice(), b"FontFile3".as_slice()] {
let Some(object) = descriptor.get(key) else { continue };
let object = doc.resolve(object);
let Object::Stream(stream) = object.as_ref() else { continue };
let Ok(mut bytes) = stream.decoded_within(16 << 20) else { continue };
let Ok(face) = skrifa::FontRef::from_index(&bytes, 0) else { continue };
if face.metrics(skrifa::instance::Size::unscaled(), skrifa::instance::LocationRef::default()).units_per_em == 0 {
continue;
}
if face.charmap().mappings().next().is_some() {
return Some(bytes);
}
// Some PDF generators keep a Mac format-6 code→glyph cmap. egui accepts Unicode
// format-4/12 cmaps. Combine the PDF's Unicode meanings with the source glyph IDs;
// only this in-memory copy gets a new cmap, never the working PDF or a font asset.
let Ok(cmap) = face.cmap() else { continue };
let metrics = Metrics::from_dict(doc, font);
if metrics.composite {
continue;
}
let mut mappings = std::collections::BTreeMap::new();
for code in 0..=255 {
let Some(text) = metrics.unicode.text(code) else { continue };
let Some(ch) = only_char(&text) else { continue };
let Some(glyph) = cmap.map_codepoint(code).or_else(|| cmap.map_codepoint(code + 0xf000)) else { continue };
let glyph = glyph.to_u32();
if glyph > 0 {
mappings.entry(u32::from(ch)).or_insert(glyph);
}
}
if !mappings.is_empty()
&& append_editor_cmap(&mut bytes, &mappings).is_some()
&& skrifa::FontRef::new(&bytes).is_ok_and(|font| font.charmap().mappings().next().is_some())
{
return Some(bytes);
}
}
None
}
/// The CIDs (below 65 536) of a Type 0 font over an embedded TrueType program whose glyph has
/// outline data, through `/CIDToGIDMap`; empty when the program or the map can't be read.
/// Subsetters keep glyph ids and empty the glyphs they drop, so an empty glyph is a missing one.
fn cids_with_outlines(doc: &Document, font: &Dict) -> Vec<u64> {
use skrifa::raw::TableProvider as _;
let Some(descendant) = dict(doc, font.get(b"DescendantFonts").and_then(|o| doc.resolve(o).as_array().and_then(|a| a.first()).cloned()).as_ref())
else {
return Vec::new();
};
let Some(program) = dict(doc, descendant.get(b"FontDescriptor")).and_then(|d| d.get(b"FontFile2").cloned()) else { return Vec::new() };
let program = doc.resolve(&program);
let Object::Stream(stream) = program.as_ref() else { return Vec::new() };
let Ok(bytes) = stream.decoded_within(16 << 20) else { return Vec::new() };
let Some(loca) = skrifa::FontRef::from_index(&bytes, 0).ok().and_then(|face| face.loca(None).ok()) else { return Vec::new() };
let outlined = |gid: u32| loca.get_raw(gid as usize).zip(loca.get_raw(gid as usize + 1)).is_some_and(|(start, end)| end > start);
match descendant.get(b"CIDToGIDMap").map(|o| doc.resolve(o)).as_deref() {
None | Some(Object::Name(_)) => bit_set_below_64k((0..loca.len().min(0x1_0000) as u32).filter(|&gid| outlined(gid))),
Some(Object::Stream(map)) => {
let Ok(map) = map.decoded_within(1 << 17) else { return Vec::new() };
bit_set_below_64k(
map.as_chunks::<2>()
.0
.iter()
.enumerate()
.filter(|(_, gid)| outlined(u32::from(u16::from_be_bytes(**gid))))
.map(|(cid, _)| cid as u32),
)
}
Some(_) => Vec::new(),
}
}
/// A simple font with no `/Widths` is drawn (and found by text search) with its embedded
/// program's own advances, so measure with them too; a standard-14 guess from the name can be
/// wider and leave the end of a line outside a redaction mark (#2130). The glyph is looked up
/// the way the renderer does: the encoding's character in the Unicode cmap, else the code itself
/// (as in a symbolic cmap), else `.notdef`. Only sfnt programs are read; others keep the guess.
fn embedded_widths(doc: &Document, font: &Dict, m: &mut Metrics) {
use skrifa::instance::{LocationRef, Size};
use skrifa::{GlyphId, MetadataProvider as _, raw::TableProvider as _};
let Some(descriptor) = dict(doc, font.get(b"FontDescriptor")) else { return };
for key in [b"FontFile2".as_slice(), b"FontFile3".as_slice()] {
let Some(object) = descriptor.get(key) else { continue };
let object = doc.resolve(object);
let Object::Stream(stream) = object.as_ref() else { continue };
let Ok(bytes) = stream.decoded_within(16 << 20) else { continue };
let Ok(face) = skrifa::FontRef::from_index(&bytes, 0) else { continue };
let upem = f64::from(face.metrics(Size::unscaled(), LocationRef::default()).units_per_em);
if upem == 0.0 {
continue;
}
let advances = face.glyph_metrics(Size::unscaled(), LocationRef::default());
let (charmap, cmap) = (face.charmap(), face.cmap().ok());
let found = |g: Option<GlyphId>| g.filter(|g| *g != GlyphId::NOTDEF);
m.widths = (0..=255u32)
.map(|code| {
let glyph = found(m.unicode.encoding.get(&code).and_then(|t| only_char(t)).and_then(|c| charmap.map(c)))
.or_else(|| [0, 0xF000, 0xF100, 0xF200].into_iter().find_map(|o| found(cmap.as_ref()?.map_codepoint(code + o))));
// A subset holds only the glyphs it draws: 0 keeps `has_glyph` false for the rest.
match glyph {
None if m.subset => 0.0,
_ => advances.advance_width(glyph.unwrap_or(GlyphId::NOTDEF)).map_or(0.0, |w| f64::from(w) * 1000.0 / upem),
}
})
.collect();
m.first = 0;
return;
}
}
/// Replace an existing cmap directory entry with a bounded Unicode format-12 table.
/// Checked ranges prevent damaged sfnt offsets from indexing outside the source program.
fn append_editor_cmap(bytes: &mut Vec<u8>, mappings: &std::collections::BTreeMap<u32, u32>) -> Option<()> {
if mappings.len() > 65536 {
return None;
}
let count = u16::from_be_bytes(bytes.get(4..6)?.try_into().ok()?) as usize;
if count > 256 {
return None;
}
let mut entry = None;
let mut head = None;
for i in 0..count {
let at = 12 + i * 16;
let record = bytes.get(at..at + 16)?;
if record.get(..4)? == b"cmap" {
entry = Some(at);
}
if record.get(..4)? == b"head" {
head = Some(u32::from_be_bytes(record.get(8..12)?.try_into().ok()?) as usize);
}
}
let entry = entry?;
let mut cmap = Vec::with_capacity(28 + mappings.len() * 12);
cmap.extend_from_slice(&[0, 0, 0, 1, 0, 3, 0, 10, 0, 0, 0, 12, 0, 12, 0, 0]);
cmap.extend_from_slice(&(16_u32 + u32::try_from(mappings.len()).ok()? * 12).to_be_bytes());
cmap.extend_from_slice(&0_u32.to_be_bytes());
cmap.extend_from_slice(&u32::try_from(mappings.len()).ok()?.to_be_bytes());
for (&ch, &gid) in mappings {
cmap.extend_from_slice(&ch.to_be_bytes());
cmap.extend_from_slice(&ch.to_be_bytes());
cmap.extend_from_slice(&gid.to_be_bytes());
}
while !bytes.len().is_multiple_of(4) {
bytes.push(0);
}
let offset = u32::try_from(bytes.len()).ok()?;
let checksum = |data: &[u8]| {
data.chunks(4).fold(0_u32, |sum, c| {
let mut word = [0; 4];
for (a, b) in word.iter_mut().zip(c) {
*a = *b;
}
sum.wrapping_add(u32::from_be_bytes(word))
})
};
bytes.get_mut(entry + 4..entry + 8)?.copy_from_slice(&checksum(&cmap).to_be_bytes());
bytes.get_mut(entry + 8..entry + 12)?.copy_from_slice(&offset.to_be_bytes());
bytes.get_mut(entry + 12..entry + 16)?.copy_from_slice(&u32::try_from(cmap.len()).ok()?.to_be_bytes());
bytes.extend_from_slice(&cmap);
if let Some(head) = head {
bytes.get_mut(head.checked_add(8)?..head.checked_add(12)?)?.fill(0);
let adjustment = 0xb1b0afba_u32.wrapping_sub(checksum(bytes));
bytes.get_mut(head + 8..head + 12)?.copy_from_slice(&adjustment.to_be_bytes());
}
Some(())
}
fn descriptor_flags(doc: &Document, descriptor: &Dict) -> u32 {
let value = descriptor.get(b"Flags").and_then(|v| doc.resolve(v).as_f64()).unwrap_or(0.0) as i64;
u32::try_from(value).unwrap_or(0)
}
/// Most mappings a CMap may add, where each `bfchar` or array-form entry counts one and each sequential
/// range counts two (it covers at least two codes). So a CMap with at most this many codes is kept whole;
/// mappings past the budget are ignored, so a stream of them can't take minutes or gigabytes.
const MAX_CMAP_ENTRIES: usize = 1 << 20;
/// Most codespace or CID ranges kept from a CMap; Adobe's largest published CMaps have a few
/// thousand.
const MAX_CMAP_RANGES: usize = 1 << 16;
/// Code → Unicode of a font. Explicit mappings and sequential ranges are kept as declared, so a range
/// costs one record however many codes it covers; where two overlap, the one declared later wins.
#[derive(Clone, Debug, Default, PartialEq)]
struct Unicode {
/// Simple fonts: the encoding table and `/Differences`, which ToUnicode overrides.
encoding: HashMap<u32, String>,
/// `bfchar` and array-form entries: the text of each code, with the declaration order of its last one.
explicit: HashMap<u32, (u32, String)>,
/// Sequential `bfrange` records, in declaration order.
ranges: Vec<BfRange>,
/// Which record covers each stretch of codes; see [`cover_index`].
cover: Vec<(u64, Option<usize>)>,
/// The latest-declared range covering each code below 65 536, as an index into `ranges` (`u32::MAX` for
/// none); empty without ranges. See [`owners_below_64k`].
low: Vec<u32>,
/// One bit per code below 65 536 that has an explicit entry; empty without any. A miss then skips the
/// hash lookup. See [`bit_set_below_64k`].
explicit_low: Vec<u64>,
}
/// A sequential `bfrange`: code `lo + k` (up to `hi`) maps to `units` with its last unit moved on by `k`.
#[derive(Clone, Debug, PartialEq)]
struct BfRange {
lo: u32,
hi: u32,
/// Declaration order, shared with the explicit entries.
seq: u32,
units: Box<[u16]>,
}
impl Unicode {
/// Whether `code` may have an explicit entry; false is certain for codes below 65 536 without one.
#[inline]
fn may_be_explicit(&self, code: u32) -> bool {
code >= 0x1_0000 || self.explicit_low.get(code as usize / 64).is_some_and(|w| (*w >> (code % 64)) & 1 == 1)
}
/// The latest-declared range that covers `code`.
#[inline]
fn covering(&self, code: u32) -> Option<&BfRange> {
if code < 0x1_0000 {
return self.ranges.get(*self.low.get(code as usize)? as usize);
}
let step = self.cover.partition_point(|&(start, _)| start <= u64::from(code));
let &(_, record) = self.cover.get(step.checked_sub(1)?)?;
self.ranges.get(record?)
}
/// The text of `code`: its explicit entry, unless a range declared after it covers the code;
/// otherwise the covering range, otherwise the encoding.
#[inline]
fn text(&self, code: u32) -> Option<Cow<'_, str>> {
let range = self.covering(code);
let explicit = if self.may_be_explicit(code) { self.explicit.get(&code) } else { None };
if let Some((seq, text)) = explicit
&& range.is_none_or(|r| r.seq < *seq)
{
return Some(Cow::Borrowed(text.as_str()));
}
match range {
Some(r) => Some(Cow::Owned(r.text_at(code))),
None => self.encoding.get(&code).map(|t| Cow::Borrowed(t.as_str())),
}
}
}
impl BfRange {
/// The text of `code`, which lies in this range.
fn text_at(&self, code: u32) -> String {
// The offset is below 65 536 (the width limit in `parse_to_unicode`), so truncating it is exact.
let offset = code.saturating_sub(self.lo) as u16;
let last = self.units.len().saturating_sub(1);
let units = self.units.iter().enumerate().map(|(i, &u)| if i == last { u.wrapping_add(offset) } else { u });
char::decode_utf16(units).map(|r| r.unwrap_or(char::REPLACEMENT_CHARACTER)).collect()
}
/// The lowest code of this range whose own text is `c` and that `valid` accepts. Only a single
/// unit (a BMP character, or U+FFFD through a surrogate unit) or a surrogate pair can decode to
/// one character `c`; longer destinations always decode to several.
fn first_code(&self, c: char, valid: impl Fn(u32) -> bool) -> Option<u32> {
let width = self.hi.checked_sub(self.lo)?;
match &*self.units {
[base] if c == '\u{FFFD}' => {
// The offsets whose unit is a surrogate or U+FFFD, in order; each decodes to U+FFFD.
let mut cursor = 0u32;
loop {
let unit = (u32::from(*base) + cursor) & 0xFFFF;
let offset = cursor.checked_add(match unit {
0..=0xD7FF => 0xD800 - unit,
0xD800..=0xDFFF | 0xFFFD => 0,
0xE000..=0xFFFC => 0xFFFD - unit,
_ => 0x1_0000 - unit + 0xD800,
})?;
if offset > width {
return None;
}
if let Some(code) = self.lo.checked_add(offset)
&& valid(code)
{
return Some(code);
}
cursor = offset.checked_add(1)?;
}
}
[base] => {
let offset = u32::from(u16::try_from(u32::from(c)).ok()?.wrapping_sub(*base));
let code = self.lo.checked_add(offset).filter(|_| offset <= width)?;
valid(code).then_some(code)
}
[lead, last] => {
let v = u32::from(c).checked_sub(0x1_0000)?;
if u32::from(*lead) != 0xD800 + (v >> 10) {
return None;
}
let low = u16::try_from(0xDC00 + (v & 0x3FF)).ok()?;
let offset = u32::from(low.wrapping_sub(*last));
let code = self.lo.checked_add(offset).filter(|_| offset <= width)?;
valid(code).then_some(code)
}
_ => None,
}
}
}
/// A token of a CMap stream (PostScript syntax, ISO 32000-2 §9.10.3).
#[derive(Debug, PartialEq)]
enum Tok<'a> {
/// A `<…>` string; `None` when it isn't an even number of hex digits.
Hex(Option<Vec<u8>>),
Open,
Close,
/// An operator, number or name (with its `/`), a `(…)` string or a `<<`/`>>`.
Word(&'a [u8]),
}
/// Splits a CMap into tokens without assuming whitespace between them (`<01><0041>` is two
/// strings), skipping `%` comments.
struct Lexer<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> Lexer<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 }
}
fn at(&self, i: usize) -> Option<u8> {
self.data.get(i).copied()
}
}
fn is_cmap_space(b: u8) -> bool {
matches!(b, 0 | b'\t' | b'\n' | 0x0c | b'\r' | b' ')
}
fn is_cmap_delimiter(b: u8) -> bool {
matches!(b, b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%')
}
impl<'a> Iterator for Lexer<'a> {
type Item = Tok<'a>;
fn next(&mut self) -> Option<Tok<'a>> {
loop {
let b = self.at(self.pos)?;
if is_cmap_space(b) {
self.pos += 1;
} else if b == b'%' {
while self.at(self.pos).is_some_and(|c| c != b'\n' && c != b'\r') {
self.pos += 1;
}
} else {
break;
}
}
let start = self.pos;
let b = self.at(start)?;
self.pos += 1;
match b {
b'[' => Some(Tok::Open),
b']' => Some(Tok::Close),
b'<' | b'>' if self.at(self.pos) == Some(b) => {
self.pos += 1;
Some(Tok::Word(self.data.get(start..self.pos)?))
}
b'<' => {
let mut digits = Vec::new();
let mut valid = true;
loop {
match self.at(self.pos) {
// Unterminated: what's left is one bad string.
None => return Some(Tok::Hex(None)),
Some(b'>') => break,
Some(c) if is_cmap_space(c) => {}
Some(c) => match (c as char).to_digit(16) {
// A hex digit is below 16, so it fits a u8.
Some(d) => digits.push(d as u8),
None => valid = false,
},
}
self.pos += 1;
}
self.pos += 1;
if !valid || digits.is_empty() || !digits.len().is_multiple_of(2) {
return Some(Tok::Hex(None));
}
// `chunks` never yields an empty slice, so `p[0]` exists.
Some(Tok::Hex(Some(digits.chunks(2).map(|p| p[0] << 4 | p.get(1).copied().unwrap_or(0)).collect())))
}
b'(' => {
// Literal strings nest and escape their parentheses; only skipping them matters.
let mut depth = 1usize;
while depth > 0 {
match self.at(self.pos) {
None => break,
Some(b'\\') => self.pos += 1,
Some(b'(') => depth += 1,
Some(b')') => depth -= 1,
_ => {}
}
self.pos += 1;
}
Some(Tok::Word(self.data.get(start..self.pos.min(self.data.len()))?))
}
_ => {
// A name keeps its `/`; other delimiters (`)`, `>`, `{`, `}`) stand alone.
if b == b'/' || !is_cmap_delimiter(b) {
while self.at(self.pos).is_some_and(|c| !is_cmap_space(c) && !is_cmap_delimiter(c)) {
self.pos += 1;
}
}
Some(Tok::Word(self.data.get(start..self.pos)?))
}
}
}
}
fn be(b: &[u8]) -> u32 {
b.iter().fold(0u32, |a, x| (a << 8) | u32::from(*x))
}
fn style_from_name(name: &str) -> (bool, bool) {
let name = name.to_ascii_lowercase();
let name = name.split_once('+').map_or(name.as_str(), |(_, n)| n);
(
["bold", "black", "heavy", "semibold", "demi"].iter().any(|s| name.contains(s)),
["italic", "oblique", "slanted"].iter().any(|s| name.contains(s)),
)
}
/// The next entry of a `begin… end…` section: `N` tokens, or `None` at its `end` operator or
/// the end of the stream.
fn entry<'a, const N: usize>(toks: &mut Lexer<'a>, end: &[u8]) -> Option<[Tok<'a>; N]> {
let mut out = Vec::with_capacity(N);
for _ in 0..N {
match toks.next()? {
Tok::Word(w) if w == end => return None,
t => out.push(t),
}
}
out.try_into().ok()
}
fn number(t: &Tok) -> Option<u32> {
match t {
Tok::Word(w) => std::str::from_utf8(w).ok()?.parse().ok(),
_ => None,
}
}
/// Codespace ranges and CID mappings from an embedded CMap stream, at most
/// [`MAX_CMAP_RANGES`] of each (every glyph searches them).
#[allow(clippy::type_complexity)]
fn parse_cmap(data: &[u8]) -> (Vec<(usize, Vec<u8>, Vec<u8>)>, Vec<(u32, u32, u32)>) {
let (mut spaces, mut cids) = (Vec::new(), Vec::new());
let mut toks = Lexer::new(data);
while let Some(t) = toks.next() {
match t {
Tok::Word(b"begincodespacerange") => {
while let Some([lo, hi]) = entry(&mut toks, b"endcodespacerange") {
if let (Tok::Hex(Some(lo)), Tok::Hex(Some(hi))) = (lo, hi)
&& lo.len() == hi.len()
&& spaces.len() < MAX_CMAP_RANGES
{
spaces.push((lo.len(), lo, hi));
}
}
}
Tok::Word(b"begincidrange") => {
while let Some([lo, hi, c]) = entry(&mut toks, b"endcidrange") {
if let (Tok::Hex(Some(lo)), Tok::Hex(Some(hi)), Some(c)) = (&lo, &hi, number(&c))
&& cids.len() < MAX_CMAP_RANGES
{
cids.push((be(lo), be(hi), c));
}
}
}
Tok::Word(b"begincidchar") => {
while let Some([code, c]) = entry(&mut toks, b"endcidchar") {
if let (Tok::Hex(Some(code)), Some(c)) = (&code, number(&c))
&& cids.len() < MAX_CMAP_RANGES
{
cids.push((be(code), be(code), c));
}
}
}
_ => {}
}
}
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: Spans::default(),
cid_map: Spans::default(),
default: 500.0,
explicit_default: false,
std14: Some(crate::Std14::Helvetica),
scale: 0.001,
ascent: 0.9,
descent: -0.25,
composite: false,
unicode: Unicode { encoding: (32..127u8).map(|c| (u32::from(c), char::from(c).to_string())).collect(), ..Unicode::default() },
base_font: "Helvetica".into(),
subset: false,
bold: false,
italic: false,
visual_order: false,
code_len: 1,
explicit_reverse: (32..127u8).map(|c| (char::from(c), u32::from(c))).collect(),
outlined_cids: Vec::new(),
}
}
pub fn from_dict(doc: &Document, font: &Dict) -> Self {
let mut m = Self::read_metrics(doc, font);
let base = font
.name(b"BaseFont")
.map(|name| String::from_utf8_lossy(name).into_owned())
.or_else(|| {
(font.name(b"Subtype") == Some(b"Type3"))
.then(|| dict(doc, font.get(b"FontDescriptor")))
.flatten()
.and_then(|d| d.name(b"FontName").map(|name| String::from_utf8_lossy(name).into_owned()))
})
.unwrap_or_default();
m.subset = base.len() > 7 && base.as_bytes()[6] == b'+' && base[..6].bytes().all(|b| b.is_ascii_uppercase());
let (bold, italic) = style_from_name(&base);
m.bold |= bold;
m.italic |= italic;
m.base_font = base;
if m.composite && m.subset {
m.outlined_cids = cids_with_outlines(doc, font);
}
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);
if m.std14.is_some() {
embedded_widths(doc, font, &mut m);
}
let mut reverse = HashMap::new();
for (&code, text) in m.unicode.explicit.iter().map(|(c, (_, t))| (c, t)).chain(m.unicode.encoding.iter()) {
if let Some(ch) = only_char(text)
&& m.shows(code, ch)
{
keep(&mut reverse, ch, code);
}
}
m.explicit_reverse = reverse;
// Only a font known to be laid out in visual order counts: a missing GSUB proves nothing
// (bare CFF and subset fonts often drop it), and the font program is never decoded here.
if legacy_indic_font(&m.base_font) {
let indic = |c: char| matches!(u32::from(c), 0x0900..=0x0D7F);
m.visual_order = m.unicode.explicit.values().any(|(_, t)| t.chars().any(indic))
|| m.unicode.encoding.values().any(|t| t.chars().any(indic))
|| m.unicode.ranges.iter().any(|r| r.units.iter().any(|&u| matches!(u, 0x0900..=0x0D7F)));
}
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 = Spans::new(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;
m.explicit_default = true;
}
if let Some(w) = desc.get(b"W").map(|w| doc.resolve(w)).and_then(|w| w.as_array().cloned()) {
let mut ranges = Vec::new();
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() {
// A run past the last CID ends there: wrapping would give CID 0 its width.
let Some(cid) = u32::try_from(k).ok().and_then(|k| c0.checked_add(k)) else { break };
if let Some(v) = doc.resolve(x).as_f64() {
m.cid_widths.insert(cid, 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()))
&& ranges.len() < MAX_CMAP_ENTRIES
{
ranges.push((c0, c1 as u32, v));
}
i += 3;
}
None => break,
}
}
m.cid_ranges = Spans::new(ranges);
}
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);
}
}
} else if m.widths.is_empty() {
let base = String::from_utf8_lossy(font.name(b"BaseFont").unwrap_or(b"")).into_owned();
// The real face, bold and italic included: the published metrics differ by style,
// so Times-Bold is not Times-Roman and neither is Helvetica scaled.
m.std14 = crate::Std14::from_base_font(&base).or(Some(crate::Std14::Helvetica));
}
}
if let Some(d) = descriptor {
let flags = descriptor_flags(doc, &d);
let angle = d.get(b"ItalicAngle").and_then(|v| doc.resolve(v).as_f64()).unwrap_or(0.0);
let weight = d.get(b"FontWeight").and_then(|v| doc.resolve(v).as_f64()).unwrap_or(0.0);
m.italic |= flags & 64 != 0 || angle.abs() > 0.1;
m.bold |= flags & 262_144 != 0 || weight >= 600.0;
// Type 3 descriptors may omit these metrics; the glyph-space FontBBox still applies.
let ascent = d.get(b"Ascent").and_then(|v| doc.resolve(v).as_f64());
let descent = d.get(b"Descent").and_then(|v| doc.resolve(v).as_f64());
// Fonts often claim 0; never shrink the glyph box below a sensible minimum.
if ascent.is_some() || subtype != b"Type3" {
let a = ascent.unwrap_or(0.0) / 1000.0;
m.ascent = if a > 0.3 { a.min(1.5) } else { 0.9 };
}
if descent.is_some() || subtype != b"Type3" {
let de = descent.unwrap_or(0.0) / 1000.0;
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.find(code).map_or(0, |(lo, _, c)| c.saturating_add(code.saturating_sub(*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.find(cid).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 {
// A font with no /Widths is one of the standard 14; use its published metrics.
// Codes the font's encoding leaves undefined fall back to /MissingWidth.
Some(f) => match u8::try_from(self.std14_code(f, code)).map(|c| f.width(c)).unwrap_or(0.0) {
0.0 => self.default * self.scale,
w => w / 1000.0,
},
None => self.default * self.scale,
}
}
/// Where the standard-14 table, indexed by WinAnsi, holds the glyph `code` draws in this font:
/// its encoding (base and `/Differences`) names the glyph, so a remapped code is measured as
/// the glyph it now shows (#2894). Symbol and ZapfDingbats are indexed by their own encodings,
/// and a glyph WinAnsi has no code for keeps the code itself.
fn std14_code(&self, face: crate::Std14, code: u32) -> u32 {
if matches!(face, crate::Std14::Symbol | crate::Std14::ZapfDingbats) {
return code;
}
self.unicode.encoding.get(&code).and_then(|t| only_char(t)).and_then(crate::win_ansi_byte).map_or(code, u32::from)
}
/// 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.text_of(c)).collect()
}
/// The Unicode text of one code, if known.
#[inline]
pub fn text_of(&self, code: u32) -> Option<Cow<'_, str>> {
self.unicode.text(code)
}
/// Whether the font has a glyph for `code` (subset fonts lack the glyphs they don't use).
/// Only asked for codes the font's Unicode mapping gives: a subset's `ToUnicode` lists the
/// glyphs it kept, while its `/W` leaves out those of the default width (#1443).
fn has_glyph(&self, code: u32) -> bool {
if self.composite {
let cid = self.cid(code);
// CID 0 is `.notdef` (an unmapped code lands there too), whatever ToUnicode says: Word
// maps it to a space and gives it a width, and typing with it draws boxes (#3223).
if cid == 0 {
return false;
}
// A CID /W leaves out takes /DW, and Skia (Chrome) leaves out every CID of that width: for
// those, only the subset's program says whether the glyph is there.
return self.cid_widths.contains_key(&cid)
|| self.cid_ranges.find(cid).is_some()
|| !self.subset
|| self.outlined_cids.get(cid as usize / 64).is_some_and(|w| (*w >> (cid % 64)) & 1 == 1)
// No readable TrueType program to ask (e.g. CFF or missing): an explicit /DW
// still says /W may leave out glyphs of that width (#1443).
|| (self.outlined_cids.is_empty() && self.explicit_default);
}
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,
}
}
/// Whether `code` shows exactly `c` and has a glyph for it.
fn shows(&self, code: u32, c: char) -> bool {
self.unicode.text(code).is_some_and(|t| t.chars().eq(std::iter::once(c))) && self.has_glyph(code)
}
/// 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>> {
// The lowest code showing each wanted character. Explicit entries and the encoding are
// checked one by one; each range is asked for the wanted characters it can show.
let wanted: HashSet<char> = text.chars().collect();
let mut reverse: HashMap<char, u32> = HashMap::new();
for &ch in &wanted {
if let Some(&code) = self.explicit_reverse.get(&ch) {
keep(&mut reverse, ch, code);
}
}
for range in &self.unicode.ranges {
for &c in &wanted {
if let Some(code) = range.first_code(c, |m| self.shows(m, c)) {
keep(&mut reverse, c, code);
}
}
}
let mut out = Vec::with_capacity(text.len() * self.code_len);
for ch in text.chars() {
let code = *reverse.get(&ch)?;
let bytes = code.to_be_bytes();
out.extend_from_slice(&bytes[4 - self.code_len.clamp(1, 4)..]);
}
Some(out)
}
}
/// Most UTF-16 units kept from one ToUnicode destination: the CMap format allows destinations of up
/// to 512 bytes, and every glyph a sequential range covers builds its text from the whole of one.
const MAX_DESTINATION_UNITS: usize = 256;
/// The UTF-16 code units of CMap hex bytes, at most [`MAX_DESTINATION_UNITS`] of them.
fn code_units(b: &[u8]) -> Vec<u16> {
b.chunks(2).take(MAX_DESTINATION_UNITS).map(|c| u16::from_be_bytes([c[0], *c.get(1).unwrap_or(&0)])).collect()
}
/// UTF-16BE (with surrogates) from CMap hex bytes.
fn utf16(b: &[u8]) -> String {
String::from_utf16_lossy(&code_units(b))
}
/// The character `s` is made of, if it is exactly one.
fn only_char(s: &str) -> Option<char> {
let mut chars = s.chars();
let c = chars.next()?;
chars.next().is_none().then_some(c)
}
/// Keeps the lowest code found for `c`.
fn keep(reverse: &mut HashMap<char, u32>, c: char, code: u32) {
reverse.entry(c).and_modify(|r| *r = (*r).min(code)).or_insert(code);
}
/// Spends `cost` of the budget; `false` when less is left.
fn spend(budget: &mut usize, cost: usize) -> bool {
let Some(left) = budget.checked_sub(cost) else { return false };
*budget = left;
true
}
/// Adds an explicit mapping; `false` once the budget is spent.
fn add_explicit(out: &mut Unicode, budget: &mut usize, seq: &mut u32, code: u32, text: String) -> bool {
if !spend(budget, 1) {
return false;
}
*seq = seq.saturating_add(1);
out.explicit.insert(code, (*seq, text));
true
}
/// Adds a sequential range, which covers at least two codes and so costs two; `false` once the budget is spent.
fn add_range(out: &mut Unicode, budget: &mut usize, seq: &mut u32, lo: u32, hi: u32, units: Box<[u16]>) -> bool {
if !spend(budget, 2) {
return false;
}
*seq = seq.saturating_add(1);
out.ranges.push(BfRange { lo, hi, seq: *seq, units });
true
}
#[cfg(test)]
thread_local! {
/// Code span records read by CID and width lookups, for the tests that keep a lookup logarithmic.
static SPAN_READS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
/// Counts one span record read (tests only).
#[inline]
fn count_span_reads() {
#[cfg(test)]
SPAN_READS.with(|c| c.set(c.get().saturating_add(1)));
}
/// Code spans `(lo, hi, value)` in declaration order, where the first-declared span covering a code
/// wins, indexed so that a lookup is one binary search.
#[derive(Clone, Debug, Default, PartialEq)]
struct Spans<T> {
spans: Vec<(u32, u32, T)>,
/// The span covering each stretch of codes; see [`stretches`].
cover: Vec<(u64, Option<usize>)>,
}
impl<T> Spans<T> {
fn new(spans: Vec<(u32, u32, T)>) -> Self {
let bounds: Vec<(u32, u32)> = spans.iter().map(|s| (s.0, s.1)).collect();
Spans { cover: stretches(&bounds, false), spans }
}
fn is_empty(&self) -> bool {
self.spans.is_empty()
}
/// The first-declared span covering `code`.
fn find(&self, code: u32) -> Option<&(u32, u32, T)> {
let step = self.cover.partition_point(|&(start, _)| {
count_span_reads();
start <= u64::from(code)
});
let &(_, record) = self.cover.get(step.checked_sub(1)?)?;
self.spans.get(record?)
}
}
/// For each stretch of codes, from its first code on, the latest-declared range covering it (`None`
/// where no range does), sorted by code; a lookup is one binary search over it.
fn cover_index(ranges: &[BfRange]) -> Vec<(u64, Option<usize>)> {
let spans: Vec<(u32, u32)> = ranges.iter().map(|r| (r.lo, r.hi)).collect();
stretches(&spans, true)
}
/// For each stretch of codes, from its first code on, the span `(lo, hi)` covering it (`None` where
/// none does), sorted by code: the latest-declared one when `later_wins`, else the first-declared.
/// At most `u32::MAX` spans are indexed.
fn stretches(spans: &[(u32, u32)], later_wins: bool) -> Vec<(u64, Option<usize>)> {
// Heap keys, largest for the winning record; the mapping is its own inverse.
let key = |record: u32| if later_wins { record } else { !record };
// Openings (first code, record) and closings (one past the last code), each in code order. The cover
// can only change at one of their codes, so the sweep visits those codes in order.
let mut opens: Vec<(u32, u32)> = spans.iter().zip(0..u32::MAX).map(|(&(lo, _), i)| (lo, i)).collect();
opens.sort_unstable();
let mut closes: Vec<u64> = spans.iter().take(opens.len()).map(|&(_, hi)| u64::from(hi) + 1).collect();
closes.sort_unstable();
// Keys of the records that have opened and not yet closed; the top is the winning one.
let mut open: BinaryHeap<u32> = BinaryHeap::new();
let mut cover: Vec<(u64, Option<usize>)> = Vec::new();
let (mut next_open, mut next_close) = (0, 0);
loop {
let p = match (opens.get(next_open), closes.get(next_close)) {
(Some(&(lo, _)), Some(&end)) => u64::from(lo).min(end),
(Some(&(lo, _)), None) => u64::from(lo),
(None, Some(&end)) => end,
(None, None) => break,
};
while let Some(&(lo, record)) = opens.get(next_open)
&& u64::from(lo) <= p
{
open.push(key(record));
next_open += 1;
}
while closes.get(next_close).is_some_and(|&end| end <= p) {
next_close += 1;
}
while open.peek().is_some_and(|&top| spans.get(key(top) as usize).is_none_or(|&(_, hi)| u64::from(hi) < p)) {
open.pop();
}
let winner = open.peek().map(|&top| key(top) as usize);
if cover.last().map(|&(_, w)| w) != Some(winner) {
cover.push((p, winner));
}
}
cover
}
/// One bit for each code below 65 536 in `codes`; empty when there is none.
/// Sized to the highest such code, so a font with a few low codes costs a few words, not 8 KB.
fn bit_set_below_64k(codes: impl Iterator<Item = u32>) -> Vec<u64> {
let low: Vec<u32> = codes.filter(|&code| code < 0x1_0000).collect();
let Some(&max) = low.iter().max() else { return Vec::new() };
let mut bits = vec![0u64; max as usize / 64 + 1];
for code in low {
if let Some(word) = bits.get_mut(code as usize / 64) {
*word |= 1u64 << (code % 64);
}
}
bits
}
/// The latest-declared range covering each code below 65 536, read off the stretches of `cover`; empty
/// when no range covers such a code. The stretches are disjoint, so this takes at most 65 536 steps
/// however many ranges there are.
///
/// The table ends with the last covered code: a code past it has no covering range, and a font whose
/// ranges cover a few low codes costs that many entries, not a fixed 256 KB.
fn owners_below_64k(cover: &[(u64, Option<usize>)]) -> Vec<u32> {
let stretch_end = |i: usize| cover.get(i + 1).map_or(0x1_0000, |&(next, _)| next.min(0x1_0000));
let len = cover
.iter()
.enumerate()
.filter(|&(_, &(start, winner))| winner.is_some() && start < 0x1_0000)
.map(|(i, _)| stretch_end(i))
.max()
.unwrap_or(0);
let mut owner = vec![u32::MAX; usize::try_from(len).unwrap_or(0)];
for (i, &(start, winner)) in cover.iter().enumerate() {
let Some(record) = winner else { continue };
// Record indices are below MAX_CMAP_ENTRIES, which fits in a u32.
let record = record as u32;
for code in start..stretch_end(i) {
if let Some(slot) = owner.get_mut(code as usize) {
*slot = record;
}
}
}
owner
}
/// `bfchar`/`bfrange` entries of a ToUnicode CMap, at most [`MAX_CMAP_ENTRIES`] mappings of them.
fn parse_to_unicode(data: &[u8], out: &mut Unicode) {
parse_to_unicode_within(data, out, MAX_CMAP_ENTRIES);
}
/// [`parse_to_unicode`] with `budget` explicit entries, so tests can use a small one.
fn parse_to_unicode_within(data: &[u8], out: &mut Unicode, mut budget: usize) {
let mut seq = 0u32;
let mut toks = Lexer::new(data);
'scan: while let Some(t) = toks.next() {
match t {
Tok::Word(b"beginbfchar") => {
while let Some([c, u]) = entry(&mut toks, b"endbfchar") {
if let (Tok::Hex(Some(c)), Tok::Hex(Some(u))) = (c, u) {
let added = add_explicit(out, &mut budget, &mut seq, be(&c), utf16(&u));
if !added {
break 'scan;
}
}
}
}
Tok::Word(b"beginbfrange") => {
while let Some([lo, hi, dst]) = entry(&mut toks, b"endbfrange") {
let (Tok::Hex(Some(lo)), Tok::Hex(Some(hi))) = (lo, hi) else { break };
let (lo, hi) = (be(&lo), be(&hi));
match dst {
// One destination per code, from `lo` up to `hi`.
Tok::Open => {
let mut code = (hi >= lo).then_some(lo);
for t in toks.by_ref() {
let Some(c) = code else {
if t == Tok::Close {
break;
}
continue;
};
match t {
Tok::Close => break,
Tok::Hex(Some(u)) => {
let added = add_explicit(out, &mut budget, &mut seq, c, utf16(&u));
if !added {
break 'scan;
}
}
// A bad entry still stands for its code.
_ => {}
}
code = c.checked_add(1).filter(|c| *c <= hi);
}
}
Tok::Hex(Some(u)) if hi >= lo && hi - lo < 65536 => {
let added = if lo == hi {
add_explicit(out, &mut budget, &mut seq, lo, utf16(&u))
} else {
add_range(out, &mut budget, &mut seq, lo, hi, code_units(&u).into_boxed_slice())
};
if !added {
break 'scan;
}
}
_ => {}
}
}
}
_ => {}
}
}
out.cover = cover_index(&out.ranges);
out.low = owners_below_64k(&out.cover);
out.explicit_low = bit_set_below_64k(out.explicit.keys().copied());
}
/// A glyph name's text, by the Adobe Glyph List *algorithm* — the published naming rules, not
/// the AGL data table, which this crate does not hold.
///
/// A name is cut at its first `.`, because a suffix marks a variant of the same character rather
/// than a different one (`a.sc` is small-capital `a`, `one.oldstyle` is still `1`). What remains
/// is split on `_` into components, so a ligature name yields several characters (`f_i` is "fi").
/// Each component is read as an Annex D name, as `uniXXXX` with one or more four-hex-digit values,
/// or as `uXXXX` through `uXXXXXX`.
///
/// Returns `None` rather than a partial answer: a name is either fully understood or not, and half
/// a ligature would be worse than admitting we cannot read it.
pub fn glyph_text(name: &str) -> Option<String> {
let stem = name.split('.').next().filter(|s| !s.is_empty())?;
let mut out = String::new();
for part in stem.split('_') {
out.push_str(&glyph_component(part)?);
}
Some(out).filter(|s| !s.is_empty())
}
/// One `_`-separated component of a glyph name.
fn glyph_component(part: &str) -> Option<String> {
if let Ok(i) = crate::encodings::NAMES.binary_search_by(|(n, _)| (*n).cmp(part)) {
return char::from_u32(crate::encodings::NAMES[i].1).map(String::from);
}
// `from_str_radix` accepts a leading `+`, which is not a glyph name, so the digits are checked.
let hexish = |h: &str| !h.is_empty() && h.bytes().all(|b| b.is_ascii_hexdigit());
if let Some(hex) = part.strip_prefix("uni").filter(|h| h.len() >= 4 && h.len() % 4 == 0 && hexish(h)) {
let mut s = String::new();
for i in (0..hex.len()).step_by(4) {
// A lone surrogate half is not a character; such a name is not usable text.
s.push(char::from_u32(u32::from_str_radix(hex.get(i..i.saturating_add(4))?, 16).ok()?)?);
}
return Some(s);
}
let hex = part.strip_prefix('u').filter(|h| (4..=6).contains(&h.len()) && hexish(h))?;
u32::from_str_radix(hex, 16).ok().and_then(char::from_u32).map(String::from)
}
/// [`glyph_text`] for names that stand for exactly one character.
pub fn glyph_unicode(name: &str) -> Option<char> {
let mut chars = glyph_text(name)?.chars().collect::<Vec<_>>().into_iter();
chars.next().filter(|_| chars.len() == 0)
}
/// Code → Unicode for a font: its ToUnicode CMap (for a composite font, also the name
/// `/Identity-H` or `/Identity-V`, read as code = Unicode), else (simple fonts) its encoding.
fn unicode_map(doc: &Document, font: &Dict, composite: bool) -> Unicode {
let mut out = Unicode::default();
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,
};
// Symbol and ZapfDingbats carry their own built-in encodings, which are not Latin
// (ISO 32000-2 9.6.6.1): Symbol code 0x61 is `alpha`, not `a`. We do not hold those two
// tables, so with no base encoding named they get no implicit mapping at all. Reporting
// nothing is honest; reporting Latin letters silently corrupts text extraction, find,
// copy and — because `redact` matches on this map — redaction.
//
// Deliberately keyed on the *name*, not on the `/FontDescriptor` symbolic flag. A great
// many ordinary Latin TrueType subsets set that flag and still rely on a Latin encoding,
// so suppressing on the flag would lose real text on a large share of real documents.
// An explicitly named base encoding is a deliberate statement and is always honoured,
// even on Symbol.
let symbolic_builtin = matches!(
font.name(b"BaseFont").map(|b| crate::Std14::from_base_font(&String::from_utf8_lossy(b))),
Some(Some(crate::Std14::Symbol | crate::Std14::ZapfDingbats))
);
let table: Option<&[u32; 256]> = match base_name.as_deref() {
Some(b"WinAnsiEncoding") => Some(&crate::encodings::WIN_ANSI),
Some(b"MacRomanEncoding") => Some(&crate::encodings::MAC_ROMAN),
Some(b"StandardEncoding") => Some(&crate::encodings::STANDARD),
// MacExpert is small caps, oldstyle figures and fractions, not Latin at these codes,
// and we hold no table for it either.
Some(b"MacExpertEncoding") => None,
_ if symbolic_builtin => None,
// Standard is the default for a simple font that names no base encoding.
_ => Some(&crate::encodings::STANDARD),
};
for (code, u) in table.into_iter().flatten().enumerate() {
if let Some(c) = char::from_u32(*u).filter(|_| *u != 0) {
out.encoding.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 {
// Out-of-range codes map nothing a simple font can show.
Object::Int(n) => code = u32::try_from((*n).max(0)).unwrap_or(u32::MAX),
Object::Name(n) => {
// A name we cannot read removes any inherited meaning for that code,
// rather than leaving the base encoding's letter standing in for it.
match glyph_text(&String::from_utf8_lossy(n)) {
Some(t) => out.encoding.insert(code, t),
None => out.encoding.remove(&code),
};
code = code.saturating_add(1);
}
_ => {}
}
}
}
}
match font.get(b"ToUnicode").map(|t| doc.resolve(t)).as_deref() {
Some(Object::Stream(s)) => {
if let Ok(data) = s.decoded() {
parse_to_unicode(&data, &mut out);
}
}
// `/ToUnicode /Identity-H` is not a stream, as ISO 32000-2 §9.10.3 requires, but CAD
// exporters (WSCAD among them) write it for composite fonts whose two-byte codes are the
// UTF-16 code units. Acrobat shows them as code = Unicode, and so does the renderer's
// text layer; without it the text could be seen but not edited or found. The same rule
// (code = Unicode, no controls or lone surrogates) is in the vendored renderer,
// `Type0Font::char_code_to_unicode` in vendor/hayro-interpret/src/font/cid.rs: keep both alike.
Some(Object::Name(n)) if composite && matches!(n.as_slice(), b"Identity-H" | b"Identity-V") => {
parse_to_unicode(&identity_to_unicode(), &mut out);
}
_ => {}
}
out
}
/// A ToUnicode CMap mapping each two-byte code to the same UTF-16 code unit, leaving out the
/// surrogates (which are not characters on their own) and the controls (C0, DEL and C1, as the
/// renderer's text layer does). No range crosses a last-byte boundary (§9.10.3).
fn identity_to_unicode() -> Vec<u8> {
let mut ranges = vec![(0x20u32, 0x7Eu32), (0xA0, 0xFF)];
ranges.extend((0x01u32..=0xFF).filter(|h| !(0xD8..=0xDF).contains(h)).map(|h| (h << 8, (h << 8) | 0xFF)));
let mut cmap = String::from("1 begincodespacerange <0000> <FFFF> endcodespacerange\n");
// At most 100 entries per section (§9.7.5.2 limits for CMap files).
for chunk in ranges.chunks(100) {
cmap.push_str(&format!("{} beginbfrange\n", chunk.len()));
for (lo, hi) in chunk {
cmap.push_str(&format!("<{lo:04X}> <{hi:04X}> <{lo:04X}>\n"));
}
cmap.push_str("endbfrange\n");
}
cmap.into_bytes()
}
#[cfg(test)]
mod tests {
use pdfcraft_cos::{Dict, Document, Object, Stream};
use super::*;
/// Span records read while `f` runs.
fn span_reads(f: impl FnOnce()) -> usize {
SPAN_READS.with(|c| c.set(0));
f();
SPAN_READS.with(|c| c.get())
}
/// A composite font with `encoding` as its CMap, `w` as its widths and `to_unicode`.
fn composite(doc: &mut Document, encoding: &str, w: Vec<Object>, to_unicode: &str) -> Dict {
let mut desc = Dict::new();
desc.set(b"Subtype".to_vec(), Object::name("CIDFontType2"));
desc.set(b"W".to_vec(), Object::Array(w));
let enc = Object::Ref(doc.add(Object::Stream(Stream::flate(Dict::new(), encoding.as_bytes()))));
let tu = Object::Ref(doc.add(Object::Stream(Stream::flate(Dict::new(), to_unicode.as_bytes()))));
font(
doc,
vec![
("Subtype", Object::name("Type0")),
("Encoding", enc),
("DescendantFonts", Object::Array(vec![Object::Dict(desc)])),
("ToUnicode", tu),
],
)
}
/// Many `/W` ranges that leave a code out cost a glyph of it a binary search, not a scan of them all.
#[test]
fn widths_of_a_code_no_range_covers_take_few_steps() {
let mut doc = Document::new_empty();
let w: Vec<Object> = (1..=10_000i64).flat_map(|k| [Object::Int(2 * k), Object::Int(2 * k), Object::Int(500)]).collect();
let f = composite(&mut doc, "", w, "");
let m = Metrics::from_dict(&doc, &f);
assert_eq!(m.width(4), 0.5);
let reads = span_reads(|| {
for _ in 0..1000 {
assert_eq!(m.width(3), 1.0);
}
});
assert!(reads <= 1000 * 64, "{reads} span reads");
}
/// Where spans overlap, the first-declared one wins, as a scan in declaration order finds it.
#[test]
fn overlapping_spans_resolve_to_the_first_declared() {
let spans = Spans::new(vec![(0, 10, 'a'), (5, 20, 'b'), (3, 3, 'c'), (30, 25, 'd'), (u32::MAX, u32::MAX, 'e')]);
let found = |code| spans.find(code).map(|s| s.2);
let want = [(0, Some('a')), (3, Some('a')), (10, Some('a')), (11, Some('b')), (20, Some('b')), (21, None), (27, None), (u32::MAX, Some('e'))];
for (code, value) in want {
assert_eq!(found(code), value, "code {code}");
}
}
/// Checking the glyph of every explicit ToUnicode entry looks its code up once in the CID map.
#[test]
fn many_unicode_entries_look_up_their_cids_in_few_steps() {
let n = 2000;
let cidchars: String = (0..n).map(|k| format!("<{:04X}> {} ", 0x8000 + k, k + 1)).collect();
let encoding = format!("1 begincodespacerange <0000> <FFFF> endcodespacerange {n} begincidchar {cidchars} endcidchar");
let bfchars: String = (0..n).map(|k| format!("<{:04X}> <{:04X}> ", 0x1000 + k, 0x4E00 + k)).collect();
let to_unicode = format!("{n} beginbfchar {bfchars} endbfchar");
let mut doc = Document::new_empty();
let f = composite(&mut doc, &encoding, Vec::new(), &to_unicode);
let reads = span_reads(|| {
Metrics::from_dict(&doc, &f);
});
assert!(reads <= n * 64, "{reads} span reads");
}
/// Every glyph of a sequential range builds its text from the destination, so a long one is cut
/// to the CMap limit once, when it is read.
#[test]
fn unicode_destinations_are_cut_to_the_cmap_limit() {
let long = "0041".repeat(4096);
let cmap = format!("2 beginbfrange <0010> <0020> <{long}> <0030> <0030> [<{long}>] endbfrange 1 beginbfchar <0040> <{long}> endbfchar");
let mut u = Unicode::default();
parse_to_unicode(cmap.as_bytes(), &mut u);
for code in [0x10, 0x20, 0x30, 0x40] {
let text = u.text(code).unwrap_or_default();
assert!(!text.is_empty() && text.len() <= MAX_DESTINATION_UNITS * 3, "code {code:#x}: {} bytes", text.len());
}
}
/// A font whose ranges cover a few low codes keeps tables that size, not a fixed 64k-entry one.
#[test]
fn low_code_tables_end_with_the_last_covered_code() {
// One range over codes 0x20..=0x21, then nothing.
let cover = [(0x20u64, Some(0usize)), (0x22, None)];
let owner = super::owners_below_64k(&cover);
assert_eq!(owner.len(), 0x22);
assert_eq!((owner[0x1F], owner[0x20], owner[0x21]), (u32::MAX, 0, 0));
assert!(super::owners_below_64k(&[(0x2_0000, Some(0))]).is_empty(), "no code below 65 536");
let bits = super::bit_set_below_64k([3u32, 130, 0x2_0000].into_iter());
assert_eq!(bits.len(), 3);
assert_eq!((bits[0], bits[2]), (1 << 3, 1 << 2));
assert!(super::bit_set_below_64k([0x1_0000u32].into_iter()).is_empty());
}
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 embedded_editor_fonts_are_validated_without_installing_or_exporting() {
use pdfcraft_cos::Stream;
let mut doc = Document::new_empty();
let bytes = include_bytes!("../../../assets/fonts/Inter-Regular.ttf");
let program = doc.add(Object::Stream(Stream::flate(Dict::new(), bytes)));
let mut descriptor = Dict::new();
descriptor.set(b"FontFile2".to_vec(), Object::Ref(program));
let simple = font(&mut doc, vec![("FontDescriptor", Object::Dict(descriptor.clone()))]);
assert_eq!(embedded_sfnt(&doc, &simple).unwrap(), bytes);
let composite = font(&mut doc, vec![("DescendantFonts", Object::Array(vec![Object::Dict(simple.clone())]))]);
assert_eq!(embedded_sfnt(&doc, &composite).unwrap(), bytes);
let broken = doc.add(Object::Stream(Stream::flate(Dict::new(), b"broken font")));
descriptor.set(b"FontFile2".to_vec(), Object::Ref(broken));
let broken = font(&mut doc, vec![("FontDescriptor", Object::Dict(descriptor))]);
assert!(embedded_sfnt(&doc, &broken).is_none());
assert!(embedded_sfnt(&doc, &Dict::new()).is_none());
}
/// #2130: with no `/Widths`, an embedded program's advances win over a guess from the name
/// ("Mono" would mean Courier, 600 units for every glyph).
#[test]
fn a_font_without_widths_measures_with_its_embedded_program() {
use pdfcraft_cos::Stream;
use skrifa::MetadataProvider as _;
let mut doc = Document::new_empty();
let bytes = include_bytes!("../../../assets/fonts/Inter-Regular.ttf");
let face = skrifa::FontRef::new(bytes).unwrap();
let (size, location) = (skrifa::instance::Size::unscaled(), skrifa::instance::LocationRef::default());
let upem = f64::from(face.metrics(size, location).units_per_em);
let advance = |c: char| f64::from(face.glyph_metrics(size, location).advance_width(face.charmap().map(c).unwrap()).unwrap()) / upem;
let program = doc.add(Object::Stream(Stream::flate(Dict::new(), bytes)));
let mut descriptor = Dict::new();
descriptor.set(b"FontFile2".to_vec(), Object::Ref(program));
let f = font(
&mut doc,
vec![
("Subtype", Object::name("TrueType")),
("BaseFont", Object::name("HouseMono")),
("Encoding", Object::name("WinAnsiEncoding")),
("FontDescriptor", Object::Dict(descriptor)),
],
);
let m = Metrics::from_dict(&doc, &f);
for c in ['i', 'l', 'W', ' '] {
assert!((m.width(u32::from(c)) - advance(c)).abs() < 1e-9, "{c}: {} vs {}", m.width(u32::from(c)), advance(c));
}
assert!(m.width(u32::from('i')) < 0.4, "not Courier's 0.6");
}
#[test]
fn mac_subset_cmap_uses_pdf_unicode_without_changing_source_program() {
use skrifa::{MetadataProvider as _, raw::TableProvider as _};
let original = include_bytes!("../../../assets/fonts/Inter-Regular.ttf");
let face = skrifa::FontRef::new(original).unwrap();
let expected = face.charmap().map('Ω').unwrap();
let mut subset = original.to_vec();
let count = u16::from_be_bytes(subset[4..6].try_into().unwrap()) as usize;
let entry = (0..count).map(|i| 12 + i * 16).find(|i| &subset[*i..*i + 4] == b"cmap").unwrap();
let mut cmap = vec![0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 12];
cmap.extend_from_slice(&[0, 6, 0, 12, 0, 0, 0, 65, 0, 1]);
cmap.extend_from_slice(&u16::try_from(expected.to_u32()).unwrap().to_be_bytes());
let offset = subset.len() as u32;
subset[entry + 8..entry + 12].copy_from_slice(&offset.to_be_bytes());
subset[entry + 12..entry + 16].copy_from_slice(&(cmap.len() as u32).to_be_bytes());
subset.extend_from_slice(&cmap);
let face = skrifa::FontRef::new(&subset).unwrap();
assert!(!face.charmap().has_map(), "egui cannot directly use Mac format 6");
assert_eq!(face.cmap().unwrap().map_codepoint(65_u32), Some(expected));
let mut doc = Document::new_empty();
let program = doc.add(Object::Stream(Stream::flate(Dict::new(), &subset)));
let mut descriptor = Dict::new();
descriptor.set(b"FontFile2".to_vec(), Object::Ref(program));
let unicode = doc.add(Object::Stream(Stream::flate(Dict::new(), b"1 beginbfchar <41> <03A9> endbfchar")));
let source = font(&mut doc, vec![("FontDescriptor", Object::Dict(descriptor)), ("ToUnicode", Object::Ref(unicode))]);
let editor = embedded_sfnt(&doc, &source).unwrap();
let face = skrifa::FontRef::new(&editor).unwrap();
assert_eq!(face.charmap().map('Ω'), Some(expected), "Unicode selects the original glyph");
let Object::Stream(source) = doc.resolve(&Object::Ref(program)).as_ref().clone() else {
panic!("source stream");
};
assert_eq!(source.decoded().unwrap(), subset, "only an in-memory editor copy is adapted");
let mut broken = vec![0; 16];
assert!(append_editor_cmap(&mut broken, &std::collections::BTreeMap::from([(65, 1)])).is_none());
}
fn metrics_with_flags(flags: Option<Object>) -> Metrics {
let mut doc = Document::new_empty();
let mut descriptor = Dict::new();
if let Some(flags) = flags {
descriptor.set(b"Flags".to_vec(), flags);
}
let font = font(&mut doc, vec![("Subtype", Object::name("TrueType")), ("FontDescriptor", Object::Dict(descriptor))]);
Metrics::from_dict(&doc, &font)
}
#[test]
fn font_descriptor_flags_stay_within_the_u32_domain() {
let valid = metrics_with_flags(Some(Object::Int(64 | 262_144)));
assert!(valid.italic && valid.bold);
for flags in [Some(Object::Int(-1)), Some(Object::Int(4_294_967_296)), Some(Object::Bool(true)), None] {
let metrics = metrics_with_flags(flags);
assert!(!metrics.italic && !metrics.bold);
}
}
#[test]
fn type3_descriptor_names_the_font_without_changing_its_metrics() {
let mut doc = Document::new_empty();
let mut f = font(
&mut doc,
vec![
("Subtype", Object::name("Type3")),
(
"FontMatrix",
Object::Array(vec![Object::Real(0.001), Object::Int(0), Object::Int(0), Object::Real(0.001), Object::Int(0), Object::Int(0)]),
),
("FontBBox", Object::Array(vec![Object::Int(0), Object::Int(-300), Object::Int(1000), Object::Int(1000)])),
],
);
let before = Metrics::from_dict(&doc, &f);
let mut descriptor = Dict::new();
descriptor.set(b"Type".to_vec(), Object::name("FontDescriptor"));
descriptor.set(b"FontName".to_vec(), Object::name("ExampleMincho-Regular"));
descriptor.set(b"Flags".to_vec(), Object::Int(6));
descriptor.set(b"ItalicAngle".to_vec(), Object::Int(0));
let reference = doc.add(Object::Dict(descriptor));
f.set(b"FontDescriptor".to_vec(), Object::Ref(reference));
let after = Metrics::from_dict(&doc, &f);
assert_eq!(after.base_font, "ExampleMincho-Regular");
assert_eq!((after.ascent, after.descent, after.scale), (before.ascent, before.descent, before.scale));
assert!(!after.bold && !after.italic && !after.subset);
f.set(b"BaseFont".to_vec(), Object::name("ExplicitName"));
assert_eq!(Metrics::from_dict(&doc, &f).base_font, "ExplicitName", "an explicit BaseFont takes precedence");
}
#[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"[..]));
}
/// A standard-14 font with no `/Widths` is measured by the glyph each code draws, not by the
/// glyph WinAnsi gives that code (#2894); `/Widths`, when present, still go by code.
#[test]
fn standard_fonts_without_widths_measure_the_glyph_their_differences_name() {
let mut doc = Document::new_empty();
let mut enc = Dict::new();
enc.set(b"BaseEncoding".to_vec(), Object::name("WinAnsiEncoding"));
let diffs =
vec![Object::Int(65), Object::name("W"), Object::Int(87), Object::name("A"), Object::name("uni03A9"), Object::name("not.a.glyph")];
enc.set(b"Differences".to_vec(), Object::Array(diffs));
let f = font(
&mut doc,
vec![("Subtype", Object::name("Type1")), ("BaseFont", Object::name("Helvetica")), ("Encoding", Object::Dict(enc.clone()))],
);
let m = Metrics::from_dict(&doc, &f);
assert_eq!((m.width(65), m.width(87)), (0.944, 0.667), "A and W swap their widths");
// Omega has no WinAnsi code and the last name reads as nothing: both keep their code's width.
assert_eq!((m.width(88), m.width(89)), (0.667, 0.667));
let f = font(
&mut doc,
vec![
("Subtype", Object::name("Type1")),
("BaseFont", Object::name("Helvetica")),
("FirstChar", Object::Int(87)),
("Widths", Object::Array(vec![Object::Int(500)])),
("Encoding", Object::Dict(enc)),
],
);
assert_eq!(Metrics::from_dict(&doc, &f).width(87), 0.5, "/Widths go by code");
}
#[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);
}
/// Word's Type 0 subsets map CID 0 as well as the real space glyph to U+0020 and give CID 0 a
/// width; CID 0 is `.notdef`, so a typed space takes the real one (#3223).
#[test]
fn composite_fonts_never_encode_with_cid_0() {
let mut doc = Document::new_empty();
let cmap = b"/CIDInit /ProcSet findresource begin 1 begincodespacerange <0000> <FFFF> endcodespacerange \
1 beginbfrange <0000> <0002> [<0020> <004D> <00E1>] endbfrange \
1 beginbfrange <0005> <0007> [<0065> <0072> <0020>] 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"));
let widths = [889, 444, 389, 278, 444, 333, 250].map(Object::Int).to_vec();
desc.set(b"W".to_vec(), Object::Array(vec![Object::Int(0), Object::Array(vec![Object::Int(778)]), Object::Int(1), Object::Array(widths)]));
let f = font(
&mut doc,
vec![
("Subtype", Object::name("Type0")),
("BaseFont", Object::name("BCDEEE+TimesNewRomanPSMT")),
("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.encode("Me er").as_deref(), Some(&[0, 1, 0, 5, 0, 7, 0, 5, 0, 6][..]));
assert_eq!(m.decode(&[0, 1, 0, 7, 0, 2]), "M á");
}
/// WSCAD writes `/ToUnicode /Identity-H` (a name, not a CMap stream) on composite fonts whose
/// codes are UTF-16 code units: the codes read as the same Unicode, as in Acrobat. Controls
/// and lone surrogates map to nothing, and a simple font's name is not a ToUnicode map.
#[test]
fn identity_to_unicode_names_read_codes_as_unicode() {
let doc = Document::new_empty();
for name in ["Identity-H", "Identity-V"] {
let f = font(
&mut Document::new_empty(),
vec![
("Subtype", Object::name("Type0")),
("BaseFont", Object::name("RobotoMono-Regular")),
("Encoding", Object::name("Identity-H")),
("DescendantFonts", Object::Array(vec![Object::Dict(Dict::new())])),
("ToUnicode", Object::name(name)),
],
);
let m = Metrics::from_dict(&doc, &f);
assert_eq!(m.decode(&[0, b'Z', 0, 0xFC, 0, b'r', 0x20, 0xAC, 0x4E, 0x2D]), "Zür€中", "{name}");
assert_eq!(m.text_of(0x0001), None);
assert_eq!(m.text_of(0x007F), None, "DEL is a control");
assert_eq!(m.text_of(0x0085), None, "a C1 control");
assert_eq!(m.text_of(0x00A0).as_deref(), Some("\u{A0}"), "the range resumes after C1");
assert_eq!(m.text_of(0x007E).as_deref(), Some("~"));
assert_eq!(m.text_of(0xD800), None);
assert_eq!(m.text_of(0xFFFD).as_deref(), Some("\u{FFFD}"));
}
let simple = font(
&mut Document::new_empty(),
vec![("Subtype", Object::name("TrueType")), ("BaseFont", Object::name("Arial")), ("ToUnicode", Object::name("Identity-H"))],
);
let m = Metrics::from_dict(&doc, &simple);
assert_eq!(m.text_of(0x41).as_deref(), Some("A"), "the encoding still applies");
}
/// Chrome's subsets leave the glyphs of `/DW` width out of `/W` (#1443): "s" (CID 0x56) is
/// still in the font, but nothing is when the font gives no default width to point at.
#[test]
fn composite_subset_glyphs_of_the_default_width_are_present() {
let metrics = |dw: bool| {
let mut doc = Document::new_empty();
let cmap = b"/CIDInit /ProcSet findresource begin 1 begincodespacerange <0000> <FFFF> endcodespacerange \
1 beginbfrange <0055> <0057> <0072> 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"));
if dw {
desc.set(b"DW".to_vec(), Object::Int(500));
}
desc.set(
b"W".to_vec(),
Object::Array(vec![
Object::Int(0x55),
Object::Array(vec![Object::Int(333)]),
Object::Int(0x57),
Object::Array(vec![Object::Int(277)]),
]),
);
let f = font(
&mut doc,
vec![
("Subtype", Object::name("Type0")),
("BaseFont", Object::name("AAAAAA+LiberationSans")),
("Encoding", Object::name("Identity-H")),
("DescendantFonts", Object::Array(vec![Object::Dict(desc)])),
("ToUnicode", Object::Ref(tu)),
],
);
Metrics::from_dict(&doc, &f)
};
let chrome = metrics(true);
assert_eq!(chrome.encode("rst").as_deref(), Some(&[0, 0x55, 0, 0x56, 0, 0x57][..]));
assert_eq!(chrome.width(0x56), 0.5);
assert_eq!(chrome.encode("x"), None, "a character the ToUnicode map doesn't give");
assert_eq!(metrics(false).encode("s"), None);
}
/// A `/W` run that starts at the last CID stops there instead of overflowing onto CID 0.
#[test]
fn composite_width_run_past_the_last_cid_is_dropped() {
let widths = |first: i64| {
let mut doc = Document::new_empty();
let mut desc = Dict::new();
desc.set(b"Subtype".to_vec(), Object::name("CIDFontType2"));
desc.set(b"DW".to_vec(), Object::Int(1000));
desc.set(b"W".to_vec(), Object::Array(vec![Object::Int(first), Object::Array(vec![Object::Int(100), Object::Int(200)])]));
let f = font(
&mut doc,
vec![
("Subtype", Object::name("Type0")),
("Encoding", Object::name("Identity-H")),
("DescendantFonts", Object::Array(vec![Object::Dict(desc)])),
],
);
Metrics::from_dict(&doc, &f)
};
let ordinary = widths(0);
assert_eq!((ordinary.width(0), ordinary.width(1)), (0.1, 0.2));
let last = widths(i64::from(u32::MAX));
assert_eq!((last.width(0), last.width(u32::MAX)), (1.0, 0.1));
}
#[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);
}
fn to_unicode(cmap: &[u8]) -> HashMap<u32, String> {
let mut u = Unicode::default();
parse_to_unicode(cmap, &mut u);
let codes: Vec<u32> = u.explicit.keys().copied().chain(u.ranges.iter().flat_map(|r| r.lo..=r.hi)).collect();
codes.into_iter().filter_map(|c| u.text(c).map(|t| (c, t.into_owned()))).collect()
}
#[test]
fn cmap_tokens_need_no_space_between_them() {
// Hex strings, brackets and operators may touch; comments and literal strings are skipped.
let cmap = b"/CIDSystemInfo << /Registry (Adobe (x) \\) beginbfchar) >> def % beginbfchar <0001> <0058>\n\
2 beginbfchar<0003><0020><0010><00 E9>endbfchar \
1 beginbfrange<0024><0026><0041>endbfrange 1 beginbfrange<0030><0031>[<0048><0069>]endbfrange";
let map = to_unicode(cmap);
let mut got: Vec<_> = map.iter().map(|(c, u)| (*c, u.as_str())).collect();
got.sort_unstable();
assert_eq!(got, [(3, " "), (0x10, "é"), (0x24, "A"), (0x25, "B"), (0x26, "C"), (0x30, "H"), (0x31, "i")]);
let (spaces, cids) = parse_cmap(b"1 begincodespacerange<00><FF>endcodespacerange 1 begincidrange<20><7E>1 endcidrange");
assert_eq!(spaces, [(1, vec![0], vec![0xff])]);
assert_eq!(cids, [(0x20, 0x7e, 1)]);
}
#[test]
fn malformed_cmaps_never_panic() {
// Non-ASCII bytes inside hex strings once split a UTF-8 character and panicked.
for cmap in [
"1 beginbfchar <0é0> <0041> <AéB> <0042> endbfchar".as_bytes(),
"1 beginbfrange <0é0> <00é> <0041> endbfrange".as_bytes(),
b"1 beginbfchar <0001> <004",
b"1 beginbfchar <123> <0041> <0002> <zz> <0003> endbfchar",
b"1 beginbfrange <FFFF> <0001> [<0041>] endbfrange",
b"1 beginbfrange <0001> <0002> [<0041> (",
b"1 beginbfrange <0001>",
b"\xff\xfe beginbfchar \x80<\xc3\xa9> <0041> endbfchar %",
] {
to_unicode(cmap);
parse_cmap(cmap);
}
// Code counters at the top of the range stop instead of overflowing.
let map = to_unicode(b"1 beginbfrange <FFFFFFFE> <FFFFFFFF> [<0041> <0042> <0043>] endbfrange");
assert_eq!(map.len(), 2);
assert_eq!(map.get(&u32::MAX).map(String::as_str), Some("B"));
// A malformed array entry keeps its place, so the next one maps the next code.
let map = to_unicode(b"1 beginbfrange <0001> <0003> [<0041> <zz> <0043>] endbfrange");
assert_eq!((map.get(&2), map.get(&3).map(String::as_str)), (None, Some("C")));
let mut m = Metrics::fallback();
m.cid_map = Spans::new(parse_cmap(b"1 begincidrange <0000> <FFFF> 4294967295 endcidrange").1);
assert_eq!(m.cid(5), u32::MAX);
let mut doc = Document::new_empty();
let mut enc = Dict::new();
enc.set(b"Differences".to_vec(), Object::Array(vec![Object::Int(i64::MAX), Object::name("A"), Object::name("B")]));
let f = font(&mut doc, vec![("Subtype", Object::name("Type1")), ("Encoding", Object::Dict(enc))]);
assert_eq!(unicode_map(&doc, &f, false).text(u32::MAX).as_deref(), Some("B"));
}
#[test]
fn sequential_ranges_past_the_entry_cap_still_map() {
// 17 ranges of 65 536 codes: 1 114 112 codes from 0.4 KB of CMap, more than MAX_CMAP_ENTRIES
// (the first 16 ranges use up the whole cap). Each range maps its codes upward from 'A', so
// the last code of the last range, 0x100019, is 'A' + 0x19 = 'Z'.
let mut cmap = b"17 beginbfrange ".to_vec();
for k in 0..17u32 {
cmap.extend(format!("<{:08X}> <{:08X}> <0041> ", k << 16, (k << 16) | 0xffff).bytes());
}
cmap.extend(b"endbfrange");
let mut doc = Document::new_empty();
let tu = doc.add(Object::Stream(Stream::from_raw(Dict::new(), cmap)));
let f = font(
&mut doc,
vec![
("Subtype", Object::name("Type0")),
("BaseFont", Object::name("Wide")),
("Encoding", Object::name("Identity-H")),
("ToUnicode", Object::Ref(tu)),
],
);
let m = Metrics::from_dict(&doc, &f);
assert_eq!(m.text_of(0x0010_0019).as_deref(), Some("Z"));
}
#[test]
fn a_cmap_adds_at_most_max_entries() {
// 200 ranges of 65 536 codes would be 13 million codes from 4 KB of stream: 200 records.
let mut cmap = b"200 beginbfrange ".to_vec();
for k in 0..200u32 {
cmap.extend(format!("<{:08X}> <{:08X}> <0041> ", k << 16, (k << 16) | 0xffff).bytes());
}
cmap.extend(b"endbfrange");
let mut u = Unicode::default();
parse_to_unicode(&cmap, &mut u);
assert_eq!((u.ranges.len(), u.explicit.len()), (200, 0));
// Explicit entries are what the budget counts: 250 of the 300 here are kept.
let mut many = b"300 beginbfchar ".to_vec();
for k in 0..300u32 {
many.extend(format!("<{k:04X}> <{:04X}> ", 0x4E00 + k).bytes());
}
many.extend(b"endbfchar");
let mut u = Unicode::default();
parse_to_unicode_within(&many, &mut u, 250);
assert_eq!((u.explicit.len(), u.text(249).is_some(), u.text(250).is_some()), (250, true, false));
let mut many = b"begincidrange ".to_vec();
for k in 0..MAX_CMAP_RANGES + 10 {
many.extend(format!("<{k:08X}> <{k:08X}> 1 ").bytes());
}
assert_eq!(parse_cmap(&many).1.len(), MAX_CMAP_RANGES);
}
#[test]
fn a_range_is_one_record_however_many_codes() {
let mut u = Unicode::default();
parse_to_unicode(b"1 beginbfrange <0000> <FFFF> <0041> endbfrange", &mut u);
assert_eq!((u.ranges.len(), u.explicit.len()), (1, 0));
assert_eq!(u.text(0xFFFF).as_deref(), Some("@"), "0x41 + 0xFFFF wraps to U+0040");
}
#[test]
fn sequential_ranges_count_against_the_budget() {
// Range k maps codes k and k + 1 and costs two, so a budget of 250 keeps ranges 0 to 124.
let mut cmap = Vec::new();
for k in 0..300u32 {
cmap.extend(format!("1 beginbfrange <{k:08X}> <{:08X}> <0041> endbfrange ", k + 1).bytes());
}
let mut u = Unicode::default();
parse_to_unicode_within(&cmap, &mut u, 250);
assert_eq!((u.ranges.len(), u.text(125).is_some(), u.text(126).is_none()), (125, true, true));
}
#[test]
fn explicit_and_range_mappings_override_the_encoding() {
let mut doc = Document::new_empty();
let cmap = b"1 beginbfchar <41> <0058> endbfchar 1 beginbfrange <42> <43> <0059> endbfrange";
let tu = doc.add(Object::Stream(Stream::from_raw(Dict::new(), cmap.to_vec())));
let f = font(
&mut doc,
vec![
("Subtype", Object::name("Type1")),
("BaseFont", Object::name("Custom")),
("Encoding", Object::name("WinAnsiEncoding")),
("ToUnicode", Object::Ref(tu)),
],
);
let m = Metrics::from_dict(&doc, &f);
let shown: Vec<Option<String>> = (0x41..=0x44).map(|c| m.text_of(c).map(|t| t.into_owned())).collect();
assert_eq!(shown, [Some("X".to_string()), Some("Y".to_string()), Some("Z".to_string()), Some("D".to_string())]);
}
/// Deterministic input for the differential test below.
struct Gen(u64);
impl Gen {
fn below(&mut self, n: u32) -> u32 {
self.0 = self.0.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
((self.0 >> 33) % u64::from(n)) as u32
}
/// A destination: a unit from a pool of edge cases, a surrogate pair, or a lone high surrogate.
fn units(&mut self) -> Vec<u16> {
const POOL: [u16; 12] = [0x41, 0x42, 0xE9, 0x0, 0xFFFD, 0xFFFE, 0xFFFF, 0x100, 0x3042, 0xD800, 0xDC00, 0xDFFF];
match self.below(6) {
0 => vec![0xD835, 0xDC00 + self.below(0x400) as u16],
1 => vec![0xD800, 0x41],
_ => vec![POOL[self.below(POOL.len() as u32) as usize]],
}
}
}
fn hex(units: &[u16]) -> String {
units.iter().map(|u| format!("{u:04X}")).collect()
}
#[test]
fn lookups_match_the_expanded_mappings() {
// Random ToUnicode CMaps; `expected` is what expanding their entries in order gives.
let mut g = Gen(0x2545_f491_4f6c_dd1d);
for _ in 0..300 {
let mut cmap = String::from("1 begincodespacerange <0000> <FFFF> endcodespacerange ");
let mut expected: HashMap<u32, String> = HashMap::new();
for _ in 0..1 + g.below(12) {
match g.below(3) {
0 => {
let (code, u) = (g.below(300), g.units());
cmap += &format!("1 beginbfchar <{code:04X}> <{}> endbfchar ", hex(&u));
expected.insert(code, String::from_utf16_lossy(&u));
}
1 => {
let (lo, width, u) = (g.below(300), g.below(13), g.units());
cmap += &format!("1 beginbfrange <{lo:04X}> <{:04X}> <{}> endbfrange ", lo + width, hex(&u));
for k in 0..=width {
let mut v = u.clone();
if let Some(last) = v.last_mut() {
*last = last.wrapping_add(k as u16);
}
expected.insert(lo + k, String::from_utf16_lossy(&v));
}
}
_ => {
let (lo, width) = (g.below(300), g.below(5));
let dests: Vec<Vec<u16>> = (0..=width).map(|_| g.units()).collect();
let list: Vec<String> = dests.iter().map(|d| format!("<{}>", hex(d))).collect();
cmap += &format!("1 beginbfrange <{lo:04X}> <{:04X}> [{}] endbfrange ", lo + width, list.join(" "));
for (k, d) in dests.iter().enumerate() {
expected.insert(lo + k as u32, String::from_utf16_lossy(d));
}
}
}
}
let mut doc = Document::new_empty();
let tu = doc.add(Object::Stream(Stream::from_raw(Dict::new(), cmap.clone().into_bytes())));
let f = font(
&mut doc,
vec![
("Subtype", Object::name("Type0")),
("BaseFont", Object::name("Gen")),
("Encoding", Object::name("Identity-H")),
("ToUnicode", Object::Ref(tu)),
],
);
let m = Metrics::from_dict(&doc, &f);
for code in 0..=320u32 {
let got = m.text_of(code).map(|t| t.into_owned());
assert_eq!(got, expected.get(&code).cloned(), "code {code:#06x} in {cmap}");
}
let mut reverse: HashMap<char, u32> = HashMap::new();
// Code 0 is CID 0, `.notdef`, which no character is encoded with.
for (&code, text) in expected.iter().filter(|&(&code, _)| code != 0) {
if let Some(c) = only_char(text) {
keep(&mut reverse, c, code);
}
}
let mut chars: Vec<char> = expected.values().flat_map(|t| t.chars()).chain(['A', '\u{FFFD}', '\u{0}', '\u{1D400}']).collect();
chars.sort_unstable();
chars.dedup();
for c in chars {
let want = reverse.get(&c).map(|code| code.to_be_bytes()[2..].to_vec());
assert_eq!(m.encode(&c.to_string()), want, "char {c:?} in {cmap}");
}
}
}
#[test]
fn fffd_lookups_skip_overridden_surrogates() {
// Each record maps every code to itself, so its surrogate offsets decode to U+FFFD. The last
// record overrides the surrogates with letters, so each lookup walks past them to U+FFFD itself.
let mut cmap = "1 beginbfrange <0000> <FFFF> <0000> endbfrange ".repeat(300);
cmap.push_str("1 beginbfrange <D800> <DFFF> <0041> endbfrange");
let mut doc = Document::new_empty();
let tu = doc.add(Object::Stream(Stream::from_raw(Dict::new(), cmap.into_bytes())));
let f = font(
&mut doc,
vec![
("Subtype", Object::name("Type0")),
("BaseFont", Object::name("Gen")),
("Encoding", Object::name("Identity-H")),
("ToUnicode", Object::Ref(tu)),
],
);
let m = Metrics::from_dict(&doc, &f);
assert_eq!(m.encode("\u{FFFD}"), Some(vec![0xFF, 0xFD]));
}
#[test]
fn hostile_ranges_stay_bounded() {
// A range wider than 65 536 codes is ignored, a reversed one maps nothing, and a maximal
// one-code range maps just its own code.
let mut u = Unicode::default();
let cmap = b"1 beginbfrange <00000000> <FFFFFFFF> <0041> endbfrange \
1 beginbfrange <0005> <0001> <0041> endbfrange \
1 beginbfrange <FFFFFFFF> <FFFFFFFF> <FFFF> endbfrange";
parse_to_unicode(cmap, &mut u);
assert_eq!((u.ranges.len(), u.text(u32::MAX).as_deref()), (0, Some("\u{FFFF}")));
}
#[test]
fn low_codes_resolve_to_the_covering_range() {
let mut u = Unicode::default();
let cmap = b"1 beginbfrange <0005> <0007> <0041> endbfrange 1 beginbfrange <0064> <0065> <0042> endbfrange \
1 beginbfrange <0000FFFE> <00010001> <0043> endbfrange";
parse_to_unicode(cmap, &mut u);
let covered: Vec<u32> = (0..0x70).filter(|&c| u.covering(c).is_some()).collect();
assert_eq!(covered, [5, 6, 7, 100, 101]);
assert_eq!((u.covering(0xFFFF).is_some(), u.covering(0x1_0001).is_some(), u.covering(0x1_0002).is_none()), (true, true, true));
}
}
#[cfg(test)]
mod encoding_tests {
use pdfcraft_cos::{Dict, Document, Object};
use super::*;
/// A simple Type 1 font, optionally symbolic-flagged and optionally with an `/Encoding`.
fn simple(base: &str, flags: Option<i64>, enc: Option<Object>) -> (Document, Dict) {
let doc = Document::new_empty();
let mut d = Dict::new();
d.set(b"Type".to_vec(), Object::name("Font"));
d.set(b"Subtype".to_vec(), Object::name("Type1"));
d.set(b"BaseFont".to_vec(), Object::name(base));
if let Some(f) = flags {
let mut desc = Dict::new();
desc.set(b"Flags".to_vec(), Object::Int(f));
d.set(b"FontDescriptor".to_vec(), Object::Dict(desc));
}
if let Some(e) = enc {
d.set(b"Encoding".to_vec(), e);
}
(doc, d)
}
fn differences(items: Vec<Object>) -> Object {
let mut enc = Dict::new();
enc.set(b"Differences".to_vec(), Object::Array(items));
Object::Dict(enc)
}
#[test]
fn glyph_names_follow_the_naming_algorithm() {
// Annex D names and the two hex forms, as before.
assert_eq!(glyph_text("quotedblleft").as_deref(), Some("\u{201c}"));
assert_eq!(glyph_text("uni20AC").as_deref(), Some("\u{20ac}"));
assert_eq!(glyph_text("u1F600").as_deref(), Some("\u{1f600}"));
// A suffix marks a variant of the same character, so the stem still reads.
assert_eq!(glyph_text("a.sc").as_deref(), Some("a"));
assert_eq!(glyph_text("one.oldstyle").as_deref(), Some("1"));
assert_eq!(glyph_text("A.alt017").as_deref(), Some("A"));
// `_` joins components, so a ligature name is more than one character.
assert_eq!(glyph_text("f_i").as_deref(), Some("fi"));
assert_eq!(glyph_text("f_f_l").as_deref(), Some("ffl"));
assert_eq!(glyph_text("a_uni0301").as_deref(), Some("a\u{301}"));
// `uniXXXX` carries several values, so one name can be a whole cluster.
assert_eq!(glyph_text("uni00660069").as_deref(), Some("fi"));
assert_eq!(glyph_text("uni004100300041").as_deref(), Some("A0A"));
// Both forms combine with a suffix.
assert_eq!(glyph_text("uni00660069.alt").as_deref(), Some("fi"));
// Not names: unknown, empty, a half-read ligature, bad hex, and the `+` that
// `from_str_radix` would otherwise accept.
assert_eq!(glyph_text("nonsense"), None);
assert_eq!(glyph_text(""), None);
assert_eq!(glyph_text(".notdef"), None);
assert_eq!(glyph_text("f_nonsense"), None, "half a ligature is not an answer");
assert_eq!(glyph_text("uni00"), None, "not a whole four-digit value");
assert_eq!(glyph_text("uni0041004"), None);
assert_eq!(glyph_text("uni+041"), None, "a sign is not a hex digit");
assert_eq!(glyph_text("u+0041"), None);
assert_eq!(glyph_text("uniD800"), None, "a lone surrogate half is not a character");
// Glyph-index names carry no meaning, and must not be guessed at.
assert_eq!(glyph_text("g3"), None);
assert_eq!(glyph_text("index42"), None);
// `glyph_unicode` is the single-character view of the same algorithm.
assert_eq!(glyph_unicode("uni20AC"), Some('\u{20ac}'));
assert_eq!(glyph_unicode("a.sc"), Some('a'));
assert_eq!(glyph_unicode("f_i"), None, "a ligature is not one character");
}
#[test]
fn a_ligature_or_unreadable_name_in_differences_is_handled() {
// A ligature name maps one code to two characters, so text extraction reads "fi".
let (doc, d) = simple("Custom", None, Some(differences(vec![Object::Int(65), Object::name("f_i")])));
let m = Metrics::from_dict(&doc, &d);
assert_eq!(m.decode(b"A"), "fi");
// An unreadable name must *remove* the base encoding's letter rather than leave it
// standing in: code 65 is no longer "A", because the font says it is something else.
let (doc, d) = simple("Custom", None, Some(differences(vec![Object::Int(65), Object::name("g7")])));
let m = Metrics::from_dict(&doc, &d);
assert_eq!(m.text_of(65), None);
assert_eq!(m.text_of(66).as_deref(), Some("B"), "neighbouring codes keep their meaning");
}
#[test]
fn symbol_and_dingbats_are_not_read_as_latin() {
// Symbol's built-in encoding puts `alpha` at 0x61. We hold no table for it, so the honest
// answer is none at all -- never "a", which would corrupt find, copy and redaction.
for base in ["Symbol", "ZapfDingbats", "ABCDEF+Symbol"] {
let (doc, d) = simple(base, Some(4), None);
let m = Metrics::from_dict(&doc, &d);
assert_eq!(m.text_of(0x61), None, "{base} must not claim 'a'");
assert_eq!(m.decode(b"abg"), "", "{base}");
}
// MacExpert is small caps and oldstyle figures at these codes, and we hold no table.
let (doc, d) = simple("Times-Roman", None, Some(Object::name("MacExpertEncoding")));
let m = Metrics::from_dict(&doc, &d);
assert_eq!(m.text_of(0x61), None);
// /Differences still applies on top of a built-in encoding we cannot read.
let (doc, d) = simple("Symbol", Some(4), Some(differences(vec![Object::Int(0x61), Object::name("uni03B1")])));
let m = Metrics::from_dict(&doc, &d);
assert_eq!(m.text_of(0x61).as_deref(), Some("\u{3b1}"), "the font told us this one");
assert_eq!(m.text_of(0x62), None, "and nothing about this one");
}
#[test]
fn a_named_base_encoding_is_always_honoured() {
// Naming an encoding is a deliberate statement, so it wins even on Symbol.
for name in ["WinAnsiEncoding", "MacRomanEncoding", "StandardEncoding"] {
let mut enc = Dict::new();
enc.set(b"BaseEncoding".to_vec(), Object::name(name));
let (doc, d) = simple("Symbol", Some(4), Some(Object::Dict(enc)));
let m = Metrics::from_dict(&doc, &d);
assert_eq!(m.text_of(0x61).as_deref(), Some("a"), "{name} was asked for by name");
}
}
/// The Annex D.2 alias codes read, through the whole decode path, as the ink they draw.
///
/// This is the map `redact` matches its patterns on (`unicode_map`), so a code that decodes
/// to an invisible formatting character is a silent miss, not a cosmetic one: "well-known"
/// written with the 0xAD byte matched no search and no redaction pattern.
#[test]
fn the_alias_codes_decode_as_ordinary_ink_and_encode_back_to_the_low_code() {
let (doc, d) = simple("Helvetica", None, Some(Object::name("WinAnsiEncoding")));
let m = Metrics::from_dict(&doc, &d);
assert_eq!(m.decode(b"well\xadknown"), "well-known");
assert_eq!(m.decode(b"a\xa0b"), "a b");
assert_eq!(m.encode("well-known a b").as_deref(), Some(&b"well-known a b"[..]), "the low code is the one written");
let (doc, d) = simple("Helvetica", None, Some(Object::name("MacRomanEncoding")));
let m = Metrics::from_dict(&doc, &d);
assert_eq!(m.decode(b"a\xcab"), "a b");
assert_eq!(m.decode(b"a\xadb"), "a\u{2260}b", "MacRoman 0xAD is `notequal`, not a hyphen");
assert_eq!(m.encode("a b").as_deref(), Some(&b"a b"[..]));
// An explicit /Differences name still says what it says: this is a property of those
// codes in those tables, not of the glyph names.
let (doc, d) = simple("Helvetica", None, Some(differences(vec![Object::Int(0x41), Object::name("sfthyphen"), Object::name("nbspace")])));
let m = Metrics::from_dict(&doc, &d);
assert_eq!(m.decode(b"AB"), "\u{ad}\u{a0}");
}
/// Pins a deliberate restraint, so it is not "fixed" into a regression: a great many ordinary
/// Latin TrueType subsets set the `/FontDescriptor` symbolic flag and still rely on a Latin
/// encoding. Suppressing the implicit table on the flag would lose real text on a large share
/// of real documents, so only Symbol and ZapfDingbats -- named, and unambiguous -- suppress it.
#[test]
fn the_symbolic_flag_alone_does_not_suppress_the_latin_default() {
let (doc, d) = simple("SomeSubsetFont", Some(4), None);
let m = Metrics::from_dict(&doc, &d);
assert_eq!(m.text_of(0x61).as_deref(), Some("a"));
assert_eq!(m.decode(b"abc"), "abc");
}
}