mod agl;
mod differences;
mod tables;
pub use agl::{adobe_name_from_unicode, unicode_from_adobe_name};
pub use differences::load_differences;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FontEncoding {
#[default]
Builtin,
WinAnsi,
MacRoman,
MacExpert,
Standard,
AdobeSymbol,
ZapfDingbats,
PdfDoc,
MsSymbol,
}
impl FontEncoding {
#[must_use]
pub fn unicodes(self) -> Option<&'static [u16; 256]> {
Some(match self {
Self::Builtin => return None,
Self::WinAnsi => &tables::ADOBE_WIN_ANSI_ENCODING,
Self::MacRoman => &tables::MAC_ROMAN_ENCODING,
Self::MacExpert => &tables::MAC_EXPERT_ENCODING,
Self::Standard => &tables::STANDARD_ENCODING,
Self::AdobeSymbol => &tables::ADOBE_SYMBOL_ENCODING,
Self::ZapfDingbats => &tables::ZAPF_ENCODING,
Self::PdfDoc => &tables::PDF_DOC_ENCODING,
Self::MsSymbol => &tables::MS_SYMBOL_ENCODING,
})
}
#[must_use]
pub fn char_name(self, code: u8) -> Option<&'static str> {
let (table, first): (&[Option<&'static str>], u8) = match self {
Self::Standard => (&tables::STANDARD_ENCODING_NAMES, 32),
Self::WinAnsi => (&tables::ADOBE_WIN_ANSI_ENCODING_NAMES, 32),
Self::MacRoman => (&tables::MAC_ROMAN_ENCODING_NAMES, 32),
Self::MacExpert => (&tables::MAC_EXPERT_ENCODING_NAMES, 32),
Self::PdfDoc => (&tables::PDF_DOC_ENCODING_NAMES, 24),
Self::AdobeSymbol => (&tables::ADOBE_SYMBOL_ENCODING_NAMES, 32),
Self::ZapfDingbats => (&tables::ZAPF_ENCODING_NAMES, 32),
Self::MsSymbol | Self::Builtin => return None,
};
let index = usize::from(code.checked_sub(first)?);
table.get(index).copied().flatten()
}
#[must_use]
pub fn from_pdf_name(name: &[u8]) -> Option<Self> {
Some(match name {
b"WinAnsiEncoding" => Self::WinAnsi,
b"MacRomanEncoding" => Self::MacRoman,
b"MacExpertEncoding" => Self::MacExpert,
b"PDFDocEncoding" => Self::PdfDoc,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FaceEncoding {
Unicode,
Latin1,
AppleRoman,
AdobeCustom,
Symbol,
Other,
}
impl FaceEncoding {
#[must_use]
pub fn charcode_from_unicode(self, unicode: u16) -> u32 {
let table: &[u16; 256] = match self {
Self::Unicode => return u32::from(unicode),
Self::Latin1 => &tables::ADOBE_WIN_ANSI_ENCODING,
Self::AppleRoman => &tables::MAC_ROMAN_ENCODING,
Self::AdobeCustom => &tables::PDF_DOC_ENCODING,
Self::Symbol => &tables::MS_SYMBOL_ENCODING,
Self::Other => return 0,
};
table
.iter()
.position(|&u| u == unicode)
.and_then(|i| u32::try_from(i).ok())
.unwrap_or(0)
}
}
#[must_use]
pub fn unicode_from_apple_roman(code: u8) -> u16 {
tables::MAC_ROMAN_ENCODING
.get(usize::from(code))
.copied()
.unwrap_or(0)
}
#[must_use]
pub fn adobe_char_name(
base: FontEncoding,
differences: &[Option<crate::ids::GlyphName>; 256],
charcode: u32,
) -> Option<&[u8]> {
let code = u8::try_from(charcode).ok()?;
if let Some(name) = differences.get(usize::from(code)).and_then(Option::as_ref) {
if !name.as_bytes().is_empty() {
return Some(name.as_bytes());
}
}
base.char_name(code).map(str::as_bytes)
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::ids::GlyphName;
const NO_DIFFS: [Option<GlyphName>; 256] = [const { None }; 256];
#[test]
fn builtin_is_the_only_encoding_without_a_unicode_table() {
assert!(FontEncoding::Builtin.unicodes().is_none());
for e in [
FontEncoding::WinAnsi,
FontEncoding::MacRoman,
FontEncoding::MacExpert,
FontEncoding::Standard,
FontEncoding::AdobeSymbol,
FontEncoding::ZapfDingbats,
FontEncoding::PdfDoc,
FontEncoding::MsSymbol,
] {
assert!(e.unicodes().is_some(), "{e:?}");
}
}
#[test]
fn name_tables_start_at_32_except_pdfdoc_which_starts_at_24() {
assert_eq!(FontEncoding::Standard.char_name(31), None);
assert_eq!(FontEncoding::Standard.char_name(32), Some("space"));
assert_eq!(FontEncoding::PdfDoc.char_name(23), None);
assert!(FontEncoding::PdfDoc.char_name(24).is_some());
}
#[test]
fn ms_symbol_and_builtin_have_no_glyph_names() {
for c in [0u8, 32, 65, 255] {
assert_eq!(FontEncoding::MsSymbol.char_name(c), None);
assert_eq!(FontEncoding::Builtin.char_name(c), None);
}
assert!(FontEncoding::MsSymbol.unicodes().is_some());
}
#[test]
fn only_four_encoding_names_are_recognised() {
assert_eq!(
FontEncoding::from_pdf_name(b"WinAnsiEncoding"),
Some(FontEncoding::WinAnsi)
);
assert_eq!(
FontEncoding::from_pdf_name(b"MacRomanEncoding"),
Some(FontEncoding::MacRoman)
);
assert_eq!(
FontEncoding::from_pdf_name(b"MacExpertEncoding"),
Some(FontEncoding::MacExpert)
);
assert_eq!(
FontEncoding::from_pdf_name(b"PDFDocEncoding"),
Some(FontEncoding::PdfDoc)
);
assert_eq!(FontEncoding::from_pdf_name(b"StandardEncoding"), None);
assert_eq!(FontEncoding::from_pdf_name(b"Identity-H"), None);
assert_eq!(FontEncoding::from_pdf_name(b""), None);
}
#[test]
fn differences_win_over_the_predefined_set() {
let mut diffs = NO_DIFFS;
diffs[65] = Some(GlyphName::from("mycustomglyph"));
assert_eq!(
adobe_char_name(FontEncoding::WinAnsi, &diffs, 65),
Some(&b"mycustomglyph"[..])
);
assert_eq!(
adobe_char_name(FontEncoding::WinAnsi, &diffs, 66),
Some(&b"B"[..])
);
}
#[test]
fn differences_are_the_only_names_a_builtin_font_has() {
let mut diffs = NO_DIFFS;
diffs[1] = Some(GlyphName::from("gee"));
assert_eq!(
adobe_char_name(FontEncoding::Builtin, &diffs, 1),
Some(&b"gee"[..])
);
assert_eq!(adobe_char_name(FontEncoding::Builtin, &diffs, 2), None);
}
#[test]
fn a_charcode_above_255_never_has_a_name() {
assert_eq!(adobe_char_name(FontEncoding::WinAnsi, &NO_DIFFS, 256), None);
assert_eq!(
adobe_char_name(FontEncoding::WinAnsi, &NO_DIFFS, u32::MAX),
None
);
}
#[test]
fn the_face_encoding_reverse_map_uses_the_right_table() {
assert_eq!(FaceEncoding::Unicode.charcode_from_unicode(0x20AC), 0x20AC);
assert_eq!(FaceEncoding::Latin1.charcode_from_unicode(0x20AC), 0x80);
assert_eq!(FaceEncoding::Latin1.charcode_from_unicode(0x4E00), 0);
assert_eq!(FaceEncoding::Other.charcode_from_unicode(0x41), 0);
}
#[test]
fn apple_roman_reads_the_mac_roman_table() {
assert_eq!(unicode_from_apple_roman(b'A'), u16::from(b'A'));
assert_eq!(unicode_from_apple_roman(0xA5), 0x2022); }
#[test]
fn every_name_table_round_trips_through_the_glyph_list() {
let mut checked = 0usize;
let mut agreed = 0usize;
for e in [
FontEncoding::Standard,
FontEncoding::WinAnsi,
FontEncoding::MacRoman,
FontEncoding::PdfDoc,
FontEncoding::AdobeSymbol,
FontEncoding::ZapfDingbats,
] {
let Some(unicodes) = e.unicodes() else {
continue;
};
for code in 0u8..=255 {
let (Some(name), u) = (e.char_name(code), unicodes[usize::from(code)]) else {
continue;
};
if u == 0 {
continue;
}
checked += 1;
if unicode_from_adobe_name(name.as_bytes()) == u {
agreed += 1;
}
}
}
assert!(checked > 1000, "expected a broad sweep, got {checked}");
assert!(
agreed * 100 / checked >= 80,
"{agreed} of {checked} names agreed with the glyph list"
);
}
}