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