Skip to main content

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}