use read_fonts::ps::{agl, encoding::PredefinedEncoding};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Encoding {
Standard,
Expert,
IsoLatin1,
Custom(Box<[Option<Box<str>>; 256]>),
}
impl Encoding {
#[must_use]
pub fn glyph_name(&self, code: u8) -> Option<&str> {
match self {
Self::Standard => predefined_name(PredefinedEncoding::Standard, code),
Self::Expert => predefined_name(PredefinedEncoding::Expert, code),
Self::IsoLatin1 => predefined_name(PredefinedEncoding::IsoLatin1, code),
Self::Custom(table) => table.get(code as usize)?.as_deref(),
}
}
#[must_use]
pub fn predefined(&self) -> Option<PredefinedEncoding> {
match self {
Self::Standard => Some(PredefinedEncoding::Standard),
Self::Expert => Some(PredefinedEncoding::Expert),
Self::IsoLatin1 => Some(PredefinedEncoding::IsoLatin1),
Self::Custom(_) => None,
}
}
}
fn predefined_name(enc: PredefinedEncoding, code: u8) -> Option<&'static str> {
match enc.name(code) {
"" | ".notdef" => None,
name => Some(name),
}
}
#[must_use]
pub fn standard_encoding_name(code: u8) -> Option<&'static str> {
predefined_name(PredefinedEncoding::Standard, code)
}
#[must_use]
pub fn unicode_from_glyph_name(name: &str) -> Option<char> {
agl::name_to_char(name)
}
pub(crate) fn custom_from_pairs<'a>(pairs: impl IntoIterator<Item = (u8, &'a [u8])>) -> Encoding {
let mut table: Box<[Option<Box<str>>; 256]> =
Box::new(core::array::from_fn(|_| Option::<Box<str>>::None));
for (code, name) in pairs {
let Ok(name) = core::str::from_utf8(name) else {
continue;
};
if name == ".notdef" || name.is_empty() {
continue;
}
if let Some(slot) = table.get_mut(code as usize) {
*slot = Some(name.into());
}
}
Encoding::Custom(table)
}
#[cfg(test)]
mod tests {
use super::{Encoding, custom_from_pairs, standard_encoding_name, unicode_from_glyph_name};
#[test]
fn standard_encoding_has_the_quirks_that_matter() {
assert_eq!(standard_encoding_name(0x27), Some("quoteright"));
assert_eq!(standard_encoding_name(0x60), Some("quoteleft"));
assert_eq!(standard_encoding_name(0xC1), Some("grave"));
assert_eq!(standard_encoding_name(0xC5), Some("macron"));
assert_eq!(standard_encoding_name(0x00), None);
assert_eq!(standard_encoding_name(0x80), None);
}
#[test]
fn custom_vector_overrides_and_ignores_notdef() {
let enc = custom_from_pairs([
(65u8, b"A".as_slice()),
(65, b"Alpha"), (66, b".notdef"),
]);
assert_eq!(enc.glyph_name(65), Some("Alpha"));
assert_eq!(enc.glyph_name(66), None);
assert_eq!(enc.glyph_name(200), None);
assert!(enc.predefined().is_none());
}
#[test]
fn agl_answers_names_and_declines_sequences() {
assert_eq!(unicode_from_glyph_name("A"), Some('A'));
assert_eq!(unicode_from_glyph_name("quoteright"), Some('\u{2019}'));
assert_eq!(unicode_from_glyph_name("uni20AC"), Some('\u{20AC}'));
assert_eq!(unicode_from_glyph_name("nosuchglyphname"), None);
assert_eq!(unicode_from_glyph_name("ffi"), Some('\u{FB03}'));
assert_eq!(unicode_from_glyph_name("uni004100420043"), None);
}
#[test]
fn predefined_round_trips() {
assert!(Encoding::Standard.predefined().is_some());
assert!(Encoding::Expert.predefined().is_some());
assert!(Encoding::IsoLatin1.predefined().is_some());
}
}