use super::tables::{ALT_FONT_NAMES, BASE14_FONT_NAMES};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum StandardFont {
Courier = 0,
CourierBold = 1,
CourierBoldOblique = 2,
CourierOblique = 3,
Helvetica = 4,
HelveticaBold = 5,
HelveticaBoldOblique = 6,
HelveticaOblique = 7,
Times = 8,
TimesBold = 9,
TimesBoldOblique = 10,
TimesOblique = 11,
Symbol = 12,
Dingbats = 13,
}
impl StandardFont {
#[must_use]
pub fn index(self) -> usize {
self as usize
}
#[must_use]
pub fn from_index(i: usize) -> Option<Self> {
Some(match i {
0 => Self::Courier,
1 => Self::CourierBold,
2 => Self::CourierBoldOblique,
3 => Self::CourierOblique,
4 => Self::Helvetica,
5 => Self::HelveticaBold,
6 => Self::HelveticaBoldOblique,
7 => Self::HelveticaOblique,
8 => Self::Times,
9 => Self::TimesBold,
10 => Self::TimesBoldOblique,
11 => Self::TimesOblique,
12 => Self::Symbol,
13 => Self::Dingbats,
_ => return None,
})
}
#[must_use]
pub fn is_symbolic(self) -> bool {
matches!(self, Self::Symbol | Self::Dingbats)
}
#[must_use]
pub fn is_fixed(self) -> bool {
self.index() < 4
}
#[must_use]
pub fn is_stylable(self) -> bool {
matches!(self, Self::Courier | Self::Helvetica | Self::Times)
}
}
#[must_use]
pub fn canonical_font_name(f: StandardFont) -> &'static str {
BASE14_FONT_NAMES.get(f.index()).copied().unwrap_or("")
}
#[must_use]
pub fn standard_font_index(name: &[u8]) -> Option<StandardFont> {
let name = std::str::from_utf8(name).ok()?;
ALT_FONT_NAMES
.iter()
.find(|(alias, _)| alias.eq_ignore_ascii_case(name))
.map(|(_, f)| *f)
}
#[must_use]
#[cfg(test)]
pub fn is_standard_font_name(name: &[u8]) -> bool {
std::str::from_utf8(name).is_ok_and(|n| BASE14_FONT_NAMES.contains(&n))
}
#[must_use]
pub fn standard_font_data(f: StandardFont) -> &'static [u8] {
match f {
StandardFont::Courier => include_bytes!("../../fontdata/FoxitFixed.cff"),
StandardFont::CourierBold => include_bytes!("../../fontdata/FoxitFixedBold.cff"),
StandardFont::CourierBoldOblique => {
include_bytes!("../../fontdata/FoxitFixedBoldItalic.cff")
}
StandardFont::CourierOblique => include_bytes!("../../fontdata/FoxitFixedItalic.cff"),
StandardFont::Helvetica => include_bytes!("../../fontdata/FoxitSans.cff"),
StandardFont::HelveticaBold => include_bytes!("../../fontdata/FoxitSansBold.cff"),
StandardFont::HelveticaBoldOblique => {
include_bytes!("../../fontdata/FoxitSansBoldItalic.cff")
}
StandardFont::HelveticaOblique => include_bytes!("../../fontdata/FoxitSansItalic.cff"),
StandardFont::Times => include_bytes!("../../fontdata/FoxitSerif.cff"),
StandardFont::TimesBold => include_bytes!("../../fontdata/FoxitSerifBold.cff"),
StandardFont::TimesBoldOblique => include_bytes!("../../fontdata/FoxitSerifBoldItalic.cff"),
StandardFont::TimesOblique => include_bytes!("../../fontdata/FoxitSerifItalic.cff"),
StandardFont::Symbol => include_bytes!("../../fontdata/FoxitSymbol.cff"),
StandardFont::Dingbats => include_bytes!("../../fontdata/FoxitDingbats.cff"),
}
}
#[cfg(test)]
pub const ALL_STANDARD_FONTS: [StandardFont; 14] = [
StandardFont::Courier,
StandardFont::CourierBold,
StandardFont::CourierBoldOblique,
StandardFont::CourierOblique,
StandardFont::Helvetica,
StandardFont::HelveticaBold,
StandardFont::HelveticaBoldOblique,
StandardFont::HelveticaOblique,
StandardFont::Times,
StandardFont::TimesBold,
StandardFont::TimesBoldOblique,
StandardFont::TimesOblique,
StandardFont::Symbol,
StandardFont::Dingbats,
];
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
#[test]
fn canonical_names_are_the_postscript_spellings() {
assert_eq!(canonical_font_name(StandardFont::Times), "Times-Roman");
assert_eq!(
canonical_font_name(StandardFont::HelveticaOblique),
"Helvetica-Oblique"
);
}
#[test]
fn the_fourteen_canonical_names_are_standard_and_nothing_else_is() {
for f in ALL_STANDARD_FONTS {
assert!(is_standard_font_name(canonical_font_name(f).as_bytes()));
}
for name in [
&b"Arial"[..],
b"arial",
b"Times-roman",
b"",
b"Helvetica-Bold-Extra",
] {
assert!(
!is_standard_font_name(name),
"{:?} is not canonical",
std::str::from_utf8(name)
);
}
}
#[test]
fn aliases_resolve_and_misses_do_not() {
for (name, expected) in [
(&b"Courier"[..], Some(StandardFont::Courier)),
(b"Times-Roman", Some(StandardFont::Times)),
(b"ZapfDingbats", Some(StandardFont::Dingbats)),
(b"ArialMT", Some(StandardFont::Helvetica)),
(b"Arial-BoldMT", Some(StandardFont::HelveticaBold)),
(b"CourierNewPSMT", Some(StandardFont::Courier)),
(b"arial", Some(StandardFont::Helvetica)),
(b"ARIALMT", Some(StandardFont::Helvetica)),
(b"Nonesuch", None),
(b"", None),
] {
assert_eq!(
standard_font_index(name),
expected,
"{:?}",
std::str::from_utf8(name)
);
}
}
#[test]
fn symbolic_and_fixed_predicates_match_the_oracle() {
assert!(StandardFont::Symbol.is_symbolic());
assert!(StandardFont::Dingbats.is_symbolic());
for f in [
StandardFont::Courier,
StandardFont::Helvetica,
StandardFont::Times,
] {
assert!(!f.is_symbolic());
}
for f in [
StandardFont::Courier,
StandardFont::CourierBold,
StandardFont::CourierBoldOblique,
StandardFont::CourierOblique,
] {
assert!(f.is_fixed());
}
for f in [
StandardFont::Helvetica,
StandardFont::Times,
StandardFont::Symbol,
] {
assert!(!f.is_fixed());
}
}
#[test]
fn the_alias_table_has_exactly_eighty_nine_entries_and_all_resolve() {
assert_eq!(ALT_FONT_NAMES.len(), 89);
for (alias, expected) in ALT_FONT_NAMES {
assert_eq!(
standard_font_index(alias.as_bytes()),
Some(*expected),
"{alias}"
);
}
}
#[test]
fn every_canonical_name_round_trips_through_the_alias_table() {
for f in ALL_STANDARD_FONTS {
let name = canonical_font_name(f);
let back = standard_font_index(name.as_bytes())
.unwrap_or_else(|| panic!("{name} is its own alias"));
assert_eq!(canonical_font_name(back), name);
}
}
#[test]
fn indices_round_trip() {
for f in ALL_STANDARD_FONTS {
assert_eq!(StandardFont::from_index(f.index()), Some(f));
}
assert_eq!(StandardFont::from_index(14), None);
assert_eq!(StandardFont::from_index(usize::MAX), None);
}
#[test]
fn the_intra_family_order_is_regular_bold_bolditalic_italic() {
for family_head in [
StandardFont::Courier,
StandardFont::Helvetica,
StandardFont::Times,
] {
let base = family_head.index();
assert_eq!(base % 4, 0);
let names: Vec<&str> = (0..4)
.filter_map(|i| StandardFont::from_index(base + i))
.map(canonical_font_name)
.collect();
assert!(!names[0].contains("Bold") && !names[0].contains("Italic"));
assert!(names[1].contains("Bold") && !names[1].contains("Italic"));
assert!(names[2].contains("Bold"));
assert!(names[3].contains("Oblique") || names[3].contains("Italic"));
}
}
#[test]
fn only_the_three_family_heads_are_stylable() {
for f in ALL_STANDARD_FONTS {
assert_eq!(
f.is_stylable(),
matches!(
f,
StandardFont::Courier | StandardFont::Helvetica | StandardFont::Times
),
"{f:?}"
);
}
}
#[test]
fn every_foxit_blob_is_bare_cff_of_the_expected_size() {
for (f, expected) in [
(StandardFont::Courier, 17_597),
(StandardFont::CourierBold, 18_055),
(StandardFont::CourierBoldOblique, 19_151),
(StandardFont::CourierOblique, 18_746),
(StandardFont::Helvetica, 15_025),
(StandardFont::HelveticaBold, 16_344),
(StandardFont::HelveticaBoldOblique, 16_418),
(StandardFont::HelveticaOblique, 16_339),
(StandardFont::Times, 19_469),
(StandardFont::TimesBold, 19_395),
(StandardFont::TimesBoldOblique, 20_733),
(StandardFont::TimesOblique, 21_227),
(StandardFont::Symbol, 16_729),
(StandardFont::Dingbats, 29_513),
] {
let data = standard_font_data(f);
assert_eq!(data.len(), expected, "{f:?}");
assert_eq!(data.get(..4), Some(&[0x01, 0x00, 0x04, 0x02][..]), "{f:?}");
}
}
#[test]
fn every_foxit_blob_parses_into_a_usable_face() {
for f in ALL_STANDARD_FONTS {
let bytes: std::sync::Arc<[u8]> = std::sync::Arc::from(standard_font_data(f));
let face = crate::glyphs::Face::new(bytes, 0)
.unwrap_or_else(|| panic!("{f:?} must be readable"));
assert!(face.num_glyphs() > 1, "{f:?}");
assert_eq!(face.units_per_em(), 1000, "{f:?}");
assert!(!face.is_truetype(), "{f:?} is bare CFF, not TrueType");
}
}
}