Skip to main content

pdfrum_font/
type3.rs

1//! Type 3 fonts: glyphs that are content streams.
2//!
3//! A Type 3 font has no face, no outlines and no glyph indices — its glyphs
4//! are `/CharProcs` entries that the page layer executes as content streams in
5//! the font's own coordinate system. This crate's job is therefore only to
6//! resolve a character code to the *name* of a procedure, and to carry the
7//! matrix and widths that place it.
8
9use crate::encoding::{FontEncoding, adobe_char_name};
10use crate::ids::GlyphName;
11use crate::tounicode::{self, ToUnicode};
12use crate::{CharCode, CharItem, FontCache, FontId, names, simple};
13use pdfrum_common::kurbo::{Affine, Rect};
14use pdfrum_common::{Diagnostics, Limits};
15use pdfrum_object::{Dict, Resolve, Stream};
16use smallvec::SmallVec;
17
18/// The maximum nesting of Type 3 glyph procedures, which may themselves show
19/// text in a Type 3 font (`kMaxType3FormLevel`). Ported verbatim.
20pub const MAX_TYPE3_DEPTH: u32 = 4;
21
22/// A font whose glyphs are content streams.
23#[derive(Debug)]
24pub struct Type3Font {
25    /// This font's identity.
26    pub(crate) id: FontId,
27    /// `/FontMatrix`, mapping glyph space to text space.
28    pub font_matrix: Affine,
29    /// `/CharProcs`: glyph name to content stream.
30    pub(crate) char_procs: Dict,
31    /// `/Resources` for the glyph procedures, which fall back to the page's.
32    pub resources: Option<Dict>,
33    /// The `/Differences` overlay. **Not cleared after loading**, unlike a
34    /// simple font's — a Type 3 font consults it for the life of the font,
35    /// because it is the only way a code names a procedure.
36    pub(crate) encoding: [Option<GlyphName>; 256],
37    /// Which predefined set the encoding resolved to, usually `Builtin`.
38    pub(crate) encoding_kind: FontEncoding,
39    /// Widths in **glyph space × 1000**, `0` meaning "ask the procedure".
40    ///
41    /// An `i32` table defaulting to zero, unlike a simple font's `u16` table
42    /// defaulting to the unset sentinel.
43    pub(crate) widths: [i32; 256],
44    /// `/FontBBox`, scaled into glyph units.
45    pub(crate) font_bbox: Rect,
46    /// The `/ToUnicode` CMap.
47    pub(crate) to_unicode: Option<ToUnicode>,
48}
49
50impl Type3Font {
51    /// The glyph procedure for a character code.
52    ///
53    /// `None` when the code names nothing — which for a font with **no
54    /// `/Encoding` at all** is every code, because the encoding stays
55    /// `Builtin` and only `/Differences` can name a procedure.
56    #[must_use]
57    pub fn char_proc(&self, code: CharCode, r: &impl Resolve) -> Option<Stream> {
58        let name = self.char_proc_name(code)?;
59        let key = pdfrum_object::Name::new(name.to_vec());
60        self.char_procs.stream(&key, r)
61    }
62
63    /// The name of the glyph procedure for a code.
64    #[must_use]
65    pub(crate) fn char_proc_name(&self, code: CharCode) -> Option<&[u8]> {
66        adobe_char_name(self.encoding_kind, &self.encoding, code.0)
67    }
68
69    /// The advance width for a code, in glyph units.
70    ///
71    /// A code at or above 256 reads code **0**, matching the simple-font
72    /// clamp. Zero means the PDF declared nothing and the procedure's own
73    /// `d0`/`d1` operator supplies the width — which only the page layer can
74    /// see, so this returns 0 and the caller asks it.
75    #[must_use]
76    pub(crate) fn char_width(&self, code: CharCode) -> f32 {
77        let code = if code.0 >= 256 { 0 } else { code.0 as usize };
78        self.widths.get(code).copied().unwrap_or(0) as f32
79    }
80
81    /// The Unicode a code stands for, from `/ToUnicode` only — a Type 3 font
82    /// has no encoding table to fall back on.
83    #[must_use]
84    pub(crate) fn unicode_from_charcode(&self, code: CharCode) -> SmallVec<[char; 2]> {
85        self.to_unicode
86            .as_ref()
87            .map(|tu| tu.lookup(code))
88            .unwrap_or_default()
89    }
90
91    /// The character code that produces `unicode`, or `None`.
92    ///
93    /// A Type 3 font has no encoding table to scan, so this is `/ToUnicode`'s
94    /// reverse map or nothing.
95    #[must_use]
96    pub(crate) fn char_code_from_unicode(&self, unicode: char) -> Option<CharCode> {
97        let code = self.to_unicode.as_ref()?.reverse(unicode);
98        (code.0 != 0).then_some(code)
99    }
100
101    pub(crate) fn char_item(&self, code: CharCode) -> CharItem {
102        CharItem {
103            code,
104            cid: None,
105            // A Type 3 font has no glyph indices by construction; the C++'s
106            // `GlyphFromCharCode` returns -1 unconditionally.
107            gid: None,
108            unicode: self.unicode_from_charcode(code),
109            width: self.char_width(code),
110            vertical_glyph: false,
111        }
112    }
113}
114
115/// Load a Type 3 font.
116///
117/// Cannot fail. Note `/Encoding` is read **only when present**: a Type 3 font
118/// without one keeps a `Builtin` encoding, which names nothing, so no glyph
119/// resolves at all — a real and reachable state.
120pub(crate) fn load(
121    dict: &Dict,
122    r: &impl Resolve,
123    cache: &FontCache,
124    limits: &Limits,
125    diags: &mut Diagnostics,
126) -> Type3Font {
127    let (font_matrix, xscale, yscale) = match dict.raw(names::FONT_MATRIX) {
128        Some(_) => {
129            let m = dict.matrix(names::FONT_MATRIX, r);
130            let c = m.as_coeffs();
131            (m, c[0], c[3])
132        }
133        None => (Affine::IDENTITY, 1.0, 1.0),
134    };
135
136    let font_bbox = match dict.array(names::FONT_BBOX, r) {
137        Some(b) => {
138            // Scaled by the matrix's diagonal, then into glyph units.
139            Rect::new(
140                f64::from(b.number_at_or_zero(0)) * xscale * 1000.0,
141                f64::from(b.number_at_or_zero(1)) * yscale * 1000.0,
142                f64::from(b.number_at_or_zero(2)) * xscale * 1000.0,
143                f64::from(b.number_at_or_zero(3)) * yscale * 1000.0,
144            )
145        }
146        None => Rect::ZERO,
147    };
148
149    let mut widths = [0i32; 256];
150    let start = dict.int(names::FIRST_CHAR, r).unwrap_or(0);
151    if let (Ok(start), Some(w)) = (usize::try_from(start), dict.array(names::WIDTHS, r))
152        && start < 256
153    {
154        let count = w.len().min(256).min(256 - start);
155        for i in 0..count {
156            let value = f64::from(w.number_at_or_zero(i)) * xscale * 1000.0;
157            if let Some(slot) = widths.get_mut(start + i) {
158                // The C++ rounds through a float, not a truncation.
159                *slot = round_f32(value);
160            }
161        }
162    }
163
164    let mut encoding: [Option<GlyphName>; 256] = [const { None }; 256];
165    let mut encoding_kind = FontEncoding::Builtin;
166    if dict.raw(names::ENCODING).is_some() {
167        simple::load_pdf_encoding(
168            dict,
169            r,
170            b"",
171            crate::FontFlags::DEFAULT,
172            false,
173            false,
174            &mut encoding_kind,
175            &mut encoding,
176        );
177    }
178
179    let to_unicode = dict
180        .stream(names::TO_UNICODE, r)
181        .map(|s| {
182            let bytes = pdfrum_filters::decode_chain(&s, 0, r, limits, diags).data;
183            tounicode::parse(&bytes, limits, diags)
184        })
185        .filter(|m| !m.is_empty());
186
187    Type3Font {
188        id: cache.next_id(),
189        font_matrix,
190        char_procs: dict.dict(names::CHAR_PROCS, r).unwrap_or_default(),
191        resources: dict.dict(names::RESOURCES, r),
192        encoding,
193        encoding_kind,
194        widths,
195        font_bbox,
196        to_unicode,
197    }
198}
199
200fn round_f32(v: f64) -> i32 {
201    let r = v.round();
202    if r.is_nan() {
203        0
204    } else if r >= f64::from(i32::MAX) {
205        i32::MAX
206    } else if r <= f64::from(i32::MIN) {
207        i32::MIN
208    } else {
209        r as i32
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    // Test expectations are exact values by design.
216    #![allow(clippy::float_cmp)]
217    use super::*;
218    use pdfrum_object::{Array, Name, NoResolve, Object};
219
220    fn font_dict(pairs: Vec<(&Name, Object)>) -> Dict {
221        Dict::from_pairs(pairs.into_iter().map(|(k, v)| (k.clone(), v)))
222    }
223
224    fn load_it(dict: &Dict) -> Type3Font {
225        load(
226            dict,
227            &NoResolve,
228            &FontCache::new(),
229            &Limits::default(),
230            &mut Diagnostics::default(),
231        )
232    }
233
234    #[test]
235    fn without_an_encoding_no_code_names_anything() {
236        // The reachable dead end: a `Builtin` encoding with no `/Differences`
237        // means `char_proc_name` is `None` for every one of the 256 codes.
238        let f = load_it(&Dict::new());
239        assert_eq!(f.encoding_kind, FontEncoding::Builtin);
240        for code in 0..256u32 {
241            assert!(f.char_proc_name(CharCode(code)).is_none(), "code {code}");
242        }
243    }
244
245    #[test]
246    fn differences_name_the_procedures() {
247        let enc = font_dict(vec![(
248            names::DIFFERENCES,
249            Object::Array(Array::of([
250                Object::Int(97),
251                Object::Name(Name::from("square")),
252                Object::Name(Name::from("triangle")),
253            ])),
254        )]);
255        let f = load_it(&font_dict(vec![(names::ENCODING, Object::Dict(enc))]));
256        assert_eq!(f.char_proc_name(CharCode(97)), Some(&b"square"[..]));
257        assert_eq!(f.char_proc_name(CharCode(98)), Some(&b"triangle"[..]));
258        // An `/Encoding` **dictionary** promotes the encoding from `Builtin`
259        // to `Standard`, so an uncovered code falls through to the
260        // predefined set rather than naming nothing — which is a different
261        // outcome from a font with no `/Encoding` at all.
262        assert_eq!(f.encoding_kind, FontEncoding::Standard);
263        assert_eq!(f.char_proc_name(CharCode(99)), Some(&b"c"[..]));
264    }
265
266    #[test]
267    fn the_font_matrix_defaults_to_identity() {
268        let f = load_it(&Dict::new());
269        assert_eq!(f.font_matrix, Affine::IDENTITY);
270    }
271
272    #[test]
273    fn widths_are_scaled_by_the_matrix_and_by_a_thousand() {
274        let f = load_it(&font_dict(vec![
275            (
276                names::FONT_MATRIX,
277                Object::Array(Array::of([
278                    Object::Real(0.01),
279                    Object::Int(0),
280                    Object::Int(0),
281                    Object::Real(0.01),
282                    Object::Int(0),
283                    Object::Int(0),
284                ])),
285            ),
286            (names::FIRST_CHAR, Object::Int(97)),
287            (
288                names::WIDTHS,
289                Object::Array(Array::of([Object::Int(50), Object::Int(75)])),
290            ),
291        ]));
292        // 50 * 0.01 * 1000 == 500.
293        assert_eq!(f.char_width(CharCode(97)), 500.0);
294        assert_eq!(f.char_width(CharCode(98)), 750.0);
295        // Undeclared codes are zero, meaning "ask the procedure".
296        assert_eq!(f.char_width(CharCode(99)), 0.0);
297    }
298
299    #[test]
300    fn a_code_at_or_above_256_reads_code_zero() {
301        let f = load_it(&font_dict(vec![
302            (names::FIRST_CHAR, Object::Int(0)),
303            (names::WIDTHS, Object::Array(Array::of([Object::Int(1)]))),
304        ]));
305        // Width at code 0 is 1 * 1 * 1000.
306        assert_eq!(f.char_width(CharCode(0)), 1000.0);
307        assert_eq!(f.char_width(CharCode(256)), 1000.0);
308        assert_eq!(f.char_width(CharCode(u32::MAX)), 1000.0);
309    }
310
311    #[test]
312    fn widths_beyond_the_table_are_dropped_without_wrapping() {
313        let f = load_it(&font_dict(vec![
314            (names::FIRST_CHAR, Object::Int(254)),
315            (
316                names::WIDTHS,
317                Object::Array(Array::of([
318                    Object::Int(1),
319                    Object::Int(2),
320                    Object::Int(3),
321                    Object::Int(4),
322                ])),
323            ),
324        ]));
325        assert_eq!(f.char_width(CharCode(254)), 1000.0);
326        assert_eq!(f.char_width(CharCode(255)), 2000.0);
327        // The remaining two had nowhere to go and did not wrap to code 0.
328        assert_eq!(f.char_width(CharCode(0)), 0.0);
329    }
330
331    #[test]
332    fn a_first_char_past_the_table_drops_every_width() {
333        let f = load_it(&font_dict(vec![
334            (names::FIRST_CHAR, Object::Int(300)),
335            (names::WIDTHS, Object::Array(Array::of([Object::Int(9)]))),
336        ]));
337        assert!(f.widths.iter().all(|&w| w == 0));
338    }
339
340    #[test]
341    fn the_bbox_is_scaled_into_glyph_units() {
342        let f = load_it(&font_dict(vec![
343            (
344                names::FONT_MATRIX,
345                Object::Array(Array::of([
346                    Object::Real(0.001),
347                    Object::Int(0),
348                    Object::Int(0),
349                    Object::Real(0.001),
350                    Object::Int(0),
351                    Object::Int(0),
352                ])),
353            ),
354            (
355                names::FONT_BBOX,
356                Object::Array(Array::of([
357                    Object::Int(0),
358                    Object::Int(0),
359                    Object::Int(1000),
360                    Object::Int(1000),
361                ])),
362            ),
363        ]));
364        // 1000 * 0.001 * 1000 == 1000, up to the float representation of
365        // 0.001 — which is why this compares within a unit rather than exactly.
366        assert!((f.font_bbox.x1 - 1000.0).abs() < 1.0, "{:?}", f.font_bbox);
367        assert!((f.font_bbox.y1 - 1000.0).abs() < 1.0, "{:?}", f.font_bbox);
368        assert_eq!((f.font_bbox.x0, f.font_bbox.y0), (0.0, 0.0));
369    }
370
371    #[test]
372    fn a_type3_font_has_no_glyphs_at_all() {
373        let f = load_it(&Dict::new());
374        let item = f.char_item(CharCode(65));
375        assert_eq!(item.gid, None);
376    }
377
378    #[test]
379    fn the_depth_cap_is_four() {
380        assert_eq!(MAX_TYPE3_DEPTH, 4);
381    }
382}