Skip to main content

pdfrum_font/simple/
mod.rs

1//! Simple fonts: one byte in, one glyph out.
2//!
3//! Type 1 and TrueType share everything except the glyph ladder itself, so
4//! they share this module and differ only in which of [`type1`] and
5//! [`truetype`] runs. The **order** of the load steps is behavior, because
6//! each one reads state the previous wrote.
7
8mod truetype;
9mod type1;
10
11use crate::descriptor::{self, FontDescriptor};
12use crate::encoding::{FontEncoding, adobe_char_name, load_differences};
13use crate::fallback::GlyphFallback;
14use crate::glyphs::{Charmap, Face, GlyphSource};
15use crate::ids::GlyphName;
16use crate::subst::{
17    self, CodePage, FontRequest, StandardFont, SubstFont, SubstitutionOptions, strip_subset_prefix,
18};
19use crate::tounicode::{self, ToUnicode};
20use crate::widths::{SimpleWidths, WIDTH_UNSET};
21use crate::{CharCode, CharItem, FontCache, FontFlags, FontId, Gid, names, widths};
22use pdfrum_common::kurbo::Rect;
23use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
24use pdfrum_object::{Dict, Resolve};
25use smallvec::SmallVec;
26use std::sync::OnceLock;
27
28/// The character code an unmapped one borrows its metrics from in a
29/// substituted font (`LoadCharMetrics`'s `LoadCharMetrics(32)` fallback).
30const SPACE: u8 = 32;
31
32/// Which of the two ladders a simple font runs.
33#[cfg(test)]
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum SimpleKind {
36    /// Type 1, MMType1, or a font with no usable `/Subtype`.
37    Type1 {
38        /// The standard font this resolved to, if any. Note an *embedded*
39        /// font is never "standard" even when it is named `Helvetica`.
40        base14: Option<StandardFont>,
41    },
42    /// TrueType.
43    TrueType,
44}
45
46/// A one-byte-per-code font.
47///
48/// The four parallel 256-entry tables are the shape PDFium works in, and they
49/// stay: the ladders write into them in an order that matters, and collapsing
50/// them into one array of records would hide which step wrote what.
51#[derive(Debug)]
52pub struct SimpleFont {
53    /// This font's identity, for glyph-cache keys.
54    pub(crate) id: FontId,
55    /// Where glyphs come from.
56    pub glyphs: GlyphSource,
57    /// Which predefined set the encoding resolved to.
58    pub(crate) encoding_kind: FontEncoding,
59    /// The Unicode each code stands for, as the ladder computed it. **Not** a
60    /// `/ToUnicode` substitute: this is the ladder's own working table, which
61    /// several branches write into and later branches read back.
62    pub(crate) unicodes: [u16; 256],
63    /// The glyph each code selects. `WIDTH_UNSET` means "no glyph", which is
64    /// distinct from glyph 0.
65    ///
66    /// Private for the same reason as [`SimpleWidths::raw`]: a public `[u16;
67    /// 256]` whose `0xffff` entries mean *absence* hands a caller a sentinel
68    /// with no exported name to compare against.
69    /// [`SimpleFont::glyph_from_charcode`] is the predicate, and it already
70    /// answers `Option<Gid>`.
71    pub(crate) glyph_index: [u16; 256],
72    /// The declared widths.
73    pub(crate) widths: SimpleWidths,
74    /// The `/ToUnicode` CMap.
75    pub(crate) to_unicode: Option<ToUnicode>,
76    /// The `/FontDescriptor`'s contents, after repair.
77    pub(crate) descriptor: FontDescriptor,
78    /// What substitution decided, when the font was not embedded.
79    pub(crate) subst: Option<SubstFont>,
80    /// Which ladder ran.
81    #[cfg(test)]
82    pub(crate) kind: SimpleKind,
83    /// Whether a usable font program was embedded. A program that failed to
84    /// parse counts as **not** embedded, which is what routes it to
85    /// substitution.
86    pub(crate) embedded: bool,
87    /// The PDF `/Subtype /TrueType`, not the face. `ShouldUseFont` branches
88    /// on the dictionary, so a substituted Type 1 named like a TrueType font
89    /// still keeps `.notdef`.
90    pub(crate) is_truetype: bool,
91    /// The base font name, subset prefix stripped.
92    pub(crate) base_font_name: Vec<u8>,
93    /// Per-code bounding boxes, filled lazily by the metric derivation.
94    char_bbox: [Rect; 256],
95    /// The Arial stand-in `GetCharPosList` builds on the first glyph the
96    /// ladder could not place. Empty until then.
97    pub(crate) fallback: OnceLock<Option<GlyphFallback>>,
98}
99
100impl SimpleFont {
101    /// The glyph a character code selects, or `None` for "draw nothing".
102    ///
103    /// All the work happened at load time; this is a table read. **Glyph 0 is
104    /// a legitimate result** and is distinct from `None`.
105    #[must_use]
106    pub(crate) fn glyph_from_charcode(&self, code: CharCode) -> Option<Gid> {
107        let index = usize::try_from(code.0).ok()?;
108        match self.glyph_index.get(index) {
109            Some(&WIDTH_UNSET) | None => None,
110            Some(&g) => Some(Gid(g)),
111        }
112    }
113
114    /// The advance width for a code, in 1000/em units.
115    ///
116    /// A code above 255 reads code **0**, not a miss — PDFium's own clamp, and
117    /// the reason a stray wide code draws a space-ish advance rather than
118    /// nothing.
119    #[must_use]
120    pub(crate) fn char_width(&self, code: CharCode) -> f32 {
121        let code = if code.0 > 0xff { 0 } else { code.0 as u8 };
122        if let Some(w) = self.widths.get(code) {
123            return w;
124        }
125        // Nothing declared: ask the face.
126        match self.glyph_from_charcode(CharCode(u32::from(code))) {
127            Some(gid) => f32::from(self.glyphs.advance_tt(gid) as i16),
128            // A code the encoding could not place has no glyph to measure. In
129            // a **substituted** font it borrows the space's metric instead of
130            // reporting nothing, which is `LoadCharMetrics`'s fallback and is
131            // load-bearing far downstream: a run of unmapped codes advances
132            // the pen, so the text object has a non-degenerate box and text
133            // extraction keeps it rather than dropping it whole. An embedded
134            // font gets no such rescue — its own program is the authority on
135            // what it can draw.
136            None if !self.embedded && code != SPACE => self.space_metric(),
137            None => 0.0,
138        }
139    }
140
141    /// The space glyph's advance, which an unmapped code borrows.
142    fn space_metric(&self) -> f32 {
143        if let Some(w) = self.widths.get(SPACE) {
144            return w;
145        }
146        match self.glyph_from_charcode(CharCode(u32::from(SPACE))) {
147            Some(gid) => f32::from(self.glyphs.advance_tt(gid) as i16),
148            None => 0.0,
149        }
150    }
151
152    /// The Unicode a code stands for, `/ToUnicode` first and the ladder's own
153    /// table second.
154    #[must_use]
155    pub(crate) fn unicode_from_charcode(&self, code: CharCode) -> SmallVec<[char; 2]> {
156        if let Some(tu) = &self.to_unicode {
157            let chars = tu.lookup(code);
158            if !chars.is_empty() {
159                return chars;
160            }
161        }
162        let Ok(index) = usize::try_from(code.0) else {
163            return SmallVec::new();
164        };
165        match self.unicodes.get(index) {
166            Some(&0) | None => {
167                // The ladder left this code unmapped. A symbolic TrueType
168                // still has an encoding table (`MsSymbol` has no glyph
169                // names, so the name-driven fill never runs), and the Arial
170                // stand-in looks that Unicode up rather than treating the
171                // raw byte as WinAnsi — `bug_1442723`.
172                match self
173                    .encoding_kind
174                    .unicodes()
175                    .and_then(|table| table.get(index))
176                    .copied()
177                {
178                    Some(0) | None => SmallVec::new(),
179                    Some(u) => char::from_u32(u32::from(u))
180                        .map(|c| SmallVec::from_slice(&[c]))
181                        .unwrap_or_default(),
182                }
183            }
184            Some(&u) => char::from_u32(u32::from(u))
185                .map(|c| SmallVec::from_slice(&[c]))
186                .unwrap_or_default(),
187        }
188    }
189
190    /// The character code that produces `unicode`, or `None`.
191    ///
192    /// `/ToUnicode`'s reverse map first, then a scan of the ladder's own
193    /// Unicode table. Appearance generation needs this to *write* text with a
194    /// font the document already carries, which is the opposite direction from
195    /// everything else here.
196    #[must_use]
197    pub(crate) fn char_code_from_unicode(&self, unicode: char) -> Option<CharCode> {
198        if let Some(tu) = &self.to_unicode {
199            let code = tu.reverse(unicode);
200            if code.0 != 0 {
201                return Some(code);
202            }
203        }
204        let target = u16::try_from(u32::from(unicode)).ok()?;
205        if target == 0 {
206            return None;
207        }
208        // The ladder's table is the same one `unicode_from_charcode` reads, so
209        // a code found here round-trips by construction.
210        self.unicodes
211            .iter()
212            .position(|&u| u == target)
213            .and_then(|i| u32::try_from(i).ok())
214            .map(CharCode)
215    }
216
217    /// The bounding box for a code, in 1000/em units.
218    ///
219    /// A code the encoding could not place borrows the **space's** box in a
220    /// substituted font, the same rescue [`char_width`](Self::char_width)
221    /// applies and for the same reason.
222    #[must_use]
223    pub(crate) fn char_bbox(&self, code: CharCode) -> Rect {
224        let code = if code.0 > 0xff { 0 } else { code.0 as u8 };
225        let stored = self
226            .char_bbox
227            .get(usize::from(code))
228            .copied()
229            .unwrap_or(Rect::ZERO);
230        if stored != Rect::ZERO
231            || self.embedded
232            || code == SPACE
233            || self
234                .glyph_from_charcode(CharCode(u32::from(code)))
235                .is_some()
236        {
237            return stored;
238        }
239        self.char_bbox
240            .get(usize::from(SPACE))
241            .copied()
242            .unwrap_or(Rect::ZERO)
243    }
244
245    /// Build one [`CharItem`].
246    pub(crate) fn char_item(&self, code: CharCode) -> CharItem {
247        let gid = self.glyph_from_charcode(code);
248        CharItem {
249            code,
250            cid: None,
251            gid,
252            unicode: self.unicode_from_charcode(code),
253            width: self.char_width(code),
254            vertical_glyph: false,
255        }
256    }
257
258    /// Whether the PDF declared widths, which gates the glyph-spacing
259    /// heuristic (`HasFontWidths`).
260    #[must_use]
261    pub(crate) fn has_font_widths(&self) -> bool {
262        self.widths.has_declared_widths()
263    }
264
265    /// Whether this font resolved to one of the standard fourteen **and** is
266    /// not embedded — an embedded font named `Helvetica` is not standard
267    /// (`IsStandardFont`).
268    #[cfg(test)]
269    #[must_use]
270    pub(crate) fn is_standard_font(&self) -> bool {
271        matches!(self.kind, SimpleKind::Type1 { base14: Some(_) }) && !self.embedded
272    }
273}
274
275/// Load a simple font (`LoadCommon`).
276///
277/// **Cannot fail.** Every path returns a font, even one with no program, no
278/// encoding and no glyphs at all — which is why the public entry point's
279/// `Option` is about Type0 fonts only.
280// One ordered sequence: every step reads state the steps above it left in
281// `flags`, `encoding` and `base_font_name`, and *when* each write happens is
282// the behavior. Helpers would move those writes behind call sites and hide the
283// order, so the sequence stays whole.
284#[allow(clippy::too_many_lines)]
285pub(crate) fn load(
286    dict: &Dict,
287    r: &impl Resolve,
288    cache: &FontCache,
289    opts: &SubstitutionOptions,
290    limits: &Limits,
291    diags: &mut Diagnostics,
292    is_truetype: bool,
293) -> SimpleFont {
294    let mut base_font_name = dict
295        .name(names::BASE_FONT)
296        .map(|n| n.as_bytes().to_vec())
297        .unwrap_or_default();
298
299    // The base-14 detection runs *before* the descriptor, and what it writes
300    // to `flags` survives only when there is no descriptor at all.
301    let base14 = if is_truetype {
302        None
303    } else {
304        subst::standard_font_index(&base_font_name)
305    };
306    let mut flags = FontFlags::DEFAULT;
307    let mut encoding_kind = FontEncoding::Builtin;
308    let mut widths_table = SimpleWidths::default();
309    if let Some(f) = base14 {
310        base_font_name = subst::canonical_font_name(f).as_bytes().to_vec();
311        flags = if f.is_symbolic() {
312            FontFlags::SYMBOLIC
313        } else {
314            FontFlags::NON_SYMBOLIC
315        };
316        if f.is_fixed() {
317            // The four Couriers: every glyph 600 units wide.
318            widths_table = SimpleWidths {
319                raw: [600; 256],
320                use_face_widths: false,
321            };
322        }
323        encoding_kind = match f {
324            StandardFont::Symbol => FontEncoding::AdobeSymbol,
325            StandardFont::Dingbats => FontEncoding::ZapfDingbats,
326            _ if flags.is_non_symbolic() => FontEncoding::Standard,
327            _ => encoding_kind,
328        };
329    }
330
331    // Step 1 — the descriptor, which overwrites `flags` when it exists.
332    let desc = dict.dict(names::FONT_DESCRIPTOR, r);
333    let mut descriptor = FontDescriptor {
334        flags,
335        ..FontDescriptor::default()
336    };
337    if let Some(d) = &desc {
338        descriptor = descriptor::load(d, r);
339    }
340
341    // The font program, whichever key carries it — the `/FontFile3` subtype is
342    // never read, so the three keys are interchangeable.
343    let (mut glyphs, mut embedded) = load_font_program(desc.as_ref(), r, limits, diags);
344
345    // Step 2 — widths. A base-14 Courier's fixed widths are only kept when the
346    // PDF declared none of its own.
347    let declared = widths::load_simple(dict, desc.as_ref(), r);
348    if declared.has_declared_widths() || !widths_table.has_declared_widths() {
349        widths_table = declared;
350    }
351
352    // Step 3 — strip a subset prefix, or substitute.
353    let mut subst_font = None;
354    if embedded {
355        base_font_name = strip_subset_prefix(&base_font_name).to_vec();
356    } else {
357        let request = FontRequest {
358            name: base_font_name.clone(),
359            is_truetype,
360            flags: descriptor.flags,
361            weight: descriptor.subst_weight(),
362            italic_angle: descriptor.italic_angle,
363            code_page: CodePage::DefAnsi,
364            vertical: false,
365        };
366        let s = substitute(&request, opts, diags);
367        glyphs = s.glyphs;
368        subst_font = Some(s.subst);
369    }
370
371    // Step 4 — a *reset*, not a default: a non-symbolic font's encoding is
372    // overwritten with Standard even when step 0 chose something else.
373    if !descriptor.flags.is_symbolic() {
374        encoding_kind = FontEncoding::Standard;
375    }
376
377    // Step 5 — the PDF's own encoding.
378    let mut differences: [Option<GlyphName>; 256] = [const { None }; 256];
379    let _has_differences = load_pdf_encoding(
380        dict,
381        r,
382        &base_font_name,
383        descriptor.flags,
384        embedded,
385        is_truetype,
386        &mut encoding_kind,
387        &mut differences,
388    );
389
390    let to_unicode = load_to_unicode(dict, r, limits, diags);
391
392    // Step 6 — the ladder.
393    let mut unicodes = [0u16; 256];
394    let mut glyph_index = [WIDTH_UNSET; 256];
395    if glyphs.is_some() {
396        let ctx = LadderContext {
397            glyphs: &glyphs,
398            encoding: encoding_kind,
399            differences: &differences,
400            flags: descriptor.flags,
401            embedded,
402            base14,
403            to_unicode: to_unicode.as_ref(),
404            first_char: dict.int(names::FIRST_CHAR, r).unwrap_or(0),
405        };
406        if is_truetype {
407            truetype::load_glyph_map(&ctx, &mut unicodes, &mut glyph_index);
408        } else {
409            type1::load_glyph_map(&ctx, &mut unicodes, &mut glyph_index);
410        }
411    }
412
413    // Step 9 — the all-caps aliasing, which for a **non-embedded** font
414    // replaces lowercase glyphs *even when they mapped successfully*.
415    if descriptor.flags.is_all_cap() {
416        apply_all_caps(&mut glyph_index, &mut widths_table, embedded);
417    }
418
419    // Step 10 — derive whatever metrics the PDF failed to declare.
420    let mut char_bbox = [Rect::ZERO; 256];
421    for (code, slot) in char_bbox.iter_mut().enumerate() {
422        let Some(&g) = glyph_index.get(code) else {
423            continue;
424        };
425        if g == WIDTH_UNSET {
426            continue;
427        }
428        if let Some(b) = glyphs.glyph_bbox(Gid(g)) {
429            *slot = b;
430        }
431    }
432    let metrics = match &glyphs {
433        GlyphSource::Fontations(f) => f.metrics(),
434        GlyphSource::Type1(f) => Some(descriptor::FaceMetrics {
435            upem: f.units_per_em(),
436            bbox_left: f.bbox().x0 as i64,
437            bbox_top: f.bbox().y1 as i64,
438            bbox_right: f.bbox().x1 as i64,
439            bbox_bottom: f.bbox().y0 as i64,
440            ascender: f.bbox().y1 as i64,
441            descender: f.bbox().y0 as i64,
442        }),
443        GlyphSource::None => None,
444    };
445    descriptor::check_font_metrics(&mut descriptor, metrics, |c| {
446        char_bbox.get(usize::from(c)).copied().unwrap_or(Rect::ZERO)
447    });
448
449    if !embedded && !glyphs.is_some() {
450        diags.record(Severity::Suspicious, DiagKind::FontSubstitutionFailed, None);
451    }
452    if !glyphs.is_some() {
453        embedded = false;
454    }
455
456    SimpleFont {
457        id: cache.next_id(),
458        glyphs,
459        encoding_kind,
460        unicodes,
461        glyph_index,
462        widths: widths_table,
463        to_unicode,
464        descriptor,
465        subst: subst_font,
466        #[cfg(test)]
467        kind: if is_truetype {
468            SimpleKind::TrueType
469        } else {
470            SimpleKind::Type1 { base14 }
471        },
472        embedded,
473        is_truetype,
474        base_font_name,
475        char_bbox,
476        fallback: OnceLock::new(),
477    }
478}
479
480/// What a ladder needs to decide a glyph.
481pub(crate) struct LadderContext<'a> {
482    pub glyphs: &'a GlyphSource,
483    pub encoding: FontEncoding,
484    pub differences: &'a [Option<GlyphName>; 256],
485    pub flags: FontFlags,
486    pub embedded: bool,
487    pub base14: Option<StandardFont>,
488    pub to_unicode: Option<&'a ToUnicode>,
489    pub first_char: i64,
490}
491
492impl LadderContext<'_> {
493    /// The merged glyph name for a code.
494    pub(crate) fn char_name(&self, code: u8) -> Option<&[u8]> {
495        adobe_char_name(self.encoding, self.differences, u32::from(code))
496    }
497
498    /// Whether `/Differences` supplied any names at all, which changes what
499    /// `char_name` can return for a `Builtin` encoding.
500    pub(crate) fn has_differences(&self) -> bool {
501        self.differences.iter().any(Option::is_some)
502    }
503}
504
505/// Read a font program from whichever of the three keys carries one.
506///
507/// The keys are tried in order and **the first present wins**; `/FontFile3`'s
508/// own `/Subtype` is never consulted, so a CFF under `/FontFile2` loads fine
509/// and so does a TrueType program under `/FontFile`. Format detection is
510/// entirely the backend's job.
511pub(crate) fn load_font_program(
512    desc: Option<&Dict>,
513    r: &impl Resolve,
514    limits: &Limits,
515    diags: &mut Diagnostics,
516) -> (GlyphSource, bool) {
517    let Some(desc) = desc else {
518        return (GlyphSource::None, false);
519    };
520    let stream = [names::FONT_FILE, names::FONT_FILE2, names::FONT_FILE3]
521        .into_iter()
522        .find_map(|k| desc.stream(k, r));
523    let Some(stream) = stream else {
524        return (GlyphSource::None, false);
525    };
526
527    // `/Length1`, `/Length2` and `/Length3` are a buffer hint only — PDFium
528    // sums them for sizing and then discards them, and never uses them to
529    // split a PFB. Trusting them loses fonts, because they are often wrong.
530    let bytes = pdfrum_filters::decode_chain(&stream, 0, r, limits, diags).data;
531    if bytes.is_empty() {
532        diags.record(Severity::Suspicious, DiagKind::FontProgramUnreadable, None);
533        return (GlyphSource::None, false);
534    }
535    let shared: std::sync::Arc<[u8]> = std::sync::Arc::from(bytes.as_slice());
536
537    if let Some(face) = Face::new(shared.clone(), 0) {
538        return (GlyphSource::Fontations(face), true);
539    }
540    // Not a table-directory font: try Type 1, which is the one format
541    // Fontations does not read end to end.
542    if let Ok(f) = pdfrum_type1::Type1Font::parse(&shared, limits, diags) {
543        (GlyphSource::Type1(std::sync::Arc::new(f)), true)
544    } else {
545        // A program nothing can read nulls the font file, which makes
546        // `IsEmbedded()` false and routes the font to substitution.
547        diags.record(Severity::Suspicious, DiagKind::FontProgramUnreadable, None);
548        (GlyphSource::None, false)
549    }
550}
551
552/// Run substitution against whichever database the options select.
553fn substitute(
554    request: &FontRequest,
555    opts: &SubstitutionOptions,
556    diags: &mut Diagnostics,
557) -> subst::Substitution {
558    subst::resolve_with_options(request, opts, diags)
559}
560
561/// `/Encoding` resolution (`LoadPDFEncoding`).
562///
563/// Returns whether `/Differences` supplied anything. Three rewrites in here
564/// look arbitrary and are not: `/MacExpertEncoding` named directly becomes
565/// WinAnsi **unconditionally**, while through `/BaseEncoding` it becomes
566/// WinAnsi only for a TrueType font — so `MacExpert` is reachable only through
567/// a non-TrueType font's `/BaseEncoding`.
568#[allow(clippy::too_many_arguments)]
569pub(crate) fn load_pdf_encoding(
570    dict: &Dict,
571    r: &impl Resolve,
572    base_font_name: &[u8],
573    flags: FontFlags,
574    embedded: bool,
575    is_truetype: bool,
576    encoding: &mut FontEncoding,
577    differences: &mut [Option<GlyphName>; 256],
578) -> bool {
579    let Some(enc) = dict.get(names::ENCODING, r) else {
580        if base_font_name == b"Symbol" {
581            *encoding = if is_truetype {
582                FontEncoding::MsSymbol
583            } else {
584                FontEncoding::AdobeSymbol
585            };
586        } else if !embedded && *encoding == FontEncoding::Builtin {
587            *encoding = FontEncoding::WinAnsi;
588        }
589        return false;
590    };
591
592    if let Some(name) = enc.as_name() {
593        // A symbolic set already chosen is never overridden by a name.
594        if matches!(
595            *encoding,
596            FontEncoding::AdobeSymbol | FontEncoding::ZapfDingbats
597        ) {
598            return false;
599        }
600        if flags.is_symbolic() && base_font_name == b"Symbol" {
601            if !is_truetype {
602                *encoding = FontEncoding::AdobeSymbol;
603            }
604            return false;
605        }
606        let mut spelling = name.as_bytes();
607        if spelling == b"MacExpertEncoding" {
608            spelling = b"WinAnsiEncoding";
609        }
610        if let Some(e) = FontEncoding::from_pdf_name(spelling) {
611            *encoding = e;
612        }
613        return false;
614    }
615
616    let Some(enc_dict) = enc.as_dict() else {
617        // An array, a number, anything else: nothing happens at all.
618        return false;
619    };
620    if !matches!(
621        *encoding,
622        FontEncoding::AdobeSymbol | FontEncoding::ZapfDingbats
623    ) && let Some(base) = enc_dict.name(names::BASE_ENCODING)
624    {
625        let mut spelling = base.as_bytes();
626        if is_truetype && spelling == b"MacExpertEncoding" {
627            spelling = b"WinAnsiEncoding";
628        }
629        if let Some(e) = FontEncoding::from_pdf_name(spelling) {
630            *encoding = e;
631        }
632    }
633    if (!embedded || is_truetype) && *encoding == FontEncoding::Builtin {
634        *encoding = FontEncoding::Standard;
635    }
636    match enc_dict.array(names::DIFFERENCES, r) {
637        Some(diffs) => load_differences(&diffs, r, differences),
638        None => false,
639    }
640}
641
642/// Read and parse `/ToUnicode`.
643pub(crate) fn load_to_unicode(
644    dict: &Dict,
645    r: &impl Resolve,
646    limits: &Limits,
647    diags: &mut Diagnostics,
648) -> Option<ToUnicode> {
649    let stream = dict.stream(names::TO_UNICODE, r)?;
650    let bytes = pdfrum_filters::decode_chain(&stream, 0, r, limits, diags).data;
651    let map = tounicode::parse(&bytes, limits, diags);
652    if map.is_empty() { None } else { Some(map) }
653}
654
655/// The all-caps glyph aliasing.
656///
657/// For each of three ranges, a lowercase code borrows the glyph 32 codes
658/// below it. The guard is the surprising part: an **embedded** font keeps a
659/// glyph it already mapped, while a **non-embedded** one has its lowercase
660/// glyphs replaced even when they mapped perfectly well.
661fn apply_all_caps(glyph_index: &mut [u16; 256], widths: &mut SimpleWidths, embedded: bool) {
662    for (lo, hi) in [(b'a', b'z'), (0xE0u8, 0xF6u8), (0xF8, 0xFD)] {
663        for i in lo..=hi {
664            let idx = usize::from(i);
665            if glyph_index.get(idx) != Some(&WIDTH_UNSET) && embedded {
666                continue;
667            }
668            let Some(j) = idx.checked_sub(32) else {
669                continue;
670            };
671            let (Some(&src_glyph), Some(&src_width)) = (glyph_index.get(j), widths.raw.get(j))
672            else {
673                continue;
674            };
675            if let Some(slot) = glyph_index.get_mut(idx) {
676                *slot = src_glyph;
677            }
678            // Note `!= 0`, not `!= WIDTH_UNSET`: an *unset* width is nonzero
679            // and therefore propagates.
680            if src_width != 0
681                && let Some(slot) = widths.raw.get_mut(idx)
682            {
683                *slot = src_width;
684            }
685        }
686    }
687}
688
689/// Look a glyph up by name in a face, for the ladders.
690pub(crate) fn name_index(glyphs: &GlyphSource, name: &[u8]) -> u16 {
691    glyphs.name_index(name)
692}
693
694/// Look a code up through a charmap, for the ladders.
695pub(crate) fn char_index(glyphs: &GlyphSource, charmap: Charmap, code: u32) -> u16 {
696    glyphs.char_index(charmap, code)
697}
698
699/// Drawn-outline access, so a caller need not reach into `glyphs`.
700impl SimpleFont {
701    /// The outline for a glyph, in 1000/em text space.
702    #[cfg(test)]
703    #[must_use]
704    pub(crate) fn glyph_path(&self, gid: Gid) -> Option<pdfrum_common::kurbo::BezPath> {
705        self.glyphs
706            .outline(gid, crate::glyphs::GlyphParams::default())
707    }
708}
709
710/// A resolved `/Encoding` value, exposed for tests of the decision table.
711#[cfg(test)]
712pub(crate) fn resolve_encoding_for_test(
713    dict: &Dict,
714    r: &impl Resolve,
715    base_font_name: &[u8],
716    flags: FontFlags,
717    embedded: bool,
718    is_truetype: bool,
719    prior: FontEncoding,
720) -> (FontEncoding, bool) {
721    let mut e = prior;
722    let mut diffs: [Option<GlyphName>; 256] = [const { None }; 256];
723    let had = load_pdf_encoding(
724        dict,
725        r,
726        base_font_name,
727        flags,
728        embedded,
729        is_truetype,
730        &mut e,
731        &mut diffs,
732    );
733    (e, had)
734}
735
736#[cfg(test)]
737use pdfrum_object::Object;
738
739#[cfg(test)]
740#[path = "simple_tests.rs"]
741mod tests;