Skip to main content

otf_pdf_font/
fonts.rs

1//! Loading fonts from font dictionaries (ISO 32000-2 §9.6, §9.7): simple
2//! fonts (Type1, TrueType, Type3) and composite fonts (Type0 with a CIDFont).
3
4use std::collections::HashMap;
5use std::sync::{Arc, Mutex};
6
7use otf_pdf_content::{Font, FontLoader};
8use otf_pdf_doc::Store;
9use otf_pdf_syntax::{Dict, ObjId, Object};
10use skrifa::{FontRef, MetadataProvider};
11
12use crate::cmap::CMap;
13use crate::glyphs::{encoding_by_name, glyph_to_unicode, standard_font, standard_width};
14use crate::tables::{
15    STANDARD_ENCODING, SYMBOL_ENCODING, WIN_ANSI_ENCODING, ZAPF_DINGBATS_ENCODING,
16};
17
18/// A font name without its subset tag (`ABCDEF+Name` → `Name`).
19fn base_name(dict: &Dict) -> String {
20    let raw = dict.get_name(b"BaseFont").unwrap_or_default();
21    let name = String::from_utf8_lossy(raw).into_owned();
22    match name.split_once('+') {
23        Some((tag, rest)) if tag.len() == 6 && tag.bytes().all(|b| b.is_ascii_uppercase()) => {
24            rest.to_owned()
25        }
26        _ => name,
27    }
28}
29
30/// The decoded bytes of an embedded font program, if any.
31fn font_file(store: &Store, descriptor: Option<&Dict>) -> Option<(Vec<u8>, &'static [u8])> {
32    let descriptor = descriptor?;
33    for key in [&b"FontFile2"[..], b"FontFile3", b"FontFile"] {
34        if let Some(Object::Stream(s)) = descriptor.get(key).map(|f| store.resolve(f)) {
35            let data = store.decode_stream(&s).ok()?.data;
36            let kind: &'static [u8] = match key {
37                b"FontFile2" => b"TrueType",
38                b"FontFile" => b"Type1",
39                _ => match s.dict.get_name(b"Subtype") {
40                    Some(b"OpenType") => b"OpenType",
41                    _ => b"CFF",
42                },
43            };
44            return Some((data, kind));
45        }
46    }
47    None
48}
49
50/// Glyph id → Unicode from an embedded TrueType/OpenType font's cmap.
51fn reverse_cmap(data: &[u8]) -> HashMap<u32, char> {
52    let mut out = HashMap::new();
53    if let Ok(font) = FontRef::new(data) {
54        for (ch, gid) in font.charmap().mappings() {
55            if let Some(c) = char::from_u32(ch) {
56                out.entry(u32::from(gid)).or_insert(c);
57            }
58        }
59    }
60    out
61}
62
63/// A simple font: one byte per code.
64#[derive(Debug)]
65struct SimpleFont {
66    name: String,
67    names: Vec<Option<String>>,
68    widths: Vec<f64>,
69    to_unicode: Option<CMap>,
70    /// Code → Unicode from the embedded font's own cmap.
71    program_unicode: HashMap<u32, char>,
72    dingbats: bool,
73    symbolic: bool,
74}
75
76impl Font for SimpleFont {
77    fn codes(&self, bytes: &[u8]) -> Vec<(u32, usize)> {
78        bytes.iter().map(|&b| (u32::from(b), 1)).collect()
79    }
80
81    fn width(&self, code: u32) -> f64 {
82        self.widths.get(code as usize).copied().unwrap_or(0.0)
83    }
84
85    fn unicode(&self, code: u32) -> Option<String> {
86        if let Some(s) = self.to_unicode.as_ref().and_then(|c| c.unicode(code)) {
87            return Some(s);
88        }
89        if let Some(s) = self
90            .names
91            .get(code as usize)
92            .and_then(Option::as_ref)
93            .and_then(|n| glyph_to_unicode(n, self.dingbats))
94        {
95            return Some(s);
96        }
97        if let Some(c) = self.program_unicode.get(&code) {
98            return Some(c.to_string());
99        }
100        // Nothing better: printable codes read as Latin-1 (what most simple
101        // fonts without an encoding mean).
102        let c = char::from_u32(code)?;
103        (!self.symbolic && (' '..='~').contains(&c) || (!self.symbolic && code >= 0xa0))
104            .then(|| c.to_string())
105    }
106
107    fn name(&self) -> &str {
108        &self.name
109    }
110}
111
112/// A composite (Type0) font: a CMap and a CIDFont.
113#[derive(Debug)]
114struct CompositeFont {
115    name: String,
116    cmap: CMap,
117    to_unicode: Option<CMap>,
118    /// (first CID, last CID, width), from `/W`.
119    widths: Vec<(u32, u32, f64)>,
120    default_width: f64,
121    /// (CID, vertical advance), from `/W2`, and `/DW2`'s advance.
122    vertical_widths: Vec<(u32, u32, f64)>,
123    default_vertical: f64,
124    /// CID → Unicode via the embedded font's cmap (for Identity mappings).
125    program_unicode: HashMap<u32, char>,
126    cid_to_gid: Option<Vec<u16>>,
127    /// CID → Unicode for an Adobe CJK collection (`/CIDSystemInfo`).
128    collection: Option<&'static [(u32, u32, u16)]>,
129}
130
131/// The code point for `cid` in a collection table.
132fn collection_unicode(table: &[(u32, u32, u16)], cid: u32) -> Option<char> {
133    let i = table
134        .partition_point(|(start, _, _)| *start <= cid)
135        .checked_sub(1)?;
136    let (start, cp, len) = *table.get(i)?;
137    (cid < start + u32::from(len))
138        .then(|| char::from_u32(cp + (cid - start)))
139        .flatten()
140}
141
142impl Font for CompositeFont {
143    fn codes(&self, bytes: &[u8]) -> Vec<(u32, usize)> {
144        self.cmap.codes(bytes)
145    }
146
147    fn width(&self, code: u32) -> f64 {
148        let Some(cid) = self.cmap.cid(code) else {
149            return self.default_width / 1000.0;
150        };
151        self.widths
152            .iter()
153            .rev()
154            .find(|(lo, hi, _)| *lo <= cid && cid <= *hi)
155            .map_or(self.default_width, |(_, _, w)| *w)
156            / 1000.0
157    }
158
159    fn is_vertical(&self) -> bool {
160        self.cmap.vertical
161    }
162
163    fn vertical_advance(&self, code: u32) -> f64 {
164        let cid = self.cmap.cid(code).unwrap_or(code);
165        self.vertical_widths
166            .iter()
167            .rev()
168            .find(|(lo, hi, _)| *lo <= cid && cid <= *hi)
169            .map_or(self.default_vertical, |(_, _, w)| *w)
170            / 1000.0
171    }
172
173    fn unicode(&self, code: u32) -> Option<String> {
174        if let Some(s) = self.to_unicode.as_ref().and_then(|c| c.unicode(code)) {
175            return Some(s);
176        }
177        if let Some(s) = self.cmap.unicode(code) {
178            return Some(s);
179        }
180        let cid = self.cmap.cid(code)?;
181        if let Some(c) = self.collection.and_then(|t| collection_unicode(t, cid)) {
182            return Some(c.to_string());
183        }
184        let gid = match &self.cid_to_gid {
185            Some(map) => u32::from(*map.get(cid as usize)?),
186            None => cid,
187        };
188        self.program_unicode.get(&gid).map(|c| c.to_string())
189    }
190
191    fn name(&self) -> &str {
192        &self.name
193    }
194}
195
196fn number(o: &Object) -> Option<f64> {
197    o.as_f64().filter(|v| v.is_finite())
198}
199
200/// Parses `/W` (`c [w1 w2 …]` and `cfirst clast w`) or, with `triple`,
201/// `/W2` (`c [w1y vx vy …]`, `cfirst clast w1y vx vy`), keeping the first
202/// number of each entry.
203fn cid_widths(store: &Store, array: &[Object], triple: bool) -> Vec<(u32, u32, f64)> {
204    let step = if triple { 3 } else { 1 };
205    let mut out = Vec::new();
206    let mut i = 0;
207    while i < array.len() && out.len() < 1 << 20 {
208        let Some(first) = array.get(i).and_then(Object::as_int) else {
209            break;
210        };
211        let first = first.clamp(0, i64::from(u32::MAX)) as u32;
212        match array.get(i + 1).map(|o| store.resolve(o)) {
213            Some(Object::Array(ws)) => {
214                for (k, chunk) in ws.chunks(step).enumerate() {
215                    if let Some(w) = chunk.first().and_then(number) {
216                        let cid = first.saturating_add(k as u32);
217                        out.push((cid, cid, w));
218                    }
219                }
220                i += 2;
221            }
222            Some(last) => {
223                let last = last.as_int().unwrap_or(0).clamp(0, i64::from(u32::MAX)) as u32;
224                if let Some(w) = array.get(i + 2).and_then(number) {
225                    out.push((first, last, w));
226                }
227                i += 2 + step;
228            }
229            None => break,
230        }
231    }
232    out
233}
234
235fn load_simple(store: &Store, dict: &Dict) -> SimpleFont {
236    let name = base_name(dict);
237    let subtype = dict.get_name(b"Subtype").unwrap_or_default();
238    let descriptor = dict
239        .get(b"FontDescriptor")
240        .and_then(|d| store.resolve_dict(d));
241    let flags = descriptor
242        .as_ref()
243        .and_then(|d| d.get_int(b"Flags"))
244        .unwrap_or(0);
245    let standard = standard_font(&name);
246    let symbol_like = matches!(standard, Some("Symbol" | "ZapfDingbats"));
247    let symbolic = (flags & 4 != 0 && flags & 32 == 0) || symbol_like;
248    let program = font_file(store, descriptor.as_ref());
249
250    // Base encoding: named, built into the program, or the default.
251    let encoding = dict.get(b"Encoding").map(|e| store.resolve(e));
252    let named_base = match &encoding {
253        Some(Object::Name(n)) => encoding_by_name(n.as_bytes()),
254        Some(Object::Dict(d)) => d.get_name(b"BaseEncoding").and_then(encoding_by_name),
255        _ => None,
256    };
257    let builtin = match &program {
258        Some((data, b"Type1")) => crate::type1::builtin_encoding(data),
259        _ => None,
260    };
261    let mut names: Vec<Option<String>> = vec![None; 256];
262    let fallback: &[&str; 256] = match standard {
263        Some("Symbol") => &SYMBOL_ENCODING,
264        Some("ZapfDingbats") => &ZAPF_DINGBATS_ENCODING,
265        _ if subtype == b"TrueType" && !symbolic => &WIN_ANSI_ENCODING,
266        _ => &STANDARD_ENCODING,
267    };
268    for (code, slot) in names.iter_mut().enumerate() {
269        let from_builtin = builtin
270            .as_ref()
271            .filter(|_| named_base.is_none())
272            .and_then(|b| b.get(code).cloned().flatten());
273        let base = named_base.unwrap_or(fallback);
274        *slot = from_builtin.or_else(|| {
275            base.get(code)
276                .filter(|n| !n.is_empty())
277                .map(|n| (*n).to_owned())
278        });
279        // Codes StandardEncoding leaves empty usually mean WinAnsi.
280        if slot.is_none() && named_base.is_none() && builtin.is_none() && !symbolic {
281            *slot = WIN_ANSI_ENCODING
282                .get(code)
283                .filter(|n| !n.is_empty())
284                .map(|n| (*n).to_owned());
285        }
286    }
287    if let Some(Object::Dict(enc)) = &encoding
288        && let Some(diffs) = enc.get_array(b"Differences")
289    {
290        let mut code = 0usize;
291        for item in diffs {
292            match item {
293                Object::Int(n) => code = (*n).clamp(0, 255) as usize,
294                Object::Name(n) => {
295                    if let Some(slot) = names.get_mut(code) {
296                        *slot = Some(String::from_utf8_lossy(n.as_bytes()).into_owned());
297                    }
298                    code += 1;
299                }
300                _ => {}
301            }
302        }
303    }
304
305    // Widths: /Widths, else the standard 14 metrics, else /MissingWidth.
306    let font_matrix_scale = if subtype == b"Type3" {
307        dict.get_array(b"FontMatrix")
308            .and_then(|m| m.first())
309            .and_then(number)
310            .unwrap_or(0.001)
311    } else {
312        0.001
313    };
314    let missing = descriptor
315        .as_ref()
316        .and_then(|d| d.get(b"MissingWidth"))
317        .and_then(number)
318        .unwrap_or(0.0);
319    let first = dict.get_int(b"FirstChar").unwrap_or(0).clamp(0, 255) as usize;
320    let declared = dict.get(b"Widths").map(|w| store.resolve(w));
321    let mut widths = vec![missing * font_matrix_scale; 256];
322    match declared.as_ref().and_then(Object::as_array) {
323        Some(ws) => {
324            for (i, w) in ws.iter().enumerate() {
325                if let (Some(slot), Some(w)) =
326                    (widths.get_mut(first + i), number(&store.resolve(w)))
327                {
328                    *slot = w * font_matrix_scale;
329                }
330            }
331        }
332        None => {
333            let std = standard.unwrap_or("Helvetica");
334            for (code, slot) in widths.iter_mut().enumerate() {
335                if let Some(w) = names
336                    .get(code)
337                    .and_then(Option::as_ref)
338                    .and_then(|n| standard_width(std, n))
339                {
340                    *slot = w / 1000.0;
341                } else if *slot == 0.0 {
342                    *slot = 0.5;
343                }
344            }
345        }
346    }
347
348    // The embedded TrueType's own cmap, for symbolic fonts without names.
349    let program_unicode = match &program {
350        Some((data, b"TrueType" | b"OpenType")) => {
351            let by_gid = reverse_cmap(data);
352            let mut out = HashMap::new();
353            if let Ok(font) = FontRef::new(data) {
354                let charmap = font.charmap();
355                for code in 0u32..256 {
356                    let gid = charmap.map(code).or_else(|| charmap.map(0xf000 + code));
357                    if let Some(c) = gid.and_then(|g| by_gid.get(&u32::from(g)))
358                        && u32::from(*c) != code + 0xf000
359                    {
360                        out.insert(code, *c);
361                    }
362                }
363            }
364            out
365        }
366        _ => HashMap::new(),
367    };
368
369    SimpleFont {
370        dingbats: standard == Some("ZapfDingbats"),
371        name,
372        names,
373        widths,
374        to_unicode: to_unicode(store, dict),
375        program_unicode,
376        symbolic,
377    }
378}
379
380fn to_unicode(store: &Store, dict: &Dict) -> Option<CMap> {
381    match dict.get(b"ToUnicode").map(|t| store.resolve(t)) {
382        Some(Object::Stream(s)) => {
383            let cmap = CMap::parse(&store.decode_stream(&s).ok()?.data);
384            cmap.has_unicode().then_some(cmap)
385        }
386        // `/Identity-H` as a ToUnicode name: codes are UTF-16.
387        Some(Object::Name(n)) if n.as_bytes().starts_with(b"Identity") => {
388            Some(CMap::predefined(b"UniGB-UCS2-H"))
389        }
390        _ => None,
391    }
392}
393
394fn load_composite(store: &Store, dict: &Dict) -> CompositeFont {
395    let cmap = match dict.get(b"Encoding").map(|e| store.resolve(e)) {
396        Some(Object::Name(n)) => CMap::predefined(n.as_bytes()),
397        Some(Object::Stream(s)) => {
398            let parsed = store
399                .decode_stream(&s)
400                .map(|d| CMap::parse(&d.data))
401                .unwrap_or_default();
402            if parsed.has_codespace() {
403                parsed
404            } else {
405                CMap::predefined(b"Identity-H")
406            }
407        }
408        _ => CMap::predefined(b"Identity-H"),
409    };
410    let descendant = dict
411        .get(b"DescendantFonts")
412        .map(|d| store.resolve(d))
413        .and_then(|d| d.as_array().and_then(|a| a.first().cloned()))
414        .and_then(|d| store.resolve_dict(&d))
415        .unwrap_or_default();
416    let descriptor = descendant
417        .get(b"FontDescriptor")
418        .and_then(|d| store.resolve_dict(d));
419    let widths = descendant
420        .get(b"W")
421        .map(|w| store.resolve(w))
422        .and_then(|w| w.as_array().map(|a| cid_widths(store, a, false)))
423        .unwrap_or_default();
424    let vertical_widths = descendant
425        .get(b"W2")
426        .map(|w| store.resolve(w))
427        .and_then(|w| w.as_array().map(|a| cid_widths(store, a, true)))
428        .unwrap_or_default();
429    let default_vertical = descendant
430        .get_array(b"DW2")
431        .and_then(|a| a.get(1))
432        .and_then(number)
433        .unwrap_or(-1000.0);
434    let cid_to_gid = match descendant.get(b"CIDToGIDMap").map(|m| store.resolve(m)) {
435        Some(Object::Stream(s)) => store.decode_stream(&s).ok().map(|d| {
436            d.data
437                .as_chunks::<2>()
438                .0
439                .iter()
440                .map(|c| {
441                    u16::from_be_bytes([
442                        c.first().copied().unwrap_or(0),
443                        c.get(1).copied().unwrap_or(0),
444                    ])
445                })
446                .collect()
447        }),
448        _ => None,
449    };
450    let program_unicode = match font_file(store, descriptor.as_ref()) {
451        Some((data, b"TrueType" | b"OpenType")) => reverse_cmap(&data),
452        _ => HashMap::new(),
453    };
454    let collection = descendant
455        .get(b"CIDSystemInfo")
456        .and_then(|i| store.resolve_dict(i))
457        .and_then(
458            |info| match info.get(b"Ordering").map(|o| store.resolve(o)) {
459                Some(Object::String(s)) => Some(s.bytes),
460                _ => None,
461            },
462        )
463        .and_then(|ordering| match ordering.as_slice() {
464            b"Japan1" => Some(crate::cid_tables::JAPAN1),
465            b"GB1" => Some(crate::cid_tables::GB1),
466            b"CNS1" => Some(crate::cid_tables::CNS1),
467            b"Korea1" | b"KR" => Some(crate::cid_tables::KOREA1),
468            _ => None,
469        });
470    CompositeFont {
471        collection,
472        name: base_name(dict),
473        cmap,
474        to_unicode: to_unicode(store, dict),
475        widths,
476        default_width: descendant.get(b"DW").and_then(number).unwrap_or(1000.0),
477        vertical_widths,
478        default_vertical,
479        program_unicode,
480        cid_to_gid,
481    }
482}
483
484/// Loads a font from its dictionary.
485pub fn load_font(store: &Store, dict: &Dict) -> Arc<dyn Font> {
486    match dict.get_name(b"Subtype") {
487        Some(b"Type0") => Arc::new(load_composite(store, dict)),
488        _ => Arc::new(load_simple(store, dict)),
489    }
490}
491
492/// A [`FontLoader`] that caches fonts by object id, so a font used on many
493/// pages is loaded once.
494#[derive(Debug, Default)]
495pub struct FontCache {
496    fonts: Mutex<HashMap<ObjId, Arc<dyn Font>>>,
497}
498
499impl FontCache {
500    /// An empty cache.
501    pub fn new() -> Self {
502        Self::default()
503    }
504}
505
506impl FontLoader for FontCache {
507    fn load(&self, store: &Store, font: &Object) -> Option<Arc<dyn Font>> {
508        let id = font.as_ref();
509        if let Some(id) = id
510            && let Some(f) = self.fonts.lock().ok().and_then(|m| m.get(&id).cloned())
511        {
512            return Some(f);
513        }
514        let dict = store.resolve_dict(font)?;
515        let loaded = load_font(store, &dict);
516        if let (Some(id), Ok(mut m)) = (id, self.fonts.lock()) {
517            m.insert(id, Arc::clone(&loaded));
518        }
519        Some(loaded)
520    }
521}
522
523#[cfg(test)]
524mod tests {
525    use super::*;
526
527    #[test]
528    fn collections() {
529        // Adobe-Japan1 CID 843 is HIRAGANA LETTER A (U+3042).
530        assert_eq!(
531            collection_unicode(crate::cid_tables::JAPAN1, 843),
532            Some('あ')
533        );
534        // CID 34 is "A" in Adobe-Japan1.
535        assert_eq!(collection_unicode(crate::cid_tables::JAPAN1, 34), Some('A'));
536        assert_eq!(
537            collection_unicode(crate::cid_tables::JAPAN1, u32::MAX),
538            None
539        );
540        assert_eq!(collection_unicode(&[], 5), None);
541    }
542}