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;