1use super::tables::{ALT_FONT_NAMES, BASE14_FONT_NAMES};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18#[repr(u8)]
19pub enum StandardFont {
20 Courier = 0,
22 CourierBold = 1,
24 CourierBoldOblique = 2,
26 CourierOblique = 3,
28 Helvetica = 4,
30 HelveticaBold = 5,
32 HelveticaBoldOblique = 6,
34 HelveticaOblique = 7,
36 Times = 8,
38 TimesBold = 9,
40 TimesBoldOblique = 10,
43 TimesOblique = 11,
45 Symbol = 12,
47 Dingbats = 13,
49}
50
51impl StandardFont {
52 #[must_use]
54 pub fn index(self) -> usize {
55 self as usize
56 }
57
58 #[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 #[must_use]
83 pub fn is_symbolic(self) -> bool {
84 matches!(self, Self::Symbol | Self::Dingbats)
85 }
86
87 #[must_use]
89 pub fn is_fixed(self) -> bool {
90 self.index() < 4
91 }
92
93 #[must_use]
96 pub fn is_stylable(self) -> bool {
97 matches!(self, Self::Courier | Self::Helvetica | Self::Times)
98 }
99}
100
101#[must_use]
103pub fn canonical_font_name(f: StandardFont) -> &'static str {
104 BASE14_FONT_NAMES.get(f.index()).copied().unwrap_or("")
105}
106
107#[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#[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#[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#[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 #![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 #[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 #[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 (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 #[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 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 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 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}