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;