Skip to main content

pdfrum_font/subst/
standard.rs

1//! The standard 14 fonts: their canonical names, the 89 aliases real files
2//! spell them with, and the Foxit programs that draw them.
3//!
4//! Every PDF reader is required to supply these fourteen faces. PDFium ships
5//! Foxit's bare-CFF clones of all fourteen plus two Multiple-Master faces for
6//! everything else, and they are what makes the substitution ladder always
7//! terminate in *something*.
8
9use super::tables::{ALT_FONT_NAMES, BASE14_FONT_NAMES};
10
11/// One of the fourteen standard fonts.
12///
13/// The discriminants are load-bearing, not cosmetic. Within a family the order
14/// is **Regular, Bold, BoldOblique, Oblique** — so `index % 4` decides the
15/// style and `index + 1` / `+2` / `+3` is how a style is *applied* to a family
16///. Changing the order silently changes substitution.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18#[repr(u8)]
19pub enum StandardFont {
20    /// `Courier`.
21    Courier = 0,
22    /// `Courier-Bold`.
23    CourierBold = 1,
24    /// `Courier-BoldOblique`.
25    CourierBoldOblique = 2,
26    /// `Courier-Oblique`.
27    CourierOblique = 3,
28    /// `Helvetica`.
29    Helvetica = 4,
30    /// `Helvetica-Bold`.
31    HelveticaBold = 5,
32    /// `Helvetica-BoldOblique`.
33    HelveticaBoldOblique = 6,
34    /// `Helvetica-Oblique`.
35    HelveticaOblique = 7,
36    /// `Times-Roman`.
37    Times = 8,
38    /// `Times-Bold`.
39    TimesBold = 9,
40    /// `Times-BoldItalic`. Note the family asymmetry: Courier and Helvetica
41    /// say `Oblique` where Times says `Italic`.
42    TimesBoldOblique = 10,
43    /// `Times-Italic`.
44    TimesOblique = 11,
45    /// `Symbol`.
46    Symbol = 12,
47    /// `ZapfDingbats`.
48    Dingbats = 13,
49}
50
51impl StandardFont {
52    /// The index into the base-14 tables.
53    #[must_use]
54    pub fn index(self) -> usize {
55        self as usize
56    }
57
58    /// Build from an index, for the arithmetic of the former working note step 10.
59    #[must_use]
60    pub fn from_index(i: usize) -> Option<Self> {
61        Some(match i {
62            0 => Self::Courier,
63            1 => Self::CourierBold,
64            2 => Self::CourierBoldOblique,
65            3 => Self::CourierOblique,
66            4 => Self::Helvetica,
67            5 => Self::HelveticaBold,
68            6 => Self::HelveticaBoldOblique,
69            7 => Self::HelveticaOblique,
70            8 => Self::Times,
71            9 => Self::TimesBold,
72            10 => Self::TimesBoldOblique,
73            11 => Self::TimesOblique,
74            12 => Self::Symbol,
75            13 => Self::Dingbats,
76            _ => return None,
77        })
78    }
79
80    /// Symbol and ZapfDingbats are the two whose glyphs are not Latin text,
81    /// which changes both their default flags and their default encoding.
82    #[must_use]
83    pub fn is_symbolic(self) -> bool {
84        matches!(self, Self::Symbol | Self::Dingbats)
85    }
86
87    /// The four Couriers, whose every glyph is 600 units wide.
88    #[must_use]
89    pub fn is_fixed(self) -> bool {
90        self.index() < 4
91    }
92
93    /// Can a style be applied to this index by arithmetic? Only the three
94    /// family *heads* can (`IsStylableBaseFont`).
95    #[must_use]
96    pub fn is_stylable(self) -> bool {
97        matches!(self, Self::Courier | Self::Helvetica | Self::Times)
98    }
99}
100
101/// The canonical PostScript name.
102#[must_use]
103pub fn canonical_font_name(f: StandardFont) -> &'static str {
104    BASE14_FONT_NAMES.get(f.index()).copied().unwrap_or("")
105}
106
107/// Resolve a `/BaseFont` name to a standard font, through the 89-entry alias
108/// table.
109///
110/// **Case-insensitive**, which is why `"arial"` resolves. PDFium binary-searches
111/// a table sorted by `FXSYS_stricmp`; a case-insensitive scan is behaviorally
112/// identical and is what we do, so the table's sortedness stops being a
113/// correctness requirement.
114#[must_use]
115pub fn standard_font_index(name: &[u8]) -> Option<StandardFont> {
116    let name = std::str::from_utf8(name).ok()?;
117    ALT_FONT_NAMES
118        .iter()
119        .find(|(alias, _)| alias.eq_ignore_ascii_case(name))
120        .map(|(_, f)| *f)
121}
122
123/// Is this **exactly** one of the fourteen canonical names?
124///
125/// Case-**sensitive**, and it does not consult the alias table — a different
126/// question from [`standard_font_index`], and PDFium asks both.
127#[must_use]
128#[cfg(test)]
129pub fn is_standard_font_name(name: &[u8]) -> bool {
130    std::str::from_utf8(name).is_ok_and(|n| BASE14_FONT_NAMES.contains(&n))
131}
132
133/// The Foxit font program for a standard font.
134///
135/// All fourteen are **bare CFF** (`01 00 04 02`), which `read-fonts` reads
136/// natively — no OpenType wrapper, no Type 1 container. Vendored from the
137/// oracle under its BSD licence; see `fontdata/PROVENANCE.md`.
138#[must_use]
139pub fn standard_font_data(f: StandardFont) -> &'static [u8] {
140    match f {
141        StandardFont::Courier => include_bytes!("../../fontdata/FoxitFixed.cff"),
142        StandardFont::CourierBold => include_bytes!("../../fontdata/FoxitFixedBold.cff"),
143        StandardFont::CourierBoldOblique => {
144            include_bytes!("../../fontdata/FoxitFixedBoldItalic.cff")
145        }
146        StandardFont::CourierOblique => include_bytes!("../../fontdata/FoxitFixedItalic.cff"),
147        StandardFont::Helvetica => include_bytes!("../../fontdata/FoxitSans.cff"),
148        StandardFont::HelveticaBold => include_bytes!("../../fontdata/FoxitSansBold.cff"),
149        StandardFont::HelveticaBoldOblique => {
150            include_bytes!("../../fontdata/FoxitSansBoldItalic.cff")
151        }
152        StandardFont::HelveticaOblique => include_bytes!("../../fontdata/FoxitSansItalic.cff"),
153        StandardFont::Times => include_bytes!("../../fontdata/FoxitSerif.cff"),
154        StandardFont::TimesBold => include_bytes!("../../fontdata/FoxitSerifBold.cff"),
155        StandardFont::TimesBoldOblique => include_bytes!("../../fontdata/FoxitSerifBoldItalic.cff"),
156        StandardFont::TimesOblique => include_bytes!("../../fontdata/FoxitSerifItalic.cff"),
157        StandardFont::Symbol => include_bytes!("../../fontdata/FoxitSymbol.cff"),
158        StandardFont::Dingbats => include_bytes!("../../fontdata/FoxitDingbats.cff"),
159    }
160}
161
162/// Every standard font, in index order.
163#[cfg(test)]
164pub const ALL_STANDARD_FONTS: [StandardFont; 14] = [
165    StandardFont::Courier,
166    StandardFont::CourierBold,
167    StandardFont::CourierBoldOblique,
168    StandardFont::CourierOblique,
169    StandardFont::Helvetica,
170    StandardFont::HelveticaBold,
171    StandardFont::HelveticaBoldOblique,
172    StandardFont::HelveticaOblique,
173    StandardFont::Times,
174    StandardFont::TimesBold,
175    StandardFont::TimesBoldOblique,
176    StandardFont::TimesOblique,
177    StandardFont::Symbol,
178    StandardFont::Dingbats,
179];
180
181#[cfg(test)]
182mod tests {
183    // Test fixtures are fixed-size arrays with known contents.
184    #![allow(clippy::indexing_slicing)]
185    use super::*;
186
187    #[test]
188    fn canonical_names_are_the_postscript_spellings() {
189        assert_eq!(canonical_font_name(StandardFont::Times), "Times-Roman");
190        assert_eq!(
191            canonical_font_name(StandardFont::HelveticaOblique),
192            "Helvetica-Oblique"
193        );
194    }
195
196    /// `cfx_standardfont_unittest.cpp`'s `IsStandardFontName`.
197    #[test]
198    fn the_fourteen_canonical_names_are_standard_and_nothing_else_is() {
199        for f in ALL_STANDARD_FONTS {
200            assert!(is_standard_font_name(canonical_font_name(f).as_bytes()));
201        }
202        for name in [
203            &b"Arial"[..],
204            b"arial",
205            b"Times-roman",
206            b"",
207            b"Helvetica-Bold-Extra",
208        ] {
209            assert!(
210                !is_standard_font_name(name),
211                "{:?} is not canonical",
212                std::str::from_utf8(name)
213            );
214        }
215    }
216
217    /// `cfx_standardfont_unittest.cpp`'s `GetStandardFontIndex`.
218    #[test]
219    fn aliases_resolve_and_misses_do_not() {
220        for (name, expected) in [
221            (&b"Courier"[..], Some(StandardFont::Courier)),
222            (b"Times-Roman", Some(StandardFont::Times)),
223            (b"ZapfDingbats", Some(StandardFont::Dingbats)),
224            (b"ArialMT", Some(StandardFont::Helvetica)),
225            (b"Arial-BoldMT", Some(StandardFont::HelveticaBold)),
226            (b"CourierNewPSMT", Some(StandardFont::Courier)),
227            // Case-insensitive, which is the assertion that pins the whole
228            // lookup being a `stricmp` compare rather than an ordered one.
229            (b"arial", Some(StandardFont::Helvetica)),
230            (b"ARIALMT", Some(StandardFont::Helvetica)),
231            (b"Nonesuch", None),
232            (b"", None),
233        ] {
234            assert_eq!(
235                standard_font_index(name),
236                expected,
237                "{:?}",
238                std::str::from_utf8(name)
239            );
240        }
241    }
242
243    /// `cfx_standardfont_unittest.cpp`'s `IsSymbolicFont` and `IsFixedFont`.
244    #[test]
245    fn symbolic_and_fixed_predicates_match_the_oracle() {
246        assert!(StandardFont::Symbol.is_symbolic());
247        assert!(StandardFont::Dingbats.is_symbolic());
248        for f in [
249            StandardFont::Courier,
250            StandardFont::Helvetica,
251            StandardFont::Times,
252        ] {
253            assert!(!f.is_symbolic());
254        }
255        for f in [
256            StandardFont::Courier,
257            StandardFont::CourierBold,
258            StandardFont::CourierBoldOblique,
259            StandardFont::CourierOblique,
260        ] {
261            assert!(f.is_fixed());
262        }
263        for f in [
264            StandardFont::Helvetica,
265            StandardFont::Times,
266            StandardFont::Symbol,
267        ] {
268            assert!(!f.is_fixed());
269        }
270    }
271
272    #[test]
273    fn the_alias_table_has_exactly_eighty_nine_entries_and_all_resolve() {
274        assert_eq!(ALT_FONT_NAMES.len(), 89);
275        for (alias, expected) in ALT_FONT_NAMES {
276            assert_eq!(
277                standard_font_index(alias.as_bytes()),
278                Some(*expected),
279                "{alias}"
280            );
281        }
282    }
283
284    #[test]
285    fn every_canonical_name_round_trips_through_the_alias_table() {
286        for f in ALL_STANDARD_FONTS {
287            let name = canonical_font_name(f);
288            let back = standard_font_index(name.as_bytes())
289                .unwrap_or_else(|| panic!("{name} is its own alias"));
290            assert_eq!(canonical_font_name(back), name);
291        }
292    }
293
294    #[test]
295    fn indices_round_trip() {
296        for f in ALL_STANDARD_FONTS {
297            assert_eq!(StandardFont::from_index(f.index()), Some(f));
298        }
299        assert_eq!(StandardFont::from_index(14), None);
300        assert_eq!(StandardFont::from_index(usize::MAX), None);
301    }
302
303    #[test]
304    fn the_intra_family_order_is_regular_bold_bolditalic_italic() {
305        // `GetStyleFromBaseFont` reads `index % 4`, and `AdjustBaseFontForStyle`
306        // adds 1, 2 or 3 — both are wrong if this order ever changes.
307        for family_head in [
308            StandardFont::Courier,
309            StandardFont::Helvetica,
310            StandardFont::Times,
311        ] {
312            let base = family_head.index();
313            assert_eq!(base % 4, 0);
314            let names: Vec<&str> = (0..4)
315                .filter_map(|i| StandardFont::from_index(base + i))
316                .map(canonical_font_name)
317                .collect();
318            assert!(!names[0].contains("Bold") && !names[0].contains("Italic"));
319            assert!(names[1].contains("Bold") && !names[1].contains("Italic"));
320            assert!(names[2].contains("Bold"));
321            assert!(names[3].contains("Oblique") || names[3].contains("Italic"));
322        }
323    }
324
325    #[test]
326    fn only_the_three_family_heads_are_stylable() {
327        for f in ALL_STANDARD_FONTS {
328            assert_eq!(
329                f.is_stylable(),
330                matches!(
331                    f,
332                    StandardFont::Courier | StandardFont::Helvetica | StandardFont::Times
333                ),
334                "{f:?}"
335            );
336        }
337    }
338
339    #[test]
340    fn every_foxit_blob_is_bare_cff_of_the_expected_size() {
341        // The sizes are the oracle's `std::array` bounds; a mis-extraction
342        // would show here rather than as a mysteriously blank glyph.
343        for (f, expected) in [
344            (StandardFont::Courier, 17_597),
345            (StandardFont::CourierBold, 18_055),
346            (StandardFont::CourierBoldOblique, 19_151),
347            (StandardFont::CourierOblique, 18_746),
348            (StandardFont::Helvetica, 15_025),
349            (StandardFont::HelveticaBold, 16_344),
350            (StandardFont::HelveticaBoldOblique, 16_418),
351            (StandardFont::HelveticaOblique, 16_339),
352            (StandardFont::Times, 19_469),
353            (StandardFont::TimesBold, 19_395),
354            (StandardFont::TimesBoldOblique, 20_733),
355            (StandardFont::TimesOblique, 21_227),
356            (StandardFont::Symbol, 16_729),
357            (StandardFont::Dingbats, 29_513),
358        ] {
359            let data = standard_font_data(f);
360            assert_eq!(data.len(), expected, "{f:?}");
361            // CFF major 1, minor 0, header size 4, offset size 2.
362            assert_eq!(data.get(..4), Some(&[0x01, 0x00, 0x04, 0x02][..]), "{f:?}");
363        }
364    }
365
366    #[test]
367    fn every_foxit_blob_parses_into_a_usable_face() {
368        for f in ALL_STANDARD_FONTS {
369            let bytes: std::sync::Arc<[u8]> = std::sync::Arc::from(standard_font_data(f));
370            let face = crate::glyphs::Face::new(bytes, 0)
371                .unwrap_or_else(|| panic!("{f:?} must be readable"));
372            assert!(face.num_glyphs() > 1, "{f:?}");
373            assert_eq!(face.units_per_em(), 1000, "{f:?}");
374            assert!(!face.is_truetype(), "{f:?} is bare CFF, not TrueType");
375        }
376    }
377}