Skip to main content

pdf_base14_metrics/
lib.rs

1//! Pre-parsed Adobe Core 14 PDF font metrics.
2//!
3//! The 14 PostScript faces every PDF 1.7-conformant viewer ships
4//! built-in: Helvetica × 4, Times × 4, Courier × 4, Symbol,
5//! `ZapfDingbats`: exposed as `&'static FontMetrics<'static>` constants
6//! that cost nothing at runtime. The AFM files are vendored from
7//! [`tecnickcom/tc-font-core14-afms`] under `data/`, parsed by the
8//! sibling [`adobe-font-metrics`] crate at build time (see `build.rs`),
9//! and baked into Rust statics in `$OUT_DIR/baked.rs`.
10//!
11
12//! [`tecnickcom/tc-font-core14-afms`]: https://github.com/tecnickcom/tc-font-core14-afms
13//! [`adobe-font-metrics`]: https://crates.io/crates/adobe-font-metrics
14//!
15//! # Quick start
16//!
17//! ```
18//! use pdf_base14_metrics::Base14Font;
19//!
20//! // Look up a glyph width by PostScript name.
21//! assert_eq!(Base14Font::Helvetica.glyph_width("A"), Some(667.0));
22//!
23//! // Or via PDF `WinAnsiEncoding` byte (Latin faces only).
24//! assert_eq!(Base14Font::Helvetica.winansi_width(b'A'), Some(667.0));
25//!
26//! // Iterate every Core 14 face in stable order.
27//! for f in Base14Font::ALL {
28//!     let m = f.metrics();
29//!     assert!(!m.character_metrics.is_empty());
30//! }
31//! ```
32//!
33//! # Encoding caveat: Symbol and `ZapfDingbats`
34//!
35//! [`Base14Font::winansi_width`] returns `None` for [`Base14Font::Symbol`]
36//! and [`Base14Font::ZapfDingbats`]: those fonts use their own
37//! PostScript encodings (Greek/math operators and named dingbats
38//! respectively), not `WinAnsi`. Querying them through a Latin-1 byte
39//! would be a category error; the byte `0x41` is `"A"` in `WinAnsi`
40//! but `"Alpha"` in Symbol. Callers must reach for the per-glyph
41//! [`Base14Font::glyph_width`] API for those two fonts.
42//!
43//! # License
44//!
45//! The crate's Rust source is MIT. The 14 vendored AFM files in
46//! `data/afm/` ship under Adobe's permissive Core 14 AFM license
47//! (`APAFML`); see `LICENSE-APAFML` in the crate root. The combined
48//! SPDX expression is `MIT AND APAFML`.
49
50#![doc(
51    html_logo_url = "https://mosaiclang.dev/assets/A4.svg",
52    html_favicon_url = "https://mosaiclang.dev/assets/A4.svg"
53)]
54#![deny(missing_docs)]
55
56pub use adobe_font_metrics::{BBox, CharacterMetric, FontMetrics, KerningPair};
57
58use std::borrow::Cow;
59
60#[doc(hidden)]
61pub mod agl_subset;
62#[doc(hidden)]
63pub mod winansi_char_map;
64#[doc(hidden)]
65pub mod winansi_table {
66    include!("winansi_table.rs");
67
68    /// PDF `WinAnsi` byte-to-glyph-name table.
69    pub const TABLE: [Option<&str>; 256] = WINANSI_TABLE;
70}
71
72// The generated file references `BBox`, `CharacterMetric`,
73// `FontMetrics`, `KerningPair`, and `Cow` unqualified; all are in
74// scope via the `pub use` and `use` above.
75include!(concat!(env!("OUT_DIR"), "/baked.rs"));
76
77/// One of the 14 standard PDF fonts every conformant PDF reader
78/// ships built in (PDF 1.7 §9.6.2.2).
79///
80/// Variants are listed in the canonical PDF order: the four
81/// Helvetica weights, four Times weights, four Courier weights,
82/// then Symbol and `ZapfDingbats`. [`Self::ALL`] iterates them in
83/// this order.
84///
85/// # Examples
86///
87/// ```
88/// use pdf_base14_metrics::Base14Font;
89///
90/// assert_eq!(Base14Font::ALL.len(), 14);
91/// assert_eq!(Base14Font::Helvetica.pdf_base_name(), "Helvetica");
92/// ```
93#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
94pub enum Base14Font {
95    /// Helvetica (regular).
96    Helvetica,
97    /// Helvetica Bold.
98    HelveticaBold,
99    /// Helvetica Oblique (regular weight, slanted).
100    HelveticaOblique,
101    /// Helvetica Bold Oblique.
102    HelveticaBoldOblique,
103    /// Times Roman (regular).
104    TimesRoman,
105    /// Times Bold.
106    TimesBold,
107    /// Times Italic.
108    TimesItalic,
109    /// Times Bold Italic.
110    TimesBoldItalic,
111    /// Courier (regular, monospace).
112    Courier,
113    /// Courier Bold (monospace).
114    CourierBold,
115    /// Courier Oblique (monospace, slanted).
116    CourierOblique,
117    /// Courier Bold Oblique (monospace).
118    CourierBoldOblique,
119    /// Adobe Symbol (Greek letters, math operators).
120    Symbol,
121    /// ITC Zapf Dingbats (decorative glyphs).
122    ZapfDingbats,
123}
124
125impl Base14Font {
126    /// Every Core 14 face in stable PDF order.
127    pub const ALL: [Self; 14] = [
128        Self::Helvetica,
129        Self::HelveticaBold,
130        Self::HelveticaOblique,
131        Self::HelveticaBoldOblique,
132        Self::TimesRoman,
133        Self::TimesBold,
134        Self::TimesItalic,
135        Self::TimesBoldItalic,
136        Self::Courier,
137        Self::CourierBold,
138        Self::CourierOblique,
139        Self::CourierBoldOblique,
140        Self::Symbol,
141        Self::ZapfDingbats,
142    ];
143
144    /// Borrows the pre-parsed Adobe AFM metrics for this face.
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// use pdf_base14_metrics::Base14Font;
150    ///
151    /// let metrics = Base14Font::Helvetica.metrics();
152    ///
153    /// assert_eq!(metrics.font_name, "Helvetica");
154    /// ```
155    #[must_use]
156    pub fn metrics(self) -> &'static FontMetrics<'static> {
157        match self {
158            Self::Helvetica => &HELVETICA,
159            Self::HelveticaBold => &HELVETICA_BOLD,
160            Self::HelveticaOblique => &HELVETICA_OBLIQUE,
161            Self::HelveticaBoldOblique => &HELVETICA_BOLDOBLIQUE,
162            Self::TimesRoman => &TIMES_ROMAN,
163            Self::TimesBold => &TIMES_BOLD,
164            Self::TimesItalic => &TIMES_ITALIC,
165            Self::TimesBoldItalic => &TIMES_BOLDITALIC,
166            Self::Courier => &COURIER,
167            Self::CourierBold => &COURIER_BOLD,
168            Self::CourierOblique => &COURIER_OBLIQUE,
169            Self::CourierBoldOblique => &COURIER_BOLDOBLIQUE,
170            Self::Symbol => &SYMBOL,
171            Self::ZapfDingbats => &ZAPFDINGBATS,
172        }
173    }
174
175    /// PDF `/BaseFont` name per PDF 1.7 §9.6.2.2. These are the
176    /// exact bytes a conformant PDF writer puts after `/BaseFont`
177    /// in a font resource dictionary.
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// use pdf_base14_metrics::Base14Font;
183    ///
184    /// assert_eq!(Base14Font::TimesBoldItalic.pdf_base_name(), "Times-BoldItalic");
185    /// ```
186    #[must_use]
187    pub const fn pdf_base_name(self) -> &'static str {
188        match self {
189            Self::Helvetica => "Helvetica",
190            Self::HelveticaBold => "Helvetica-Bold",
191            Self::HelveticaOblique => "Helvetica-Oblique",
192            Self::HelveticaBoldOblique => "Helvetica-BoldOblique",
193            Self::TimesRoman => "Times-Roman",
194            Self::TimesBold => "Times-Bold",
195            Self::TimesItalic => "Times-Italic",
196            Self::TimesBoldItalic => "Times-BoldItalic",
197            Self::Courier => "Courier",
198            Self::CourierBold => "Courier-Bold",
199            Self::CourierOblique => "Courier-Oblique",
200            Self::CourierBoldOblique => "Courier-BoldOblique",
201            Self::Symbol => "Symbol",
202            Self::ZapfDingbats => "ZapfDingbats",
203        }
204    }
205
206    /// Width of the glyph with the given PostScript name, in 1/1000
207    /// em. Returns `None` if no such glyph exists in this font.
208    ///
209    /// This is an O(n) linear scan over the font's character metrics
210    /// (~315 entries for the Latin faces). Prefer
211    /// [`Self::winansi_width`] when querying by byte; that path
212    /// goes through a pre-baked O(1) table. For the Latin Core 12
213    /// faces, [`Self::glyph_width_by_name`] goes through a baked
214    /// sorted index instead and is O(log n).
215    ///
216    /// # Examples
217    ///
218    /// ```
219    /// use pdf_base14_metrics::Base14Font;
220    ///
221    /// assert_eq!(Base14Font::Helvetica.glyph_width("A"), Some(667.0));
222    /// ```
223    #[must_use]
224    pub fn glyph_width(self, name: &str) -> Option<f32> {
225        self.metrics()
226            .character_metrics
227            .iter()
228            .find(|c| c.name == name)
229            .map(|c| c.width_x)
230    }
231
232    /// Width of the glyph with the given PostScript name, looked up
233    /// through a baked sorted index. O(log n), allocation-free,
234    /// safe to call once per character per PDF page in tight loops.
235    ///
236    /// Returns `None` for [`Self::Symbol`] and [`Self::ZapfDingbats`]
237    ///: their AFMs are intentionally unindexed because those faces
238    /// don't participate in `/Differences`-style remapping. Callers
239    /// that need Symbol/Dingbat widths must use [`Self::glyph_width`].
240    ///
241    /// # Examples
242    ///
243    /// ```
244    /// use pdf_base14_metrics::Base14Font;
245    ///
246    /// assert_eq!(Base14Font::Helvetica.glyph_width_by_name("A"), Some(667.0));
247    /// assert_eq!(Base14Font::Symbol.glyph_width_by_name("Alpha"), None);
248    /// ```
249    #[must_use]
250    pub fn glyph_width_by_name(self, name: &str) -> Option<f32> {
251        let table = self.name_width_table()?;
252        table
253            .binary_search_by(|(n, _)| (*n).cmp(name))
254            .ok()
255            .map(|i| table[i].1)
256    }
257
258    /// Returns the baked `(name, width)` index for Latin Core 12
259    /// faces, or `None` for `Symbol`/`ZapfDingbats`.
260    const fn name_width_table(self) -> Option<&'static [(&'static str, f32)]> {
261        match self {
262            Self::Symbol | Self::ZapfDingbats => None,
263            Self::Helvetica => Some(HELVETICA_NAME_WIDTHS),
264            Self::HelveticaBold => Some(HELVETICA_BOLD_NAME_WIDTHS),
265            Self::HelveticaOblique => Some(HELVETICA_OBLIQUE_NAME_WIDTHS),
266            Self::HelveticaBoldOblique => Some(HELVETICA_BOLDOBLIQUE_NAME_WIDTHS),
267            Self::TimesRoman => Some(TIMES_ROMAN_NAME_WIDTHS),
268            Self::TimesBold => Some(TIMES_BOLD_NAME_WIDTHS),
269            Self::TimesItalic => Some(TIMES_ITALIC_NAME_WIDTHS),
270            Self::TimesBoldItalic => Some(TIMES_BOLDITALIC_NAME_WIDTHS),
271            Self::Courier => Some(COURIER_NAME_WIDTHS),
272            Self::CourierBold => Some(COURIER_BOLD_NAME_WIDTHS),
273            Self::CourierOblique => Some(COURIER_OBLIQUE_NAME_WIDTHS),
274            Self::CourierBoldOblique => Some(COURIER_BOLDOBLIQUE_NAME_WIDTHS),
275        }
276    }
277
278    /// Width of the glyph at PDF `WinAnsiEncoding` byte `code`, in
279    /// 1/1000 em. Returns `None` when:
280    ///
281    /// - `code` is unmapped by PDF `WinAnsi` (control characters
282    ///   `0x00..=0x1F`, the gaps `0x7F` / `0x81` / `0x8D` / `0x8F`
283    ///   / `0x90` / `0x9D`); or
284    /// - `self` is [`Self::Symbol`] or [`Self::ZapfDingbats`].
285    ///   those fonts do not use `WinAnsi` (see the crate-level docs).
286    ///
287    /// Implemented as a single `[Option<f32>; 256]` indexed load
288    /// per call: the table is baked at build time alongside the
289    /// font metrics. Hot enough for `mos-fonts::text_width` to
290    /// call once per character per typeset paragraph.
291    ///
292    /// # Examples
293    ///
294    /// ```
295    /// use pdf_base14_metrics::Base14Font;
296    ///
297    /// assert_eq!(Base14Font::Helvetica.winansi_width(b'A'), Some(667.0));
298    /// assert_eq!(Base14Font::Symbol.winansi_width(b'A'), None);
299    /// ```
300    #[must_use]
301    pub fn winansi_width(self, code: u8) -> Option<f32> {
302        self.winansi_table().and_then(|t| t[code as usize])
303    }
304
305    /// The pre-baked `WinAnsi` width table, or `None` for fonts whose
306    /// canonical encoding isn't `WinAnsi`.
307    const fn winansi_table(self) -> Option<&'static [Option<f32>; 256]> {
308        match self {
309            Self::Symbol | Self::ZapfDingbats => None,
310            Self::Helvetica => Some(&HELVETICA_WINANSI),
311            Self::HelveticaBold => Some(&HELVETICA_BOLD_WINANSI),
312            Self::HelveticaOblique => Some(&HELVETICA_OBLIQUE_WINANSI),
313            Self::HelveticaBoldOblique => Some(&HELVETICA_BOLDOBLIQUE_WINANSI),
314            Self::TimesRoman => Some(&TIMES_ROMAN_WINANSI),
315            Self::TimesBold => Some(&TIMES_BOLD_WINANSI),
316            Self::TimesItalic => Some(&TIMES_ITALIC_WINANSI),
317            Self::TimesBoldItalic => Some(&TIMES_BOLDITALIC_WINANSI),
318            Self::Courier => Some(&COURIER_WINANSI),
319            Self::CourierBold => Some(&COURIER_BOLD_WINANSI),
320            Self::CourierOblique => Some(&COURIER_OBLIQUE_WINANSI),
321            Self::CourierBoldOblique => Some(&COURIER_BOLDOBLIQUE_WINANSI),
322        }
323    }
324}
325
326/// Returns the PostScript glyph name assigned to PDF `WinAnsiEncoding`
327/// byte `code`, or `None` for unmapped codes.
328///
329/// PDF `WinAnsi` is **not** Microsoft CP1252; see PDF 1.7 Annex D.2
330/// for the canonical table. The two encodings differ at codes
331/// `0x7F`, `0x81`, `0x8D`, `0x8F`, `0x90`, and `0x9D` (gaps in PDF,
332/// assorted glyphs or DEL in CP1252).
333///
334/// This is exposed primarily so downstream crates (e.g.
335/// `mos-fonts`) can delegate to the canonical table rather than
336/// maintain their own copy.
337///
338/// # Examples
339///
340/// ```
341/// use pdf_base14_metrics::winansi_glyph_name;
342///
343/// assert_eq!(winansi_glyph_name(b'A'), Some("A"));
344/// assert_eq!(winansi_glyph_name(0x7F), None);
345/// ```
346#[must_use]
347pub const fn winansi_glyph_name(code: u8) -> Option<&'static str> {
348    winansi_table::TABLE[code as usize]
349}
350
351/// Returns the PDF `WinAnsiEncoding` byte that encodes `ch`, or
352/// `None` if `ch` has no slot in `WinAnsi`.
353///
354/// The inverse of the byte→char mapping transcribed from
355/// PDF 1.7 Annex D.2 Table D.2 into
356/// `winansi_char_map::WINANSI_CHAR_MAP`. Returns `None` for:
357///
358/// - Characters that have no glyph in `WinAnsi` (Cyrillic, CJK,
359///   most accented Vietnamese, etc.).
360/// - The six `WinAnsi` gap bytes (`0x7F`, `0x81`, `0x8D`, `0x8F`,
361///   `0x90`, `0x9D`).
362///
363/// O(n) scan over 256 slots: fine for callers that touch it once
364/// per text run, sensible to memoize for hotter paths.
365///
366/// # Examples
367///
368/// ```
369/// use pdf_base14_metrics::winansi_byte;
370///
371/// assert_eq!(winansi_byte('A'), Some(b'A'));
372/// assert_eq!(winansi_byte('Ж'), None);
373/// ```
374#[must_use]
375pub fn winansi_byte(ch: char) -> Option<u8> {
376    winansi_char_map::WINANSI_CHAR_MAP
377        .iter()
378        .position(|&c| c == Some(ch))
379        .and_then(|i| u8::try_from(i).ok())
380}
381
382// Test-only visibility shim for `tests/winansi_vendor.rs`. The const
383// is `#[doc(hidden)]` so it doesn't leak into the public API surface,
384// and lives here only so the integration test can re-derive the same
385// map from the Adobe Glyph List at test runtime and assert
386// byte-for-byte equality.
387#[doc(hidden)]
388pub const __WINANSI_CHAR_MAP: [Option<char>; 256] = winansi_char_map::WINANSI_CHAR_MAP;
389
390/// Returns the extended-tier PostScript glyph name for `ch`.
391///
392/// Extended-tier means a Core 14 AFM glyph that has no `WinAnsi` byte and
393/// therefore must be reached through a custom `/Encoding` `/Differences` slot.
394/// The extended tier covers:
395///
396/// - most of Latin Extended-A (`Ł`, `ł`, `Ě`, `ě`, `Ő`, `ő`, …,
397///   excluding those that already live in `WinAnsi` like
398///   `š`/`Š`/`ž`/`Ž`);
399/// - the Latin Extended-B comma-below set `Ș`/`ș`/`Ț`/`ț`;
400/// - the spacing diacritics `˘ˇ˙˝˛˚`;
401/// - the math operators `−≤≥≠√∂∑∆◊`;
402/// - the `fraction` slash `⁄` and the `fi`/`fl` ligatures.
403///
404/// Returns `None` for **two distinct cases that callers must
405/// distinguish**:
406///
407/// 1. **`WinAnsi` natives**: `š` (U+0161), `ž` (U+017E), `Š`, `Ž`,
408///    the accented Latin-1 alphabet, `€`, `“`, ... These *do* have
409///    PostScript glyph names in the AFM, but this function returns
410///    `None` for them because they're reachable through
411///    [`winansi_byte`] instead and don't need a `/Differences` slot.
412///    Callers querying "what's the AFM glyph name for `é`?" should
413///    use [`Base14Font::glyph_width_by_name`] on the result of
414///    <code>[winansi_glyph_name]([winansi_byte](ch)?)</code>, or just
415///    measure widths through [`Base14Font::winansi_width`].
416/// 2. **Unmappable codepoints** with no glyph in any Core 14 font
417///    (Cyrillic, CJK, emoji, most non-European scripts). The PDF
418///    backend silently substitutes these to `?` for Base14 runs;
419///    real coverage requires the bundled embedded family that
420///    `mos-fonts` provides.
421///
422/// The name `extended_glyph_name` is deliberately chosen over the
423/// shorter `glyph_name` to avoid surprising readers who reach for
424/// the function expecting "AFM name for any char." For *any-tier*
425/// AFM lookup the two-step (`winansi_glyph_name` ∘ `winansi_byte`)
426/// then-fallback-to-`extended_glyph_name` composition is the way.
427///
428/// Used by the PDF backend's `/Differences`-based encoding planner
429/// to allocate slots for the extended tier.
430///
431/// # Examples
432///
433/// ```
434/// use pdf_base14_metrics::extended_glyph_name;
435///
436/// assert_eq!(extended_glyph_name('Ł'), Some("Lslash"));
437/// assert_eq!(extended_glyph_name('A'), None);
438/// ```
439#[must_use]
440pub fn extended_glyph_name(ch: char) -> Option<&'static str> {
441    agl_subset::agl_glyph_name(ch)
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447
448    #[test]
449    fn glyph_width_by_name_matches_linear_scan_for_every_helvetica_glyph() {
450        let face = Base14Font::Helvetica;
451        for c in face.metrics().character_metrics.iter() {
452            let by_name = face.glyph_width_by_name(c.name.as_ref());
453            assert_eq!(
454                by_name,
455                Some(c.width_x),
456                "by-name mismatch for {:?}",
457                c.name
458            );
459        }
460    }
461
462    #[test]
463    fn glyph_width_by_name_resolves_non_winansi_glyphs() {
464        // Helvetica.adobe-font-metrics:  C -1 ; WX 222 ; N lslash ; ...  (well, lslash
465        // is actually encoded at C 248 in AdobeStandardEncoding, but
466        // either way the width is the same.) The PDF spec lets us
467        // address it through /Differences.
468        let face = Base14Font::Helvetica;
469        assert_eq!(face.glyph_width_by_name("lslash"), Some(222.0));
470        assert_eq!(face.glyph_width_by_name("Lslash"), Some(556.0));
471        assert_eq!(face.glyph_width_by_name("ecaron"), Some(556.0));
472        assert_eq!(face.glyph_width_by_name("rcaron"), Some(333.0));
473    }
474
475    #[test]
476    fn glyph_width_by_name_returns_none_for_unknown_glyph() {
477        assert_eq!(Base14Font::Helvetica.glyph_width_by_name(""), None);
478        assert_eq!(
479            Base14Font::Helvetica.glyph_width_by_name("notarealglyph"),
480            None
481        );
482    }
483
484    #[test]
485    fn glyph_width_by_name_returns_none_for_symbol_and_dingbats() {
486        // Documented contract: those faces don't participate in
487        // /Differences-based remapping.
488        assert_eq!(Base14Font::Symbol.glyph_width_by_name("A"), None);
489        assert_eq!(Base14Font::ZapfDingbats.glyph_width_by_name("A"), None);
490    }
491
492    #[test]
493    fn courier_carries_the_same_extended_glyph_set_as_helvetica() {
494        // The 12 Latin Core 14 faces share an identical 315-name glyph
495        // inventory (verified by `diff` on the AFM CharSets); the
496        // planner can rely on "if Helvetica has it, Courier does too"
497        // when deciding whether to remap a slot.
498        for name in &["lslash", "ecaron", "tcommaaccent", "ohungarumlaut"] {
499            assert!(
500                Base14Font::Courier.glyph_width_by_name(name).is_some(),
501                "Courier missing {name}"
502            );
503        }
504    }
505
506    #[test]
507    fn extended_glyph_name_resolves_polish_and_czech() {
508        assert_eq!(extended_glyph_name('ł'), Some("lslash"));
509        assert_eq!(extended_glyph_name('Ł'), Some("Lslash"));
510        assert_eq!(extended_glyph_name('ě'), Some("ecaron"));
511        // ž is a WinAnsi native, not in the extended tier: by
512        // contract `extended_glyph_name` returns `None` even though
513        // the AFM does carry a `zcaron` glyph (reachable through
514        // `winansi_byte` / `winansi_glyph_name` instead).
515        assert_eq!(extended_glyph_name('ž'), None);
516        // 'A' is also a WinAnsi native and returns None.
517        assert_eq!(extended_glyph_name('A'), None);
518    }
519}