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