pdfrum_type1/encoding.rs
1//! The font's built-in encoding: the `/Encoding` vector that maps character
2//! codes 0–255 to glyph names.
3//!
4//! A Type 1 program writes this in one of two ways, and both appear in the
5//! wild:
6//!
7//! - the name of a predefined vector — in practice always `StandardEncoding`;
8//! - a built array, `0 1 255 {1 index exch /.notdef put} for` followed by a run
9//! of `dup <code> /<name> put`, terminated by `readonly def`.
10//!
11//! The predefined tables themselves are `read_fonts::ps::encoding`'s, which is
12//! genuine coverage rather than a table we would otherwise copy: they are the
13//! Adobe CFF standard/expert/ISO-Latin-1 encodings, identical in both formats.
14//! The same crate's Adobe Glyph List answers the name → Unicode question that
15//! the synthesized charmap needs.
16
17use read_fonts::ps::{agl, encoding::PredefinedEncoding};
18
19/// The font's built-in character-code → glyph-name mapping.
20///
21/// The `Custom` case stores names rather than glyph ids so a caller can ask
22/// what a code *means* even when the `/CharStrings` dictionary has no such
23/// glyph — which is exactly the situation a subsetted font leaves behind.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum Encoding {
26 /// Adobe `StandardEncoding`.
27 Standard,
28 /// Adobe `ExpertEncoding`.
29 Expert,
30 /// Adobe `ISOLatin1Encoding`.
31 IsoLatin1,
32 /// A vector the font built itself. Entries the font left at `.notdef` are
33 /// `None`.
34 Custom(Box<[Option<Box<str>>; 256]>),
35}
36
37impl Encoding {
38 /// The glyph name a character code selects, or `None` where the vector is
39 /// `.notdef`.
40 ///
41 /// ```
42 /// use pdfrum_type1::Encoding;
43 ///
44 /// assert_eq!(Encoding::Standard.glyph_name(b'A'), Some("A"));
45 /// assert_eq!(Encoding::Standard.glyph_name(b'\''), Some("quoteright"));
46 /// assert_eq!(Encoding::Standard.glyph_name(0), None); // .notdef
47 /// ```
48 #[must_use]
49 pub fn glyph_name(&self, code: u8) -> Option<&str> {
50 match self {
51 Self::Standard => predefined_name(PredefinedEncoding::Standard, code),
52 Self::Expert => predefined_name(PredefinedEncoding::Expert, code),
53 Self::IsoLatin1 => predefined_name(PredefinedEncoding::IsoLatin1, code),
54 Self::Custom(table) => table.get(code as usize)?.as_deref(),
55 }
56 }
57
58 /// Whether this is one of the three predefined vectors, and which.
59 ///
60 /// `pdfrum-font` needs this to reproduce PDFium's `UseType1Charmap`
61 /// decision, which turns on whether the face carries a *built-in* Adobe
62 /// encoding rather than a font-specific one.
63 #[must_use]
64 pub fn predefined(&self) -> Option<PredefinedEncoding> {
65 match self {
66 Self::Standard => Some(PredefinedEncoding::Standard),
67 Self::Expert => Some(PredefinedEncoding::Expert),
68 Self::IsoLatin1 => Some(PredefinedEncoding::IsoLatin1),
69 Self::Custom(_) => None,
70 }
71 }
72}
73
74/// `PredefinedEncoding::name` returns `.notdef` — spelled out — for the holes
75/// in the table. Callers here want `None` for a hole.
76fn predefined_name(enc: PredefinedEncoding, code: u8) -> Option<&'static str> {
77 match enc.name(code) {
78 "" | ".notdef" => None,
79 name => Some(name),
80 }
81}
82
83/// The name a `seac` component code selects.
84///
85/// `seac` (standard-encoded accented character) composes two glyphs named by
86/// their *`StandardEncoding`* codes regardless of what the font's own
87/// `/Encoding` says — that indirection is the whole point of the operator, and
88/// getting it wrong silently swaps accents.
89#[must_use]
90pub fn standard_encoding_name(code: u8) -> Option<&'static str> {
91 predefined_name(PredefinedEncoding::Standard, code)
92}
93
94/// The Unicode scalar a glyph name denotes, by the Adobe Glyph List plus the
95/// `uniXXXX` / `uXXXXXX` conventions.
96///
97/// Returns `None` for a name that maps to a sequence rather than a single
98/// scalar (`ffi`), which is the right answer for a charmap: a `char` → glyph
99/// lookup cannot represent it.
100#[must_use]
101pub fn unicode_from_glyph_name(name: &str) -> Option<char> {
102 agl::name_to_char(name)
103}
104
105/// Build a `Custom` encoding from the `(code, name)` pairs a font declared.
106///
107/// Later pairs win, matching PostScript's `put` semantics.
108pub(crate) fn custom_from_pairs<'a>(pairs: impl IntoIterator<Item = (u8, &'a [u8])>) -> Encoding {
109 // `[None; 256]` needs `Copy`, which `Option<Box<str>>` is not.
110 let mut table: Box<[Option<Box<str>>; 256]> =
111 Box::new(core::array::from_fn(|_| Option::<Box<str>>::None));
112 for (code, name) in pairs {
113 let Ok(name) = core::str::from_utf8(name) else {
114 continue;
115 };
116 if name == ".notdef" || name.is_empty() {
117 continue;
118 }
119 if let Some(slot) = table.get_mut(code as usize) {
120 *slot = Some(name.into());
121 }
122 }
123 Encoding::Custom(table)
124}
125
126#[cfg(test)]
127mod tests {
128 use super::{Encoding, custom_from_pairs, standard_encoding_name, unicode_from_glyph_name};
129
130 #[test]
131 fn standard_encoding_has_the_quirks_that_matter() {
132 // The three that separate StandardEncoding from Latin-1 and that a
133 // `seac` decomposition depends on.
134 assert_eq!(standard_encoding_name(0x27), Some("quoteright"));
135 assert_eq!(standard_encoding_name(0x60), Some("quoteleft"));
136 assert_eq!(standard_encoding_name(0xC1), Some("grave"));
137 assert_eq!(standard_encoding_name(0xC5), Some("macron"));
138 // And the holes really are holes.
139 assert_eq!(standard_encoding_name(0x00), None);
140 assert_eq!(standard_encoding_name(0x80), None);
141 }
142
143 #[test]
144 fn custom_vector_overrides_and_ignores_notdef() {
145 let enc = custom_from_pairs([
146 (65u8, b"A".as_slice()),
147 (65, b"Alpha"), // later put wins
148 (66, b".notdef"),
149 ]);
150 assert_eq!(enc.glyph_name(65), Some("Alpha"));
151 assert_eq!(enc.glyph_name(66), None);
152 assert_eq!(enc.glyph_name(200), None);
153 assert!(enc.predefined().is_none());
154 }
155
156 #[test]
157 fn agl_answers_names_and_declines_sequences() {
158 assert_eq!(unicode_from_glyph_name("A"), Some('A'));
159 assert_eq!(unicode_from_glyph_name("quoteright"), Some('\u{2019}'));
160 assert_eq!(unicode_from_glyph_name("uni20AC"), Some('\u{20AC}'));
161 assert_eq!(unicode_from_glyph_name("nosuchglyphname"), None);
162 // `ffi` has a single-scalar ligature codepoint, so it *is* mappable.
163 assert_eq!(unicode_from_glyph_name("ffi"), Some('\u{FB03}'));
164 // A name spelling a multi-scalar sequence is not.
165 assert_eq!(unicode_from_glyph_name("uni004100420043"), None);
166 }
167
168 #[test]
169 fn predefined_round_trips() {
170 assert!(Encoding::Standard.predefined().is_some());
171 assert!(Encoding::Expert.predefined().is_some());
172 assert!(Encoding::IsoLatin1.predefined().is_some());
173 }
174}