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