Skip to main content

pdfrum_font/
fallback.rs

1//! Per-glyph Arial fallback (`ShouldUseFont` / `GetCharPosList`).
2//!
3//! PDFium does not stop at "no glyph → empty cell". After
4//! `GlyphFromCharCode`, `GetCharPosList` can swap the **entire face** for a
5//! lazily created Arial substitute and look the character's Unicode up there.
6//! The original font still supplies the advance, so the rest of the run stays
7//! put while those cells become Arial ink instead of `.notdef` or nothing.
8//!
9//! Two distinct triggers, transcribed from `cpdf_font.cpp:368-389`:
10//!
11//! - `gid == -1` (`None` here) — **any** simple or CID font, embedded or not.
12//! - `gid == 0` and no `/ToUnicode` — **non-embedded TrueType only**.
13//!
14//! Type 3 fonts never take this path: they have no glyph indices, and
15//! `ProcessType3Text` does not call `GetCharPosList`.
16
17use std::sync::OnceLock;
18
19use crate::glyphs::{Charmap, GlyphSource, SynthGlyph};
20use crate::ids::{CharCode, FontFlags, FontId, Gid};
21use crate::subst::{self, CodePage, FontRequest, SubstFont, SubstitutionOptions};
22use pdfrum_common::Diagnostics;
23use pdfrum_common::kurbo::BezPath;
24
25/// The Arial stand-in `LoadSubstFace("Arial", …)` builds on first miss.
26#[derive(Debug)]
27pub struct GlyphFallback {
28    /// The substitute face.
29    pub(crate) glyphs: GlyphSource,
30    /// The synthetic skew and embolden that follow from the request.
31    pub(crate) subst: SubstFont,
32    /// A font identity distinct from the host's, so a glyph-cache entry for
33    /// Arial's gid 5 cannot be confused with the host's gid 5.
34    pub(crate) id: FontId,
35}
36
37impl GlyphFallback {
38    /// This stand-in's identity, for glyph-cache keys.
39    #[must_use]
40    pub fn id(&self) -> FontId {
41        self.id
42    }
43
44    /// The glyph Arial draws for this character, or `None` when Arial has
45    /// none either (`FallbackGlyphFromCharcode` returning `-1`).
46    ///
47    /// Unicode comes from the host's `UnicodeFromCharCode`; an empty mapping
48    /// falls back to the raw character code, which is what the C++ passes to
49    /// `GetCharIndex`.
50    #[must_use]
51    pub fn gid(&self, unicode: &[char], code: CharCode) -> Option<Gid> {
52        let u = unicode.first().copied().map_or(code.0, u32::from);
53        let gid = self.glyphs.char_index(Charmap::Unicode, u);
54        (gid != 0).then_some(Gid(gid))
55    }
56
57    /// A glyph's outline, grid-fitted at 64 ppem, for the bitmap path.
58    #[must_use]
59    pub fn hinted_path(&self, gid: Gid) -> Option<BezPath> {
60        self.glyphs.hinted_outline(gid)
61    }
62
63    /// The face's own advance for this glyph, in 1000/em units. The spacing
64    /// heuristic compares this to the host font's `/Widths`.
65    #[must_use]
66    pub fn advance(&self, gid: Gid) -> i32 {
67        self.glyphs
68            .advance(gid, crate::glyphs::GlyphParams::default())
69    }
70
71    /// The synthetic italic and embolden the bitmap path applies, resolved
72    /// against the device matrix. Arial is never a CID font.
73    #[must_use]
74    pub fn render_synth(&self, xx: i32, xy: i32, vertical: bool) -> Option<SynthGlyph> {
75        let level = self.subst.embolden_level_for_render(false, xx, xy)?;
76        Some(SynthGlyph {
77            skew: self.subst.effective_skew(false),
78            vertical,
79            embolden: f64::from(level) / 64.0,
80        })
81    }
82}
83
84/// Whether this character's glyph is drawn from the host font
85/// (`ShouldUseFont`).
86///
87/// `is_truetype` is the PDF `/Subtype`, not the face: a `CIDFontType2` is not
88/// a TrueType font in this test, so `.notdef` stays `.notdef`.
89#[must_use]
90pub(crate) fn should_use_own_glyph(
91    embedded: bool,
92    is_truetype: bool,
93    has_to_unicode: bool,
94    gid: Option<Gid>,
95) -> bool {
96    let Some(gid) = gid else {
97        return false;
98    };
99    if embedded {
100        return true;
101    }
102    if !is_truetype {
103        return true;
104    }
105    gid != Gid(0) || has_to_unicode
106}
107
108/// Materialize the Arial stand-in on first miss.
109///
110/// Weight is `stem_v * 5`, saturating to 400 on overflow — `FX_SAFE_INT32`
111/// plus `kFontWeightNormal`. The high bit of `host_id` distinguishes this
112/// face from the host without needing the document's [`FontCache`].
113pub(crate) fn ensure(
114    slot: &OnceLock<Option<GlyphFallback>>,
115    host_id: FontId,
116    is_truetype: bool,
117    flags: FontFlags,
118    stem_v: i32,
119    italic_angle: i32,
120    vertical: bool,
121) -> Option<&GlyphFallback> {
122    slot.get_or_init(|| {
123        let weight = i32::try_from(i64::from(stem_v).saturating_mul(5)).unwrap_or(400);
124        let request = FontRequest {
125            name: b"Arial".to_vec(),
126            is_truetype,
127            flags,
128            weight,
129            italic_angle,
130            code_page: CodePage::DefAnsi,
131            vertical,
132        };
133        let resolved = subst::resolve_with_options(
134            &request,
135            &SubstitutionOptions::default(),
136            &mut Diagnostics::with_limit(0),
137        );
138        if !resolved.glyphs.is_some() {
139            return None;
140        }
141        Some(GlyphFallback {
142            glyphs: resolved.glyphs,
143            subst: resolved.subst,
144            id: FontId(host_id.0 | (1 << 63)),
145        })
146    })
147    .as_ref()
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn missing_glyph_always_falls_back() {
156        assert!(!should_use_own_glyph(true, true, true, None));
157        assert!(!should_use_own_glyph(false, false, false, None));
158    }
159
160    #[test]
161    fn embedded_notdef_is_kept() {
162        assert!(should_use_own_glyph(true, true, false, Some(Gid(0))));
163    }
164
165    #[test]
166    fn non_embedded_truetype_notdef_without_tounicode_falls_back() {
167        assert!(!should_use_own_glyph(false, true, false, Some(Gid(0))));
168    }
169
170    #[test]
171    fn tounicode_keeps_a_truetype_notdef() {
172        assert!(should_use_own_glyph(false, true, true, Some(Gid(0))));
173    }
174
175    #[test]
176    fn type1_notdef_is_kept() {
177        assert!(should_use_own_glyph(false, false, false, Some(Gid(0))));
178    }
179
180    #[test]
181    fn a_real_glyph_is_kept() {
182        assert!(should_use_own_glyph(false, true, false, Some(Gid(1))));
183    }
184}