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}