1mod agl;
12mod differences;
13mod tables;
14
15pub use agl::{adobe_name_from_unicode, unicode_from_adobe_name};
16pub use differences::load_differences;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
26pub enum FontEncoding {
27 #[default]
30 Builtin,
31 WinAnsi,
33 MacRoman,
35 MacExpert,
38 Standard,
40 AdobeSymbol,
42 ZapfDingbats,
44 PdfDoc,
46 MsSymbol,
49}
50
51impl FontEncoding {
52 #[must_use]
57 pub fn unicodes(self) -> Option<&'static [u16; 256]> {
58 Some(match self {
59 Self::Builtin => return None,
60 Self::WinAnsi => &tables::ADOBE_WIN_ANSI_ENCODING,
61 Self::MacRoman => &tables::MAC_ROMAN_ENCODING,
62 Self::MacExpert => &tables::MAC_EXPERT_ENCODING,
63 Self::Standard => &tables::STANDARD_ENCODING,
64 Self::AdobeSymbol => &tables::ADOBE_SYMBOL_ENCODING,
65 Self::ZapfDingbats => &tables::ZAPF_ENCODING,
66 Self::PdfDoc => &tables::PDF_DOC_ENCODING,
67 Self::MsSymbol => &tables::MS_SYMBOL_ENCODING,
68 })
69 }
70
71 #[must_use]
79 pub fn char_name(self, code: u8) -> Option<&'static str> {
80 let (table, first): (&[Option<&'static str>], u8) = match self {
81 Self::Standard => (&tables::STANDARD_ENCODING_NAMES, 32),
82 Self::WinAnsi => (&tables::ADOBE_WIN_ANSI_ENCODING_NAMES, 32),
83 Self::MacRoman => (&tables::MAC_ROMAN_ENCODING_NAMES, 32),
84 Self::MacExpert => (&tables::MAC_EXPERT_ENCODING_NAMES, 32),
85 Self::PdfDoc => (&tables::PDF_DOC_ENCODING_NAMES, 24),
86 Self::AdobeSymbol => (&tables::ADOBE_SYMBOL_ENCODING_NAMES, 32),
87 Self::ZapfDingbats => (&tables::ZAPF_ENCODING_NAMES, 32),
88 Self::MsSymbol | Self::Builtin => return None,
89 };
90 let index = usize::from(code.checked_sub(first)?);
91 table.get(index).copied().flatten()
92 }
93
94 #[must_use]
100 pub fn from_pdf_name(name: &[u8]) -> Option<Self> {
101 Some(match name {
102 b"WinAnsiEncoding" => Self::WinAnsi,
103 b"MacRomanEncoding" => Self::MacRoman,
104 b"MacExpertEncoding" => Self::MacExpert,
105 b"PDFDocEncoding" => Self::PdfDoc,
106 _ => return None,
107 })
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
115pub enum FaceEncoding {
116 Unicode,
118 Latin1,
121 AppleRoman,
123 AdobeCustom,
125 Symbol,
127 Other,
129}
130
131impl FaceEncoding {
132 #[must_use]
139 pub fn charcode_from_unicode(self, unicode: u16) -> u32 {
140 let table: &[u16; 256] = match self {
141 Self::Unicode => return u32::from(unicode),
143 Self::Latin1 => &tables::ADOBE_WIN_ANSI_ENCODING,
144 Self::AppleRoman => &tables::MAC_ROMAN_ENCODING,
145 Self::AdobeCustom => &tables::PDF_DOC_ENCODING,
146 Self::Symbol => &tables::MS_SYMBOL_ENCODING,
147 Self::Other => return 0,
148 };
149 table
150 .iter()
151 .position(|&u| u == unicode)
152 .and_then(|i| u32::try_from(i).ok())
153 .unwrap_or(0)
154 }
155}
156
157#[must_use]
160pub fn unicode_from_apple_roman(code: u8) -> u16 {
161 tables::MAC_ROMAN_ENCODING
162 .get(usize::from(code))
163 .copied()
164 .unwrap_or(0)
165}
166
167#[must_use]
174pub fn adobe_char_name(
175 base: FontEncoding,
176 differences: &[Option<crate::ids::GlyphName>; 256],
177 charcode: u32,
178) -> Option<&[u8]> {
179 let code = u8::try_from(charcode).ok()?;
180 if let Some(name) = differences.get(usize::from(code)).and_then(Option::as_ref) {
181 if !name.as_bytes().is_empty() {
183 return Some(name.as_bytes());
184 }
185 }
186 base.char_name(code).map(str::as_bytes)
187}
188
189#[cfg(test)]
190mod tests {
191 #![allow(clippy::indexing_slicing)]
193 use super::*;
194 use crate::ids::GlyphName;
195
196 const NO_DIFFS: [Option<GlyphName>; 256] = [const { None }; 256];
197
198 #[test]
199 fn builtin_is_the_only_encoding_without_a_unicode_table() {
200 assert!(FontEncoding::Builtin.unicodes().is_none());
201 for e in [
202 FontEncoding::WinAnsi,
203 FontEncoding::MacRoman,
204 FontEncoding::MacExpert,
205 FontEncoding::Standard,
206 FontEncoding::AdobeSymbol,
207 FontEncoding::ZapfDingbats,
208 FontEncoding::PdfDoc,
209 FontEncoding::MsSymbol,
210 ] {
211 assert!(e.unicodes().is_some(), "{e:?}");
212 }
213 }
214
215 #[test]
216 fn name_tables_start_at_32_except_pdfdoc_which_starts_at_24() {
217 assert_eq!(FontEncoding::Standard.char_name(31), None);
219 assert_eq!(FontEncoding::Standard.char_name(32), Some("space"));
220 assert_eq!(FontEncoding::PdfDoc.char_name(23), None);
222 assert!(FontEncoding::PdfDoc.char_name(24).is_some());
223 }
224
225 #[test]
226 fn ms_symbol_and_builtin_have_no_glyph_names() {
227 for c in [0u8, 32, 65, 255] {
228 assert_eq!(FontEncoding::MsSymbol.char_name(c), None);
229 assert_eq!(FontEncoding::Builtin.char_name(c), None);
230 }
231 assert!(FontEncoding::MsSymbol.unicodes().is_some());
233 }
234
235 #[test]
236 fn only_four_encoding_names_are_recognised() {
237 assert_eq!(
238 FontEncoding::from_pdf_name(b"WinAnsiEncoding"),
239 Some(FontEncoding::WinAnsi)
240 );
241 assert_eq!(
242 FontEncoding::from_pdf_name(b"MacRomanEncoding"),
243 Some(FontEncoding::MacRoman)
244 );
245 assert_eq!(
246 FontEncoding::from_pdf_name(b"MacExpertEncoding"),
247 Some(FontEncoding::MacExpert)
248 );
249 assert_eq!(
250 FontEncoding::from_pdf_name(b"PDFDocEncoding"),
251 Some(FontEncoding::PdfDoc)
252 );
253 assert_eq!(FontEncoding::from_pdf_name(b"StandardEncoding"), None);
256 assert_eq!(FontEncoding::from_pdf_name(b"Identity-H"), None);
257 assert_eq!(FontEncoding::from_pdf_name(b""), None);
258 }
259
260 #[test]
261 fn differences_win_over_the_predefined_set() {
262 let mut diffs = NO_DIFFS;
263 diffs[65] = Some(GlyphName::from("mycustomglyph"));
264 assert_eq!(
265 adobe_char_name(FontEncoding::WinAnsi, &diffs, 65),
266 Some(&b"mycustomglyph"[..])
267 );
268 assert_eq!(
270 adobe_char_name(FontEncoding::WinAnsi, &diffs, 66),
271 Some(&b"B"[..])
272 );
273 }
274
275 #[test]
276 fn differences_are_the_only_names_a_builtin_font_has() {
277 let mut diffs = NO_DIFFS;
278 diffs[1] = Some(GlyphName::from("gee"));
279 assert_eq!(
280 adobe_char_name(FontEncoding::Builtin, &diffs, 1),
281 Some(&b"gee"[..])
282 );
283 assert_eq!(adobe_char_name(FontEncoding::Builtin, &diffs, 2), None);
284 }
285
286 #[test]
287 fn a_charcode_above_255_never_has_a_name() {
288 assert_eq!(adobe_char_name(FontEncoding::WinAnsi, &NO_DIFFS, 256), None);
289 assert_eq!(
290 adobe_char_name(FontEncoding::WinAnsi, &NO_DIFFS, u32::MAX),
291 None
292 );
293 }
294
295 #[test]
296 fn the_face_encoding_reverse_map_uses_the_right_table() {
297 assert_eq!(FaceEncoding::Unicode.charcode_from_unicode(0x20AC), 0x20AC);
299 assert_eq!(FaceEncoding::Latin1.charcode_from_unicode(0x20AC), 0x80);
301 assert_eq!(FaceEncoding::Latin1.charcode_from_unicode(0x4E00), 0);
303 assert_eq!(FaceEncoding::Other.charcode_from_unicode(0x41), 0);
304 }
305
306 #[test]
307 fn apple_roman_reads_the_mac_roman_table() {
308 assert_eq!(unicode_from_apple_roman(b'A'), u16::from(b'A'));
309 assert_eq!(unicode_from_apple_roman(0xA5), 0x2022); }
311
312 #[test]
313 fn every_name_table_round_trips_through_the_glyph_list() {
314 let mut checked = 0usize;
319 let mut agreed = 0usize;
320 for e in [
321 FontEncoding::Standard,
322 FontEncoding::WinAnsi,
323 FontEncoding::MacRoman,
324 FontEncoding::PdfDoc,
325 FontEncoding::AdobeSymbol,
326 FontEncoding::ZapfDingbats,
327 ] {
328 let Some(unicodes) = e.unicodes() else {
329 continue;
330 };
331 for code in 0u8..=255 {
332 let (Some(name), u) = (e.char_name(code), unicodes[usize::from(code)]) else {
333 continue;
334 };
335 if u == 0 {
336 continue;
337 }
338 checked += 1;
339 if unicode_from_adobe_name(name.as_bytes()) == u {
340 agreed += 1;
341 }
342 }
343 }
344 assert!(checked > 1000, "expected a broad sweep, got {checked}");
345 assert!(
354 agreed * 100 / checked >= 80,
355 "{agreed} of {checked} names agreed with the glyph list"
356 );
357 }
358}