Skip to main content

docling_pdf/render/font/
mod.rs

1//! PDF fonts for the renderer: a font dictionary becomes a [`LoadedFont`]
2//! that splits strings into codes, gives each code its advance and its glyph
3//! outline in text space, or — for Type 3 — its glyph procedure.
4//!
5//! Glyph programs: TrueType / OpenType (`/FontFile2`, `/FontFile3 /OpenType`)
6//! and bare CFF (`/FontFile3 /Type1C`, `/CIDFontType0C`) through ttf-parser,
7//! Type 1 (`/FontFile`) through [`type1`]. Glyph selection follows ISO
8//! 32000-1 9.6.6 / 9.7.4: a simple font's code goes through `/Differences`,
9//! the base encoding or the program's built-in one to a glyph *name* (Type 1,
10//! CFF) or through the `cmap` subtables to a glyph *index* (TrueType — (3,0)
11//! symbol lookups with the `F0xx` convention, (1,0) by code, (3,1) by the
12//! name's Unicode, `post` names, and the bare code when the font has no
13//! `cmap`); a composite font's code goes through its CMap to a CID and
14//! `/CIDToGIDMap` or the CFF charset to a glyph. A font without a program is
15//! drawn with a host face chosen by [`fallback`].
16
17pub mod cmap;
18pub mod dotsection;
19pub mod encodings;
20pub mod fallback;
21pub mod type1;
22
23use std::cell::RefCell;
24use std::collections::HashMap;
25use std::rc::Rc;
26use std::sync::Arc;
27
28use lopdf::{Dictionary, Document, Object, ObjectId};
29use ttf_parser::GlyphId;
30
31use super::geom::Mat;
32use super::objects::{
33    as_dict, as_stream, deref, get, get_dict, get_int, get_name, get_num, name, num, nums,
34};
35use cmap::CMap;
36
37/// A glyph outline in the program's own units with its advance.
38pub struct GlyphPath {
39    pub path: Option<tiny_skia::Path>,
40    pub advance: f64,
41}
42
43/// The program that draws the glyphs.
44enum Program {
45    /// TrueType / OpenType (glyf or CFF table): parsed per use, cheap.
46    Sfnt {
47        data: Vec<u8>,
48        /// glyph units → text space (1/unitsPerEm, or the CFF FontMatrix).
49        matrix: Mat,
50        /// CID → glyph index for a CID-keyed CFF inside the sfnt.
51        cid_to_gid: Option<HashMap<u16, u16>>,
52        has_cmap: bool,
53        has_glyf: bool,
54        /// Glyph names of a CFF-flavoured sfnt, for name lookups.
55        names: HashMap<String, u16>,
56    },
57    /// A bare CFF program.
58    Cff {
59        data: Vec<u8>,
60        matrix: Mat,
61        cid_to_gid: Option<HashMap<u16, u16>>,
62        /// glyph name → glyph index (ttf-parser's own lookup by name has
63        /// no answer for the predefined ISOAdobe/Expert charsets).
64        names: HashMap<String, u16>,
65    },
66    Type1 {
67        font: type1::Type1Font,
68        matrix: Mat,
69    },
70    /// A host face standing in for a non-embedded font.
71    Fallback {
72        face: Arc<fallback::FallbackFace>,
73        matrix: Mat,
74    },
75    None,
76}
77
78/// Type 3 glyph procedures.
79pub struct Type3 {
80    pub font_matrix: Mat,
81    pub char_procs: HashMap<String, ObjectId>,
82    pub resources: Option<Dictionary>,
83}
84
85pub struct LoadedFont {
86    pub composite: bool,
87    pub type3: Option<Type3>,
88    program: Program,
89    /// Simple fonts: code → glyph name from `/Encoding` (`/Differences` over
90    /// the base encoding), when the PDF gives one.
91    encoding_names: HashMap<u8, String>,
92    /// The base encoding was named explicitly (`/WinAnsiEncoding`, …).
93    has_base_encoding: bool,
94    /// The font is symbolic (descriptor flag 3) and the PDF names no encoding.
95    symbolic: bool,
96    /// Composite fonts: string → codes → CIDs.
97    cmap: Option<CMap>,
98    cid_to_gid: CidToGid,
99    /// code (simple) or CID (composite) → advance in 1/1000 text space.
100    widths: HashMap<u32, f64>,
101    default_width: Option<f64>,
102    /// Type 3: `/Widths` are in glyph space (through `FontMatrix`).
103    /// `ToUnicode`, for a fallback face's cmap lookups.
104    to_unicode: HashMap<u32, String>,
105    /// Standard-14 metrics for a non-embedded font without `/Widths`.
106    std14: Option<crate::std14::Std14Widths>,
107    /// Glyph cache: code/CID → outline in text space (font size 1).
108    cache: RefCell<HashMap<u32, Option<Rc<tiny_skia::Path>>>>,
109    advance_cache: RefCell<HashMap<u32, f64>>,
110    pub vertical: bool,
111    is_dingbats: bool,
112    is_symbol_face: bool,
113}
114
115enum CidToGid {
116    Identity,
117    Map(Vec<u8>),
118}
119
120fn descriptor<'a>(
121    doc: &'a Document,
122    fdict: &'a Dictionary,
123    composite: bool,
124) -> Option<&'a Dictionary> {
125    let owner = if composite {
126        descendant(doc, fdict)?
127    } else {
128        fdict
129    };
130    get_dict(doc, owner, b"FontDescriptor")
131}
132
133fn descendant<'a>(doc: &'a Document, fdict: &'a Dictionary) -> Option<&'a Dictionary> {
134    match get(doc, fdict, b"DescendantFonts")? {
135        Object::Array(a) => a.first().and_then(|o| as_dict(doc, o)),
136        Object::Dictionary(d) => Some(d),
137        _ => None,
138    }
139}
140
141fn base_font_name(doc: &Document, fdict: &Dictionary) -> String {
142    let raw = get_name(doc, fdict, b"BaseFont")
143        .map(|n| String::from_utf8_lossy(n).into_owned())
144        .unwrap_or_default();
145    // Strip an `ABCDEF+` subset prefix.
146    match raw.split_once('+') {
147        Some((pre, rest)) if pre.len() == 6 && pre.bytes().all(|b| b.is_ascii_uppercase()) => {
148            rest.to_string()
149        }
150        _ => raw,
151    }
152}
153
154impl LoadedFont {
155    pub fn load(doc: &Document, fdict: &Dictionary) -> LoadedFont {
156        let subtype = get_name(doc, fdict, b"Subtype").unwrap_or(b"");
157        let composite = subtype == b"Type0";
158        let base_font = base_font_name(doc, fdict);
159        let desc = descriptor(doc, fdict, composite);
160        let flags = desc.and_then(|d| get_int(doc, d, b"Flags"));
161        let symbolic_flag = flags.is_some_and(|f| f & 4 != 0 && f & 32 == 0);
162
163        // --- the embedded program -------------------------------------------
164        let mut program = Program::None;
165        if let Some(d) = desc {
166            if let Some(s) = d.get(b"FontFile2").ok().and_then(|o| as_stream(doc, o)) {
167                if let Ok(data) = s.decompressed_content() {
168                    program = sfnt_program(data).unwrap_or(Program::None);
169                }
170            }
171            if matches!(program, Program::None) {
172                if let Some(s) = d.get(b"FontFile3").ok().and_then(|o| as_stream(doc, o)) {
173                    if let Ok(data) = s.decompressed_content() {
174                        let st = get_name(doc, &s.dict, b"Subtype").unwrap_or(b"");
175                        program = if st == b"OpenType"
176                            || data.starts_with(b"OTTO")
177                            || data.starts_with(&[0, 1, 0, 0])
178                            || data.starts_with(b"true")
179                        {
180                            sfnt_program(data.clone())
181                                .or_else(|| cff_program(data))
182                                .unwrap_or(Program::None)
183                        } else {
184                            cff_program(data.clone())
185                                .or_else(|| sfnt_program(data))
186                                .unwrap_or(Program::None)
187                        };
188                    }
189                }
190            }
191            if matches!(program, Program::None) {
192                if let Some(s) = d.get(b"FontFile").ok().and_then(|o| as_stream(doc, o)) {
193                    if let Ok(data) = s.decompressed_content() {
194                        if let Some(f) = type1::Type1Font::parse(&data) {
195                            let m =
196                                Mat::from_slice(&f.font_matrix).unwrap_or(Mat::scale(0.001, 0.001));
197                            program = Program::Type1 { font: f, matrix: m };
198                        } else if let Some(p) =
199                            cff_program(data.clone()).or_else(|| sfnt_program(data))
200                        {
201                            // Mislabelled programs happen.
202                            program = p;
203                        }
204                    }
205                }
206            }
207        }
208
209        // --- Type 3 ----------------------------------------------------------
210        let type3 = if subtype == b"Type3" {
211            let fm = get(doc, fdict, b"FontMatrix")
212                .and_then(|o| nums(doc, o))
213                .and_then(|v| Mat::from_slice(&v))
214                .unwrap_or(Mat::scale(0.001, 0.001));
215            let mut char_procs = HashMap::new();
216            if let Some(cp) = get_dict(doc, fdict, b"CharProcs") {
217                for (k, v) in cp.iter() {
218                    if let Object::Reference(id) = v {
219                        char_procs.insert(String::from_utf8_lossy(k).into_owned(), *id);
220                    }
221                }
222            }
223            let resources = get_dict(doc, fdict, b"Resources").cloned();
224            Some(Type3 {
225                font_matrix: fm,
226                char_procs,
227                resources,
228            })
229        } else {
230            None
231        };
232
233        // --- encodings -----------------------------------------------------
234        let lower = base_font.to_ascii_lowercase();
235        let is_symbol_face = lower.starts_with("symbol");
236        let is_dingbats = lower.contains("dingbat");
237        let mut encoding_names: HashMap<u8, String> = HashMap::new();
238        let mut has_base_encoding = false;
239        let enc = get(doc, fdict, b"Encoding");
240        let mut cmap = None;
241        if composite {
242            cmap = Some(match enc {
243                Some(Object::Name(n)) => CMap::predefined(n),
244                Some(Object::Stream(s)) => {
245                    let mut c = s
246                        .decompressed_content()
247                        .ok()
248                        .map(|d| CMap::parse(&d))
249                        .unwrap_or_else(CMap::identity_h);
250                    if let Some(Object::Name(n)) = get(doc, &s.dict, b"UseCMap") {
251                        if n.starts_with(b"Identity") && !c.identity {
252                            // Codes not covered map to themselves.
253                        }
254                    }
255                    if get_int(doc, &s.dict, b"WMode") == Some(1) {
256                        c.vertical = true;
257                    }
258                    c
259                }
260                _ => CMap::identity_h(),
261            });
262        } else {
263            let base_table: Option<&'static [(u8, &'static str)]> = match enc {
264                Some(Object::Name(n)) => {
265                    has_base_encoding = true;
266                    encodings::by_name(n)
267                }
268                Some(Object::Dictionary(d)) => match get_name(doc, d, b"BaseEncoding") {
269                    Some(n) => {
270                        has_base_encoding = true;
271                        encodings::by_name(n)
272                    }
273                    None => None,
274                },
275                _ => None,
276            };
277            // The implicit base: Standard for non-symbolic fonts; the face's
278            // own encoding for Symbol/ZapfDingbats; nothing for other
279            // symbolic fonts (their program's built-in encoding applies).
280            let implicit: Option<&'static [(u8, &'static str)]> = if is_symbol_face
281                && matches!(program, Program::None)
282            {
283                Some(encodings::SYMBOL)
284            } else if is_dingbats && matches!(program, Program::None) {
285                Some(encodings::ZAPF_DINGBATS)
286            } else if !symbolic_flag || matches!(program, Program::None | Program::Fallback { .. })
287            {
288                Some(encodings::STANDARD)
289            } else {
290                None
291            };
292            if let Some(t) = base_table.or(implicit) {
293                for &(c, n) in t {
294                    encoding_names.insert(c, n.to_string());
295                }
296            }
297            if let Some(Object::Dictionary(d)) = enc {
298                if let Some(Object::Array(diffs)) = get(doc, d, b"Differences") {
299                    let mut code: i64 = 0;
300                    for el in diffs {
301                        match deref(doc, el) {
302                            Object::Integer(i) => code = *i,
303                            Object::Real(r) => code = *r as i64,
304                            Object::Name(n) => {
305                                if (0..=255).contains(&code) {
306                                    encoding_names.insert(
307                                        code as u8,
308                                        String::from_utf8_lossy(n).into_owned(),
309                                    );
310                                }
311                                code += 1;
312                            }
313                            _ => {}
314                        }
315                    }
316                }
317            }
318        }
319
320        // --- widths ----------------------------------------------------------
321        let mut widths = HashMap::new();
322        let mut default_width = None;
323        if composite {
324            if let Some(dd) = descendant(doc, fdict) {
325                default_width = Some(get_num(doc, dd, b"DW").unwrap_or(1000.0));
326                if let Some(Object::Array(w)) = get(doc, dd, b"W") {
327                    let mut i = 0;
328                    while i < w.len() {
329                        let c = w.get(i).map(|o| deref(doc, o)).and_then(num);
330                        match (c, w.get(i + 1).map(|o| deref(doc, o))) {
331                            (Some(c), Some(Object::Array(list))) => {
332                                for (k, wv) in list.iter().enumerate() {
333                                    if let Some(wv) = num(deref(doc, wv)) {
334                                        widths.insert(c as u32 + k as u32, wv);
335                                    }
336                                }
337                                i += 2;
338                            }
339                            (Some(c1), Some(o2)) => {
340                                if let (Some(c2), Some(wv)) =
341                                    (num(o2), w.get(i + 2).map(|o| deref(doc, o)).and_then(num))
342                                {
343                                    let (c1, c2) = (c1.max(0.0) as u32, c2.max(0.0) as u32);
344                                    if c2 >= c1 && c2 - c1 < 65536 {
345                                        for cid in c1..=c2 {
346                                            widths.insert(cid, wv);
347                                        }
348                                    }
349                                }
350                                i += 3;
351                            }
352                            _ => break,
353                        }
354                    }
355                }
356            }
357        } else {
358            let first = get_int(doc, fdict, b"FirstChar").unwrap_or(0);
359            if let Some(Object::Array(w)) = get(doc, fdict, b"Widths") {
360                for (k, wv) in w.iter().enumerate() {
361                    if let Some(wv) = num(deref(doc, wv)) {
362                        let code = first + k as i64;
363                        if (0..=255).contains(&code) {
364                            widths.insert(code as u32, wv);
365                        }
366                    }
367                }
368                if !w.is_empty() {
369                    default_width = Some(
370                        desc.and_then(|d| get_num(doc, d, b"MissingWidth"))
371                            .unwrap_or(0.0),
372                    );
373                }
374            }
375            if widths.is_empty() {
376                if let Some(mw) = desc.and_then(|d| get_num(doc, d, b"MissingWidth")) {
377                    if mw > 0.0 {
378                        default_width = Some(mw);
379                    }
380                }
381            }
382        }
383        let std14 = if widths.is_empty() && !composite && type3.is_none() {
384            crate::std14::widths_for(base_font.as_bytes())
385        } else {
386            None
387        };
388
389        // --- CID → GID -------------------------------------------------------
390        let mut cid_to_gid = CidToGid::Identity;
391        if composite {
392            if let Some(dd) = descendant(doc, fdict) {
393                if let Some(Object::Stream(s)) = get(doc, dd, b"CIDToGIDMap") {
394                    if let Ok(data) = s.decompressed_content() {
395                        cid_to_gid = CidToGid::Map(data);
396                    }
397                }
398            }
399        }
400
401        // --- fallback face ---------------------------------------------------
402        if matches!(program, Program::None) && type3.is_none() {
403            let ordering_cjk = composite
404                && descendant(doc, fdict)
405                    .and_then(|dd| get_dict(doc, dd, b"CIDSystemInfo"))
406                    .and_then(|si| get(doc, si, b"Ordering"))
407                    .and_then(|o| match o {
408                        Object::String(s, _) => Some(s.clone()),
409                        _ => None,
410                    })
411                    .is_some_and(|o| {
412                        matches!(
413                            o.as_slice(),
414                            b"Japan1" | b"GB1" | b"CNS1" | b"Korea1" | b"KR"
415                        )
416                    });
417            let mut style = fallback::style_for(&base_font, flags, None);
418            if ordering_cjk {
419                style.family = fallback::Family::Cjk;
420            }
421            if let Some(face) = fallback::face(style) {
422                let upem = ttf_parser::Face::parse(&face.data, 0)
423                    .ok()
424                    .map(|f| f64::from(f.units_per_em()))
425                    .filter(|u| *u > 0.0)
426                    .unwrap_or(1000.0);
427                program = Program::Fallback {
428                    face,
429                    matrix: Mat::scale(1.0 / upem, 1.0 / upem),
430                };
431            }
432        }
433
434        let to_unicode = get(doc, fdict, b"ToUnicode")
435            .and_then(|o| match o {
436                Object::Stream(s) => s.decompressed_content().ok(),
437                _ => None,
438            })
439            .map(|d| crate::textparse::parse_tounicode(&d))
440            .unwrap_or_default();
441
442        let vertical = cmap.as_ref().is_some_and(|c| c.vertical);
443        LoadedFont {
444            composite,
445            type3,
446            program,
447            encoding_names,
448            has_base_encoding,
449            symbolic: symbolic_flag && enc.is_none(),
450            cmap,
451            cid_to_gid,
452            widths,
453            default_width,
454            to_unicode,
455            std14,
456            cache: RefCell::new(HashMap::new()),
457            advance_cache: RefCell::new(HashMap::new()),
458            vertical,
459            is_dingbats,
460            is_symbol_face,
461        }
462    }
463
464    /// Split a string operand into `(code, cid, is_single_byte)`; `cid` is
465    /// the code itself for a simple font.
466    pub fn decode(&self, bytes: &[u8]) -> Vec<(u32, u32, bool)> {
467        match &self.cmap {
468            Some(c) => c
469                .split(bytes)
470                .into_iter()
471                .map(|(code, n)| (code, c.cid(code), n == 1))
472                .collect(),
473            None => bytes
474                .iter()
475                .map(|&b| (u32::from(b), u32::from(b), true))
476                .collect(),
477        }
478    }
479
480    /// The horizontal advance of `cid` (a code for simple fonts) in text
481    /// space (font size 1). Type 3 advances go through the font matrix.
482    pub fn advance(&self, code: u32, cid: u32) -> f64 {
483        let key = if self.composite { cid } else { code };
484        if let Some(a) = self.advance_cache.borrow().get(&key) {
485            return *a;
486        }
487        let a = self.advance_uncached(code, cid);
488        self.advance_cache.borrow_mut().insert(key, a);
489        a
490    }
491
492    fn advance_uncached(&self, code: u32, cid: u32) -> f64 {
493        let key = if self.composite { cid } else { code };
494        if let Some(t3) = &self.type3 {
495            let w = self.widths.get(&key).copied().unwrap_or(0.0);
496            let (ax, _) = t3.font_matrix.apply_vec(w, 0.0);
497            return ax;
498        }
499        if let Some(w) = self.widths.get(&key) {
500            return w / 1000.0;
501        }
502        if let Some(std) = &self.std14 {
503            if let Some(ch) = self.unicode_of(code, cid) {
504                if let Some(w) = std.width(ch) {
505                    return w / 1000.0;
506                }
507            }
508        }
509        if let Some(dw) = self.default_width {
510            if dw > 0.0 || !self.widths.is_empty() {
511                return dw / 1000.0;
512            }
513        }
514        // The program's own advance.
515        if let Some(adv) = self.program_advance(code, cid) {
516            return adv;
517        }
518        if self.default_width.is_some() {
519            return self.default_width.unwrap_or(0.0) / 1000.0;
520        }
521        0.5
522    }
523
524    fn unicode_of(&self, code: u32, cid: u32) -> Option<char> {
525        let key = if self.composite { cid } else { code };
526        if let Some(s) = self
527            .to_unicode
528            .get(&key)
529            .or_else(|| self.to_unicode.get(&code))
530        {
531            return s.chars().next();
532        }
533        if !self.composite {
534            if let Some(n) = self.encoding_names.get(&(code as u8)) {
535                return crate::textparse::glyph_name_to_char(n.as_bytes());
536            }
537            return char::from_u32(code).filter(|c| c.is_ascii_graphic() || *c == ' ');
538        }
539        if self.cmap.as_ref().is_some_and(|c| c.unicode_codes) {
540            return char::from_u32(code);
541        }
542        None
543    }
544
545    /// A Type 3 font's procedure name for `code` (its `/Encoding /Differences`).
546    pub fn type3_glyph_name(&self, code: u32) -> Option<String> {
547        self.encoding_names.get(&(code as u8)).cloned()
548    }
549
550    /// The glyph name a simple font's code selects, if the PDF names one.
551    fn glyph_name(&self, code: u32) -> Option<&str> {
552        self.encoding_names.get(&(code as u8)).map(String::as_str)
553    }
554
555    /// The glyph outline for a code, in text space (font size 1), cached.
556    pub fn glyph(&self, code: u32, cid: u32) -> Option<Rc<tiny_skia::Path>> {
557        let key = if self.composite { cid } else { code };
558        if let Some(p) = self.cache.borrow().get(&key) {
559            return p.clone();
560        }
561        let built = self.build_glyph(code, cid).and_then(|g| {
562            let m = self.program_matrix();
563            g.path.and_then(|p| p.transform(m.to_ts())).map(Rc::new)
564        });
565        self.cache.borrow_mut().insert(key, built.clone());
566        built
567    }
568
569    fn program_matrix(&self) -> Mat {
570        match &self.program {
571            Program::Sfnt { matrix, .. }
572            | Program::Cff { matrix, .. }
573            | Program::Type1 { matrix, .. }
574            | Program::Fallback { matrix, .. } => *matrix,
575            Program::None => Mat::scale(0.001, 0.001),
576        }
577    }
578
579    pub fn has_program(&self) -> bool {
580        !matches!(self.program, Program::None)
581    }
582
583    fn program_advance(&self, code: u32, cid: u32) -> Option<f64> {
584        let g = self.build_glyph(code, cid)?;
585        let (ax, _) = self.program_matrix().apply_vec(g.advance, 0.0);
586        Some(ax)
587    }
588
589    fn build_glyph(&self, code: u32, cid: u32) -> Option<GlyphPath> {
590        match &self.program {
591            Program::None => None,
592            Program::Type1 { font, .. } => {
593                let name = self.type1_glyph_name(font, code)?;
594                font.glyph(&name)
595            }
596            Program::Cff {
597                data,
598                cid_to_gid,
599                names,
600                ..
601            } => {
602                let table = ttf_parser::cff::Table::parse(data)?;
603                let gid = if self.composite {
604                    let gid = match &self.cid_to_gid {
605                        CidToGid::Map(m) => map_cid(m, cid),
606                        CidToGid::Identity => cid,
607                    };
608                    match cid_to_gid {
609                        Some(map) => GlyphId(*map.get(&(gid as u16))?),
610                        None => GlyphId(gid as u16),
611                    }
612                } else {
613                    self.cff_simple_gid(&table, names, code)?
614                };
615                outline_cff(&table, gid)
616            }
617            Program::Sfnt {
618                data,
619                cid_to_gid,
620                has_cmap,
621                has_glyf,
622                names,
623                ..
624            } => {
625                let face = ttf_parser::Face::parse(data, 0).ok()?;
626                let gid = if self.composite {
627                    let gid = match &self.cid_to_gid {
628                        CidToGid::Map(m) => map_cid(m, cid),
629                        CidToGid::Identity => cid,
630                    };
631                    match cid_to_gid {
632                        Some(map) => GlyphId(*map.get(&(gid as u16))?),
633                        None => GlyphId(gid as u16),
634                    }
635                } else {
636                    self.sfnt_simple_gid(&face, names, code, *has_cmap, *has_glyf)?
637                };
638                outline_face(&face, gid)
639            }
640            Program::Fallback { face, .. } => {
641                let f = ttf_parser::Face::parse(&face.data, 0).ok()?;
642                let gid = self.fallback_gid(&f, code, cid)?;
643                outline_face(&f, gid)
644            }
645        }
646    }
647
648    fn type1_glyph_name(&self, font: &type1::Type1Font, code: u32) -> Option<String> {
649        let c = code as u8;
650        // /Differences (or an explicit base encoding) first, then the
651        // program's built-in encoding, then Standard.
652        let pdf_name = self.glyph_name(code);
653        let builtin = font.encoding.get(&c).cloned();
654        let candidates: Vec<String> = if self.symbolic && !self.has_base_encoding {
655            // Symbolic without /Encoding: the built-in encoding rules, but a
656            // /Differences entry (already merged into encoding_names) wins.
657            [pdf_name.map(str::to_string), builtin.clone()]
658                .into_iter()
659                .flatten()
660                .collect()
661        } else {
662            [pdf_name.map(str::to_string), builtin.clone()]
663                .into_iter()
664                .flatten()
665                .collect()
666        };
667        for cand in &candidates {
668            if font.has_glyph(cand) {
669                return Some(cand.clone());
670            }
671            // `uniXXXX` / AGL name → the program's own name for that character.
672            if let Some(ch) = crate::textparse::glyph_name_to_char(cand.as_bytes()) {
673                if let Some(n) = self.type1_name_for_char(font, ch) {
674                    return Some(n);
675                }
676            }
677        }
678        if let Some(n) = encodings::lookup(encodings::STANDARD, c) {
679            if font.has_glyph(n) {
680                return Some(n.to_string());
681            }
682        }
683        if let Some(b) = builtin {
684            return Some(b);
685        }
686        None
687    }
688
689    fn type1_name_for_char(&self, font: &type1::Type1Font, ch: char) -> Option<String> {
690        // Reverse AGL over the font's names (small fonts, done rarely).
691        font.glyph_names()
692            .find(|n| crate::textparse::glyph_name_to_char(n.as_bytes()) == Some(ch))
693            .cloned()
694    }
695
696    fn cff_simple_gid(
697        &self,
698        table: &ttf_parser::cff::Table,
699        names: &HashMap<String, u16>,
700        code: u32,
701    ) -> Option<GlyphId> {
702        let c = code as u8;
703        let by_name = |n: &str| -> Option<GlyphId> {
704            names
705                .get(n)
706                .map(|g| GlyphId(*g))
707                .or_else(|| table.glyph_index_by_name(n))
708        };
709        if let Some(name) = self.glyph_name(code) {
710            if let Some(g) = by_name(name) {
711                return Some(g);
712            }
713            if let Some(ch) = crate::textparse::glyph_name_to_char(name.as_bytes()) {
714                // Another name for the same character (`uni0041` vs `A`).
715                if let Some(std_name) = agl_name(ch) {
716                    if let Some(g) = by_name(std_name) {
717                        return Some(g);
718                    }
719                }
720                let uni = format!("uni{:04X}", ch as u32);
721                if let Some(g) = by_name(&uni) {
722                    return Some(g);
723                }
724                if let Some(g) = names
725                    .iter()
726                    .find(|(n, _)| crate::textparse::glyph_name_to_char(n.as_bytes()) == Some(ch))
727                    .map(|(_, g)| GlyphId(*g))
728                {
729                    return Some(g);
730                }
731            }
732        }
733        // The program's built-in encoding.
734        if let Some(g) = table.glyph_index(c) {
735            if g.0 != 0 {
736                return Some(g);
737            }
738        }
739        if let Some(n) = encodings::lookup(encodings::STANDARD, c) {
740            if let Some(g) = by_name(n) {
741                return Some(g);
742            }
743        }
744        None
745    }
746
747    fn sfnt_simple_gid(
748        &self,
749        face: &ttf_parser::Face,
750        names: &HashMap<String, u16>,
751        code: u32,
752        has_cmap: bool,
753        has_glyf: bool,
754    ) -> Option<GlyphId> {
755        let c = code as u8;
756        let cmap = face.tables().cmap.as_ref();
757        let symbol_lookup = |code: u32| -> Option<GlyphId> {
758            let cm = cmap?;
759            for sub in cm.subtables {
760                if sub.platform_id == ttf_parser::PlatformId::Windows && sub.encoding_id == 0 {
761                    for probe in [code, 0xF000 + code, 0xF100 + code, 0xF200 + code] {
762                        if let Some(g) = sub.glyph_index(probe) {
763                            if g.0 != 0 {
764                                return Some(g);
765                            }
766                        }
767                    }
768                }
769            }
770            None
771        };
772        let mac_lookup = |code: u32| -> Option<GlyphId> {
773            let cm = cmap?;
774            for sub in cm.subtables {
775                if sub.platform_id == ttf_parser::PlatformId::Macintosh {
776                    if let Some(g) = sub.glyph_index(code) {
777                        if g.0 != 0 {
778                            return Some(g);
779                        }
780                    }
781                }
782            }
783            None
784        };
785        let unicode_lookup = |ch: char| -> Option<GlyphId> {
786            let cm = cmap?;
787            for sub in cm.subtables {
788                if sub.is_unicode() {
789                    if let Some(g) = sub.glyph_index(ch as u32) {
790                        if g.0 != 0 {
791                            return Some(g);
792                        }
793                    }
794                }
795            }
796            None
797        };
798        let name = self.glyph_name(code);
799        let symbolic_path = self.symbolic || !self.has_base_encoding && name.is_none();
800        if symbolic_path {
801            if let Some(g) = symbol_lookup(code) {
802                return Some(g);
803            }
804            if let Some(g) = mac_lookup(code) {
805                return Some(g);
806            }
807        }
808        if let Some(n) = name {
809            if let Some(ch) = crate::textparse::glyph_name_to_char(n.as_bytes()) {
810                if let Some(g) = unicode_lookup(ch) {
811                    return Some(g);
812                }
813                if let Some(g) = symbol_lookup(ch as u32) {
814                    return Some(g);
815                }
816            }
817            if let Some(g) = names
818                .get(n)
819                .map(|g| GlyphId(*g))
820                .or_else(|| face.glyph_index_by_name(n))
821            {
822                if g.0 != 0 {
823                    return Some(g);
824                }
825            }
826            // `gXX` / `glyphXX` / `index XX` names address glyphs directly.
827            if let Some(g) = gid_from_name(n) {
828                return Some(GlyphId(g));
829            }
830        }
831        if !symbolic_path {
832            if let Some(g) = symbol_lookup(code) {
833                return Some(g);
834            }
835            if let Some(g) = mac_lookup(code) {
836                return Some(g);
837            }
838        }
839        if let Some(std) = encodings::lookup(encodings::STANDARD, c) {
840            if let Some(ch) = crate::textparse::glyph_name_to_char(std.as_bytes()) {
841                if let Some(g) = unicode_lookup(ch) {
842                    return Some(g);
843                }
844            }
845        }
846        if !has_cmap || face.tables().cmap.is_none() {
847            // No usable cmap: the code is the glyph index.
848            let _ = has_glyf;
849            return Some(GlyphId(code as u16));
850        }
851        None
852    }
853
854    fn fallback_gid(&self, face: &ttf_parser::Face, code: u32, cid: u32) -> Option<GlyphId> {
855        // Symbol / Dingbats faces are addressed by glyph name.
856        if !self.composite && (self.is_symbol_face || self.is_dingbats) {
857            if let Some(n) = self.glyph_name(code) {
858                if let Some(g) = face.glyph_index_by_name(n) {
859                    return Some(g);
860                }
861            }
862        }
863        if !self.composite {
864            if let Some(n) = self.glyph_name(code) {
865                if let Some(ch) = crate::textparse::glyph_name_to_char(n.as_bytes()) {
866                    if let Some(g) = face.glyph_index(ch) {
867                        return Some(g);
868                    }
869                }
870                if let Some(g) = face.glyph_index_by_name(n) {
871                    return Some(g);
872                }
873            }
874        }
875        let ch = self.unicode_of(code, cid)?;
876        face.glyph_index(ch)
877    }
878}
879
880fn map_cid(map: &[u8], cid: u32) -> u32 {
881    let i = cid as usize * 2;
882    match (map.get(i), map.get(i + 1)) {
883        (Some(hi), Some(lo)) => (u32::from(*hi) << 8) | u32::from(*lo),
884        _ => 0,
885    }
886}
887
888fn gid_from_name(n: &str) -> Option<u16> {
889    for prefix in ["glyph", "index", "cid", "g", "G"] {
890        if let Some(rest) = n.strip_prefix(prefix) {
891            if !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit()) {
892                return rest.parse().ok();
893            }
894        }
895    }
896    None
897}
898
899/// The Standard-encoding name of a character (the reverse of the AGL subset
900/// used here), for CFF fonts named by convention.
901fn agl_name(ch: char) -> Option<&'static str> {
902    encodings::STANDARD
903        .iter()
904        .chain(encodings::WIN_ANSI.iter())
905        .find(|(_, n)| crate::textparse::glyph_name_to_char(n.as_bytes()) == Some(ch))
906        .map(|(_, n)| *n)
907}
908
909struct Builder(tiny_skia::PathBuilder);
910
911impl ttf_parser::OutlineBuilder for Builder {
912    fn move_to(&mut self, x: f32, y: f32) {
913        self.0.move_to(x, y);
914    }
915    fn line_to(&mut self, x: f32, y: f32) {
916        self.0.line_to(x, y);
917    }
918    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
919        self.0.quad_to(x1, y1, x, y);
920    }
921    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
922        self.0.cubic_to(x1, y1, x2, y2, x, y);
923    }
924    fn close(&mut self) {
925        self.0.close();
926    }
927}
928
929fn outline_face(face: &ttf_parser::Face, gid: GlyphId) -> Option<GlyphPath> {
930    let mut b = Builder(tiny_skia::PathBuilder::new());
931    let bbox = face.outline_glyph(gid, &mut b);
932    let advance = face
933        .glyph_hor_advance(gid)
934        .map(f64::from)
935        .or_else(|| {
936            face.tables()
937                .cff
938                .as_ref()
939                .and_then(|c| c.glyph_width(gid))
940                .map(f64::from)
941        })
942        .unwrap_or(0.0);
943    if bbox.is_none() && gid.0 >= face.number_of_glyphs() {
944        return None;
945    }
946    Some(GlyphPath {
947        path: b.0.finish(),
948        advance,
949    })
950}
951
952fn outline_cff(table: &ttf_parser::cff::Table, gid: GlyphId) -> Option<GlyphPath> {
953    let mut b = Builder(tiny_skia::PathBuilder::new());
954    let ok = table.outline(gid, &mut b).is_ok();
955    let advance = table.glyph_width(gid).map(f64::from).unwrap_or(0.0);
956    if !ok && gid.0 >= table.number_of_glyphs() {
957        return None;
958    }
959    Some(GlyphPath {
960        path: b.0.finish(),
961        advance,
962    })
963}
964
965fn cff_matrix(m: ttf_parser::cff::Matrix) -> Mat {
966    let mat = Mat::new(
967        f64::from(m.sx),
968        f64::from(m.ky),
969        f64::from(m.kx),
970        f64::from(m.sy),
971        f64::from(m.tx),
972        f64::from(m.ty),
973    );
974    if mat.is_finite() && mat.det().abs() > 1e-12 {
975        mat
976    } else {
977        Mat::scale(0.001, 0.001)
978    }
979}
980
981fn sfnt_program(mut data: Vec<u8>) -> Option<Program> {
982    dotsection::strip_sfnt(&mut data);
983    let face = ttf_parser::Face::parse(&data, 0).ok()?;
984    let tables = face.tables();
985    let has_glyf = tables.glyf.is_some();
986    let has_cmap = tables.cmap.is_some();
987    let (matrix, cid_to_gid, names) = match &tables.cff {
988        Some(cff) if !has_glyf => (cff_matrix(cff.matrix()), cid_map(cff), cff_names(cff)),
989        _ => {
990            let upem = f64::from(face.units_per_em()).max(1.0);
991            (Mat::scale(1.0 / upem, 1.0 / upem), None, HashMap::new())
992        }
993    };
994    Some(Program::Sfnt {
995        data,
996        matrix,
997        cid_to_gid,
998        has_cmap,
999        has_glyf,
1000        names,
1001    })
1002}
1003
1004fn cff_program(mut data: Vec<u8>) -> Option<Program> {
1005    dotsection::strip(&mut data);
1006    let table = ttf_parser::cff::Table::parse(&data)?;
1007    let matrix = cff_matrix(table.matrix());
1008    let cid_to_gid = cid_map(&table);
1009    let names = cff_names(&table);
1010    Some(Program::Cff {
1011        data,
1012        matrix,
1013        cid_to_gid,
1014        names,
1015    })
1016}
1017
1018/// glyph name → glyph index over the whole charset (name-keyed fonts only).
1019fn cff_names(table: &ttf_parser::cff::Table) -> HashMap<String, u16> {
1020    let mut map = HashMap::new();
1021    if table.glyph_cid(GlyphId(0)).is_some() {
1022        return map;
1023    }
1024    for g in 0..table.number_of_glyphs() {
1025        if let Some(n) = table.glyph_name(GlyphId(g)) {
1026            map.entry(n.to_string()).or_insert(g);
1027        }
1028    }
1029    map
1030}
1031
1032/// CID → GID for a CID-keyed CFF (`None` for a name-keyed one).
1033fn cid_map(table: &ttf_parser::cff::Table) -> Option<HashMap<u16, u16>> {
1034    let n = table.number_of_glyphs();
1035    // A name-keyed font has no CIDs: probe glyph 1.
1036    table.glyph_cid(GlyphId(0))?;
1037    let mut map = HashMap::with_capacity(n as usize);
1038    for g in 0..n {
1039        if let Some(cid) = table.glyph_cid(GlyphId(g)) {
1040            map.entry(cid).or_insert(g);
1041        }
1042    }
1043    Some(map)
1044}
1045
1046/// A per-document font cache keyed by the font dictionary's object id.
1047#[derive(Default)]
1048pub struct FontCache {
1049    by_id: HashMap<ObjectId, Rc<LoadedFont>>,
1050    /// Direct (unreferenced) font dictionaries, keyed by their debug print.
1051    by_repr: HashMap<String, Rc<LoadedFont>>,
1052}
1053
1054impl FontCache {
1055    pub fn get(&mut self, doc: &Document, obj: &Object) -> Option<Rc<LoadedFont>> {
1056        match obj {
1057            Object::Reference(id) => {
1058                if let Some(f) = self.by_id.get(id) {
1059                    return Some(f.clone());
1060                }
1061                let d = as_dict(doc, obj)?;
1062                let f = Rc::new(LoadedFont::load(doc, d));
1063                self.by_id.insert(*id, f.clone());
1064                Some(f)
1065            }
1066            Object::Dictionary(d) => {
1067                let key = format!("{d:?}");
1068                if let Some(f) = self.by_repr.get(&key) {
1069                    return Some(f.clone());
1070                }
1071                let f = Rc::new(LoadedFont::load(doc, d));
1072                self.by_repr.insert(key, f.clone());
1073                Some(f)
1074            }
1075            _ => None,
1076        }
1077    }
1078}
1079
1080#[allow(dead_code)]
1081fn _unused(_: &Dictionary) -> Option<&[u8]> {
1082    None.and_then(name)
1083}