Skip to main content

pdfrum_type1/
lib.rs

1#![doc = include_str!("../README.md")]
2// Two things need this crate, and nothing else does:
3//
4// 1. Embedded Type 1 programs — a `/FontFile` stream, or a `/FontFile3` whose
5//    payload turns out to be Type 1 rather than the usual bare CFF.
6// 2. The two generic fallback faces, which are PFB Multiple-Master Type 1 and
7//    are the terminal rung of the substitution ladder: when a document names a
8//    font nothing on the system resembles, they are what draws it. A renderer
9//    that cannot instantiate them at an arbitrary weight draws nothing at all
10//    for such a font.
11//
12// The design-coordinate interface is what the renderer needs: PDFium picks a
13// width axis coordinate by bisecting on the advance widths two probe instances
14// report, and picks the weight axis straight from the substitution font's
15// weight.
16//
17// Damage tolerated on the way in: a truncated PFB segment, hex that stops
18// mid-byte, a charstring that runs off its end, a Multiple-Master declaration
19// whose parts disagree.
20#![forbid(unsafe_code)]
21#![cfg_attr(docsrs, feature(doc_cfg))]
22// Every byte here came from an untrusted `/FontFile` stream: index with
23// `get()`, never with `[]`.
24#![warn(clippy::indexing_slicing)]
25// Font units are integers stored as floats and character codes are `u8`s cut
26// out of wider values; the conversions below are the format's own arithmetic,
27// each one pinned by a test.
28#![allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
29
30mod blend;
31mod charstring;
32mod container;
33mod eexec;
34mod encoding;
35mod error;
36mod postscript;
37mod program;
38
39pub use blend::{AxisKind, MmAxis};
40pub use charstring::Glyph;
41pub use container::{Container, FontFile, font_file};
42pub use eexec::{CHARSTRING_SEED, DEFAULT_LEN_IV, EEXEC_SEED, EEXEC_SKIP, decrypt, encrypt};
43pub use encoding::{Encoding, standard_encoding_name, unicode_from_glyph_name};
44pub use error::Error;
45
46use blend::Blend;
47use pdfrum_common::kurbo::{Affine, BezPath, Rect, Shape};
48use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
49use std::collections::HashMap;
50
51/// A glyph index into a [`Type1Font`]'s `/CharStrings`, in declaration order.
52///
53/// Type 1 has no glyph-index concept of its own — glyphs are named — so this
54/// is our numbering, fixed by the order the dictionary declared them, which is
55/// the same convention FreeType and `read-fonts` use.
56///
57/// Not interchangeable with `pdfrum_font::Gid`, which indexes whatever program
58/// a face was loaded from. The two are separate index spaces that coincide
59/// numerically only for a face that *is* a Type 1 program; `pdfrum-font` owns
60/// the conversion between them.
61// A shared identifier in `pdfrum-common` would be wrong: merging them would
62// make an sfnt glyph index — `skrifa`'s `GlyphId`, a numbering this crate never
63// produces and never sees — assignable to a `/CharStrings` slot with no
64// conversion. The `From` impls live at the one boundary that owns both spaces.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
66pub struct Gid(pub u16);
67
68/// A parsed Type 1 font program.
69///
70/// A record, not an engine: the fields below are everything the program said,
71/// and every method is a pure function of them. Outlines are computed on
72/// demand and not cached here — the caller's glyph cache owns that, keyed by
73/// the instantiation as well as the glyph.
74#[derive(Debug, Clone)]
75pub struct Type1Font {
76    container: Container,
77    font_name: Option<Box<str>>,
78    full_name: Option<Box<str>>,
79    family_name: Option<Box<str>>,
80    italic_angle: f32,
81    is_fixed_pitch: bool,
82    font_matrix: Affine,
83    font_bbox: Rect,
84    encoding: Encoding,
85    subrs: Vec<Vec<u8>>,
86    charstrings: Vec<Vec<u8>>,
87    glyph_names: Vec<Box<str>>,
88    by_name: HashMap<Box<str>, u16>,
89    unicode_map: HashMap<char, u16>,
90    blend: Option<Blend>,
91}
92
93/// The default `/FontMatrix` when a program does not declare one: 1000 units
94/// per em, which is what every Type 1 font in practice uses.
95const DEFAULT_MATRIX: Affine = Affine::new([0.001, 0.0, 0.0, 0.001, 0.0, 0.0]);
96
97impl Type1Font {
98    /// Parse a font program: sniff PFB/PFA/bare, decrypt `eexec`, read the
99    /// cleartext and private dictionaries.
100    ///
101    /// `/Length1`, `/Length2` and `/Length3` from the font descriptor are
102    /// deliberately *not* a parameter. PDFium ignores them, because they are
103    /// wrong often enough that trusting them loses more fonts than it saves,
104    /// and the container is self-describing anyway.
105    ///
106    /// # Errors
107    ///
108    /// [`Error::Empty`] for no bytes, [`Error::PfbSegment`] for a PFB whose
109    /// first segment header is unusable, [`Error::NoEexec`] when no private
110    /// section can be found, and [`Error::NoCharStrings`] when the program
111    /// declares no glyphs.
112    pub fn parse(bytes: &[u8], limits: &Limits, diags: &mut Diagnostics) -> Result<Self, Error> {
113        let split = container::split(bytes, diags)?;
114        let plain = eexec::decrypt(&split.cipher, eexec::EEXEC_SEED, eexec::EEXEC_SKIP);
115        // A correctly-keyed decryption always yields printable PostScript in
116        // its first line; random bytes almost never do. Checking is what turns
117        // "fed the wrong offset" into a clear error instead of an empty font.
118        if !looks_like_postscript(&plain) {
119            return Err(Error::EexecGarbage);
120        }
121
122        let header = program::read_header(&split.clear);
123        let private = program::read_private(&plain);
124        if private.charstrings.is_empty() {
125            return Err(Error::NoCharStrings);
126        }
127
128        let cap = limits.max_array_len.min(u16::MAX as usize);
129        let (glyph_names, charstrings): (Vec<_>, Vec<_>) =
130            private.charstrings.into_iter().take(cap).unzip();
131
132        // Later declarations of a name win, matching PostScript's `put`.
133        let by_name: HashMap<Box<str>, u16> = glyph_names
134            .iter()
135            .enumerate()
136            .filter_map(|(i, n)| Some((n.clone(), u16::try_from(i).ok()?)))
137            .collect();
138        // The synthesized Unicode charmap. First name wins, so a font
139        // declaring both `A` and `uni0041` maps U+0041 to the earlier one —
140        // the same tie-break FreeType applies.
141        let mut unicode_map: HashMap<char, u16> = HashMap::new();
142        for (i, name) in glyph_names.iter().enumerate() {
143            if let (Some(ch), Ok(gid)) = (encoding::unicode_from_glyph_name(name), u16::try_from(i))
144            {
145                unicode_map.entry(ch).or_insert(gid);
146            }
147        }
148
149        let blend = program::build_blend(&header, diags);
150        let encoding = header.encoding.unwrap_or(Encoding::Standard);
151        note_missing_encoding_glyphs(&encoding, &by_name, diags);
152
153        let font_matrix = header.font_matrix.unwrap_or(DEFAULT_MATRIX);
154        Ok(Self {
155            container: split.container,
156            font_name: header.font_name,
157            full_name: header.full_name,
158            family_name: header.family_name,
159            italic_angle: header.italic_angle,
160            is_fixed_pitch: header.is_fixed_pitch,
161            font_matrix,
162            font_bbox: header.font_bbox.unwrap_or(Rect::ZERO),
163            encoding,
164            subrs: private.subrs,
165            charstrings,
166            glyph_names,
167            by_name,
168            unicode_map,
169            blend,
170        })
171    }
172
173    /// Which wrapper the program arrived in.
174    #[must_use]
175    pub fn container(&self) -> Container {
176        self.container
177    }
178
179    /// Design units per em, derived from the `/FontMatrix` — a matrix of
180    /// `0.001` means 1000 units per em.
181    ///
182    /// Rounded to the nearest integer and floored at 1, because a rasterizer
183    /// dividing by this must never divide by zero and a font declaring a
184    /// degenerate matrix is not worth refusing over.
185    #[must_use]
186    pub fn units_per_em(&self) -> u16 {
187        let sx = self.font_matrix.as_coeffs().first().copied().unwrap_or(0.0);
188        if sx.abs() < 1e-12 {
189            return 1000;
190        }
191        let upem = (1.0 / sx).abs().round();
192        if upem.is_finite() && (1.0..=f64::from(u16::MAX)).contains(&upem) {
193            // Range-checked immediately above, so the cast is exact.
194            #[allow(clippy::cast_sign_loss)]
195            {
196                upem as u16
197            }
198        } else {
199            1000
200        }
201    }
202
203    /// The `/FontMatrix`: font units to text space.
204    #[must_use]
205    pub fn font_matrix(&self) -> Affine {
206        self.font_matrix
207    }
208
209    /// The declared `/FontBBox`, in font units. `Rect::ZERO` when the program
210    /// declared none — callers derive one from the glyphs in that case.
211    #[must_use]
212    pub fn bbox(&self) -> Rect {
213        self.font_bbox
214    }
215
216    /// Number of glyphs in `/CharStrings`.
217    #[must_use]
218    pub fn num_glyphs(&self) -> u32 {
219        self.charstrings.len() as u32
220    }
221
222    /// Whether the program declared `/isFixedPitch true`.
223    #[must_use]
224    pub fn is_fixed_pitch(&self) -> bool {
225        self.is_fixed_pitch
226    }
227
228    /// `/ItalicAngle`, in degrees counter-clockwise from vertical (so an
229    /// oblique face reports a negative value).
230    #[must_use]
231    pub fn italic_angle(&self) -> f32 {
232        self.italic_angle
233    }
234
235    /// `/FontName` — the PostScript name.
236    #[must_use]
237    pub fn postscript_name(&self) -> Option<&str> {
238        self.font_name.as_deref()
239    }
240
241    /// `/FullName` from `/FontInfo`.
242    #[must_use]
243    pub fn full_name(&self) -> Option<&str> {
244        self.full_name.as_deref()
245    }
246
247    /// `/FamilyName` from `/FontInfo`.
248    #[must_use]
249    pub fn family_name(&self) -> Option<&str> {
250        self.family_name.as_deref()
251    }
252
253    /// The font's built-in `/Encoding` vector.
254    #[must_use]
255    pub fn encoding(&self) -> &Encoding {
256        &self.encoding
257    }
258
259    /// Character code to glyph, through the built-in encoding.
260    #[must_use]
261    pub fn code_to_gid(&self, code: u8) -> Option<Gid> {
262        self.name_to_gid(self.encoding.glyph_name(code)?)
263    }
264
265    /// Unicode scalar to glyph, through the Adobe-Glyph-List charmap
266    /// synthesized from the glyph names.
267    #[must_use]
268    pub fn unicode_to_gid(&self, ch: char) -> Option<Gid> {
269        self.unicode_map.get(&ch).copied().map(Gid)
270    }
271
272    /// Every Unicode scalar the synthesized charmap maps, in arbitrary order.
273    pub fn unicode_pairs(&self) -> impl Iterator<Item = (char, Gid)> + '_ {
274        self.unicode_map.iter().map(|(&ch, &gid)| (ch, Gid(gid)))
275    }
276
277    /// Glyph name to glyph.
278    #[must_use]
279    pub fn name_to_gid(&self, name: &str) -> Option<Gid> {
280        self.by_name.get(name).copied().map(Gid)
281    }
282
283    /// The name a glyph was declared under.
284    #[must_use]
285    pub fn glyph_name(&self, gid: Gid) -> Option<&str> {
286        self.glyph_names.get(gid.0 as usize).map(AsRef::as_ref)
287    }
288
289    /// Always true: Type 1 identifies glyphs by name, so every glyph has one.
290    #[must_use]
291    pub fn has_glyph_names(&self) -> bool {
292        true
293    }
294
295    /// Every `(glyph name, glyph)` pair, in glyph order.
296    pub fn glyph_names(&self) -> impl Iterator<Item = (Gid, &str)> {
297        self.glyph_names
298            .iter()
299            .enumerate()
300            .filter_map(|(i, n)| Some((Gid(u16::try_from(i).ok()?), n.as_ref())))
301    }
302
303    /// The unscaled outline in font units, and the advance width `hsbw`/`sbw`
304    /// declared.
305    ///
306    /// The outline is *not* transformed by the `/FontMatrix`; a caller that
307    /// wants text-space coordinates applies [`font_matrix`](Self::font_matrix)
308    /// itself, which is what `pdfrum-font` does so it can compose the matrix
309    /// with the text matrix in one step.
310    ///
311    /// For a Multiple-Master font this uses the weight vector the program
312    /// shipped with — [`instantiate`](Self::instantiate) is how a caller asks
313    /// for a different one.
314    #[must_use]
315    pub fn outline(&self, gid: Gid) -> Option<(BezPath, f32)> {
316        let weights = self.default_weights();
317        let g = self.interpret(gid, &weights, &mut Diagnostics::with_limit(0))?;
318        Some((g.path, g.advance))
319    }
320
321    /// [`outline`](Self::outline), recording an interpretation failure.
322    ///
323    /// The outline comes back either way; the diagnostic says whether it is
324    /// the whole glyph or as much of it as the charstring allowed.
325    #[must_use]
326    pub fn outline_with_diagnostics(
327        &self,
328        gid: Gid,
329        diags: &mut Diagnostics,
330    ) -> Option<(BezPath, f32)> {
331        let weights = self.default_weights();
332        let g = self.interpret(gid, &weights, diags)?;
333        Some((g.path, g.advance))
334    }
335
336    /// The glyph's bounding box in font units, or `None` for an empty glyph
337    /// (a space) as well as for a glyph that does not exist.
338    #[must_use]
339    pub fn glyph_bounds(&self, gid: Gid) -> Option<Rect> {
340        let (path, _) = self.outline(gid)?;
341        (!path.is_empty()).then(|| path.bounding_box())
342    }
343
344    /// The Multiple-Master design axes, or `None` for an ordinary font.
345    ///
346    /// ```
347    /// # use pdfrum_common::Diagnostics;
348    /// # use pdfrum_type1::Type1Font;
349    /// # fn demo(pfb: &[u8]) -> Option<()> {
350    /// let font = Type1Font::parse(pfb, &Default::default(), &mut Diagnostics::default()).ok()?;
351    /// for axis in font.mm_axes()? {
352    ///     assert!(axis.min <= axis.default && axis.default <= axis.max);
353    /// }
354    /// # Some(())
355    /// # }
356    /// ```
357    #[must_use]
358    pub fn mm_axes(&self) -> Option<&[MmAxis]> {
359        self.blend.as_ref().map(|b| b.axes.as_slice())
360    }
361
362    /// Instantiate at design coordinates — one per axis, in
363    /// [`mm_axes`](Self::mm_axes) order.
364    ///
365    /// Coordinates outside an axis's range clamp to it, and a short list
366    /// leaves the remaining axes at their defaults. Returns `None` for a font
367    /// with no Multiple-Master declaration, which is how a caller tells "this
368    /// face cannot vary" from "it varied to the value you asked for".
369    #[must_use]
370    pub fn instantiate(&self, coords: &[f32]) -> Option<Type1Instance<'_>> {
371        let blend = self.blend.as_ref()?;
372        Some(Type1Instance {
373            font: self,
374            weights: blend.weights_for(coords),
375        })
376    }
377
378    /// The weight vector the program shipped with, or an empty slice for a
379    /// non-Multiple-Master font.
380    #[must_use]
381    pub fn default_weight_vector(&self) -> &[f32] {
382        self.blend.as_ref().map_or(&[], |b| &b.default_weights)
383    }
384
385    fn default_weights(&self) -> Vec<f32> {
386        self.blend
387            .as_ref()
388            .map(|b| b.default_weights.clone())
389            .unwrap_or_default()
390    }
391
392    fn interpret(&self, gid: Gid, weights: &[f32], diags: &mut Diagnostics) -> Option<Glyph> {
393        let code = self.charstrings.get(gid.0 as usize)?;
394        let lookup = |name: &str| self.by_name.get(name).map(|g| *g as usize);
395        let (glyph, abort) = charstring::interpret(
396            code,
397            charstring::Env {
398                subrs: &self.subrs,
399                charstrings: &self.charstrings,
400                name_lookup: &lookup,
401                weights,
402                blend: self.blend.as_ref(),
403            },
404        );
405        if abort.is_some() {
406            diags.record(
407                Severity::Suspicious,
408                DiagKind::Type1CharstringAborted,
409                Some(u64::from(gid.0)),
410            );
411        }
412        Some(glyph)
413    }
414}
415
416/// A Multiple-Master font blended at one point in design space.
417///
418/// Borrows its font, so it is free to create — the interpolation happens per
419/// glyph, at [`outline`](Self::outline) time, exactly as it does in the
420/// charstring machine.
421#[derive(Debug, Clone)]
422pub struct Type1Instance<'a> {
423    font: &'a Type1Font,
424    weights: Vec<f32>,
425}
426
427impl Type1Instance<'_> {
428    /// The blended outline in font units, and its advance width.
429    ///
430    /// The advance is what makes this the interface PDFium's
431    /// `AdjustVariationParams` needs: it probes the width axis at both ends,
432    /// reads the advance each returns, and interpolates to hit a target width.
433    #[must_use]
434    pub fn outline(&self, gid: Gid) -> Option<(BezPath, f32)> {
435        let g = self
436            .font
437            .interpret(gid, &self.weights, &mut Diagnostics::with_limit(0))?;
438        Some((g.path, g.advance))
439    }
440
441    /// The advance width alone, without keeping the outline.
442    #[must_use]
443    pub fn advance(&self, gid: Gid) -> Option<f32> {
444        self.outline(gid).map(|(_, a)| a)
445    }
446
447    /// The weight vector this instance blends with — the per-master
448    /// coefficients the charstring machine applies.
449    #[must_use]
450    pub fn weight_vector(&self) -> &[f32] {
451        &self.weights
452    }
453
454    /// The font this instance came from.
455    #[must_use]
456    pub fn font(&self) -> &Type1Font {
457        self.font
458    }
459}
460
461/// A decrypted private dictionary starts with PostScript, and a wrongly-keyed
462/// one starts with noise. Testing the first non-blank run for printability
463/// separates them reliably without demanding an exact prefix, which real fonts
464/// vary (`dup /Private`, `/Private`, `2 index /Private`).
465fn looks_like_postscript(plain: &[u8]) -> bool {
466    let head = plain.get(..64).unwrap_or(plain);
467    if head.is_empty() {
468        return false;
469    }
470    let printable = head
471        .iter()
472        .filter(|b| b.is_ascii_graphic() || b.is_ascii_whitespace())
473        .count();
474    printable * 4 >= head.len() * 3
475}
476
477/// Record encoding entries naming glyphs the font does not define — a
478/// subsetted font's calling card, and something a caller may want to know
479/// before it decides the font is unusable.
480fn note_missing_encoding_glyphs(
481    enc: &Encoding,
482    by_name: &HashMap<Box<str>, u16>,
483    diags: &mut Diagnostics,
484) {
485    if let Encoding::Custom(table) = enc {
486        for (code, slot) in table.iter().enumerate() {
487            if let Some(name) = slot
488                && !by_name.contains_key(name.as_ref())
489            {
490                diags.record(
491                    Severity::Suspicious,
492                    DiagKind::Type1EncodingGlyphMissing,
493                    Some(code as u64),
494                );
495            }
496        }
497    }
498}
499
500#[cfg(test)]
501mod tests;