Skip to main content

read_fonts/tables/
post.rs

1//! the [post (PostScript)](https://docs.microsoft.com/en-us/typography/opentype/spec/post#header) table
2
3include!("../../generated/generated_post.rs");
4
5#[allow(clippy::needless_lifetimes)] // 'a is used with experimental_traverse feature below
6impl<'a> Post<'a> {
7    /// The number of glyph names covered by this table
8    pub fn num_names(&self) -> usize {
9        match self.version() {
10            Version16Dot16::VERSION_1_0 => DEFAULT_GLYPH_NAMES.len(),
11            Version16Dot16::VERSION_2_0 => self.num_glyphs().unwrap_or_default() as usize,
12            _ => 0,
13        }
14    }
15
16    /// Returns the name for the given glyph.
17    ///
18    /// Note that this is a relatively expensive operation, as it may require
19    /// a linear scan through the string data to find the target name. If you
20    /// need to iterate over all glyph names or collect them into a map for
21    /// faster access, use [`Self::glyph_names`] instead.
22    pub fn glyph_name(&self, glyph_id: GlyphId16) -> Option<&'a str> {
23        let glyph_id = glyph_id.to_u16() as usize;
24        match self.version() {
25            Version16Dot16::VERSION_1_0 => DEFAULT_GLYPH_NAMES.get(glyph_id).copied(),
26            Version16Dot16::VERSION_2_0 => {
27                let idx = self.glyph_name_index()?.get(glyph_id)?.get() as usize;
28                if idx < DEFAULT_GLYPH_NAMES.len() {
29                    return DEFAULT_GLYPH_NAMES.get(idx).copied();
30                }
31                let idx = idx - DEFAULT_GLYPH_NAMES.len();
32                self.string_data()?.get(idx)?.ok().map(|s| s.0)
33            }
34            _ => None,
35        }
36    }
37
38    /// Return an iterator over the glyph names in this table.
39    pub fn glyph_names(&self) -> GlyphNames<'a> {
40        let num_names = self.num_names() as u32;
41        let kind = match self.version() {
42            Version16Dot16::VERSION_1_0 => GlyphNameIterKind::V1(self.clone(), 0),
43            Version16Dot16::VERSION_2_0 => GlyphNameIterKind::V2 {
44                post: self.clone(),
45                idx: 0,
46                checkpoint_stride: (num_names as usize).div_ceil(NUM_CHECKPOINTS + 1).max(1),
47                checkpoints: [UNSET_CHECKPOINT; NUM_CHECKPOINTS],
48                last_actual_idx: None,
49                last_offset: 0,
50            },
51            _ => GlyphNameIterKind::None,
52        };
53        GlyphNames { num_names, kind }
54    }
55
56    //FIXME: how do we want to traverse this? I want to stop needing to
57    // add special cases for things...
58    #[cfg(feature = "experimental_traverse")]
59    fn traverse_string_data(&self) -> FieldType<'a> {
60        FieldType::I8(-42) // meaningless value
61    }
62}
63
64/// A string in the post table.
65///
66/// This is basically just a newtype that knows how to parse from a Pascal-style
67/// string.
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct PString<'a>(&'a str);
70
71impl<'a> PString<'a> {
72    pub fn as_str(&self) -> &'a str {
73        self.0
74    }
75}
76
77impl std::ops::Deref for PString<'_> {
78    type Target = str;
79    fn deref(&self) -> &Self::Target {
80        self.0
81    }
82}
83
84impl PartialEq<&str> for PString<'_> {
85    fn eq(&self, other: &&str) -> bool {
86        self.0 == *other
87    }
88}
89
90impl ReadArgs for PString<'_> {
91    type Args = ();
92}
93
94impl<'a> FontRead<'a> for PString<'a> {
95    fn read_with_args(data: FontData<'a>, _: ()) -> Result<Self, ReadError> {
96        let len: u8 = data.read_at(0)?;
97        let pstring = data
98            .as_bytes()
99            .get(1..len as usize + 1)
100            .ok_or(ReadError::OutOfBounds)?;
101
102        if pstring.is_ascii() {
103            Ok(PString(std::str::from_utf8(pstring).unwrap()))
104        } else {
105            //FIXME not really sure how we want to handle this?
106            Err(ReadError::MalformedData("Must be valid ascii"))
107        }
108    }
109}
110
111impl VarSize for PString<'_> {
112    type Size = u8;
113}
114
115const NUM_CHECKPOINTS: usize = 16;
116const UNSET_CHECKPOINT: u32 = u32::MAX;
117
118/// Iterator over the glyph names in a post table.
119#[derive(Clone)]
120pub struct GlyphNames<'a> {
121    num_names: u32,
122    kind: GlyphNameIterKind<'a>,
123}
124
125#[derive(Clone)]
126enum GlyphNameIterKind<'a> {
127    None,
128    V1(Post<'a>, u32),
129    V2 {
130        post: Post<'a>,
131        idx: u32,
132        // The number of indices between checkpoints
133        checkpoint_stride: usize,
134        // The offset of each checkpoint; checkpoint 0 is implicit
135        checkpoints: [u32; NUM_CHECKPOINTS],
136        // The last actual index and that was scanned, for monotonic fast path
137        last_actual_idx: Option<usize>,
138        // The offset associated with the last scanned index
139        last_offset: usize,
140    },
141}
142
143impl<'a> Iterator for GlyphNames<'a> {
144    type Item = (GlyphId, &'a str);
145
146    fn next(&mut self) -> Option<Self::Item> {
147        match &mut self.kind {
148            GlyphNameIterKind::None => None,
149            GlyphNameIterKind::V1(post, idx) => {
150                if *idx >= self.num_names {
151                    return None;
152                }
153                let gid = GlyphId16::new(*idx as u16);
154                let name = post.glyph_name(gid)?;
155                *idx += 1;
156                Some((gid.into(), name))
157            }
158            GlyphNameIterKind::V2 {
159                post,
160                idx,
161                checkpoint_stride,
162                checkpoints,
163                last_actual_idx,
164                last_offset,
165            } => {
166                if *idx >= self.num_names {
167                    return None;
168                }
169                let stride = *checkpoint_stride;
170                let gid = GlyphId16::new(*idx as u16);
171                let mut actual_idx = post.glyph_name_index()?.get(*idx as usize)?.get() as usize;
172                let name = if actual_idx < DEFAULT_GLYPH_NAMES.len() {
173                    DEFAULT_GLYPH_NAMES.get(actual_idx).copied()?
174                } else {
175                    actual_idx -= DEFAULT_GLYPH_NAMES.len();
176                    let string_data = post.data.slice(post.string_data_byte_range())?;
177                    // Checkpoint 0 is implicit and always at offset 0; the
178                    // array stores logical checkpoints 1..=NUM_CHECKPOINTS.
179                    let target_slot = (actual_idx / stride).min(NUM_CHECKPOINTS);
180                    // Find the the starting location for our scan
181                    let (mut scan_idx, mut offset) = {
182                        // Search backward from the target slot to find the
183                        // nearest checkpoint that has been set
184                        let mut slot = target_slot;
185                        while slot > 0 && checkpoints[slot - 1] == UNSET_CHECKPOINT {
186                            slot -= 1;
187                        }
188                        if slot == 0 {
189                            // Fallback to implicit checkpoint 0
190                            (0, 0)
191                        } else {
192                            // Otherwise, start scanning from the nearest
193                            // checkpoint
194                            (slot * stride, checkpoints[slot - 1] as usize)
195                        }
196                    };
197                    // See if we can use the monotonic fast path.
198                    if let Some(last_idx) = *last_actual_idx {
199                        // Start scanning from the last index if that provides
200                        // a smaller search space than the nearest checkpoint
201                        if last_idx <= actual_idx && last_idx > scan_idx {
202                            scan_idx = last_idx;
203                            offset = *last_offset;
204                        }
205                    }
206                    // Now do the linear scan over the string data
207                    while scan_idx < actual_idx {
208                        let item_len = PString::read_len_at(string_data, offset)?;
209                        offset = offset.checked_add(item_len)?;
210                        scan_idx += 1;
211                        // If this index is a checkpoint, record the offset
212                        // for future scans
213                        if scan_idx % stride == 0 {
214                            let slot = (scan_idx / stride).min(NUM_CHECKPOINTS);
215                            if slot > 0 {
216                                checkpoints[slot - 1] = u32::try_from(offset).ok()?;
217                            }
218                        }
219                    }
220                    if actual_idx % stride == 0 && target_slot > 0 {
221                        checkpoints[target_slot - 1] = u32::try_from(offset).ok()?;
222                    }
223                    // Record the last index and offset for future scans
224                    *last_actual_idx = Some(actual_idx);
225                    *last_offset = offset;
226                    PString::read(string_data.split_off(offset)?).ok()?.0
227                };
228                *idx += 1;
229                Some((gid.into(), name))
230            }
231        }
232    }
233}
234
235/// The 258 glyph names defined for Macintosh TrueType fonts
236#[rustfmt::skip]
237pub static DEFAULT_GLYPH_NAMES: [&str; 258] = [
238    ".notdef", ".null", "nonmarkingreturn", "space", "exclam", "quotedbl", "numbersign", "dollar",
239    "percent", "ampersand", "quotesingle", "parenleft", "parenright", "asterisk", "plus", "comma",
240    "hyphen", "period", "slash", "zero", "one", "two", "three", "four", "five", "six", "seven",
241    "eight", "nine", "colon", "semicolon", "less", "equal", "greater", "question", "at", "A", "B",
242    "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U",
243    "V", "W", "X", "Y", "Z", "bracketleft", "backslash", "bracketright", "asciicircum",
244    "underscore", "grave", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n",
245    "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "braceleft", "bar", "braceright",
246    "asciitilde", "Adieresis", "Aring", "Ccedilla", "Eacute", "Ntilde", "Odieresis", "Udieresis",
247    "aacute", "agrave", "acircumflex", "adieresis", "atilde", "aring", "ccedilla", "eacute",
248    "egrave", "ecircumflex", "edieresis", "iacute", "igrave", "icircumflex", "idieresis", "ntilde",
249    "oacute", "ograve", "ocircumflex", "odieresis", "otilde", "uacute", "ugrave", "ucircumflex",
250    "udieresis", "dagger", "degree", "cent", "sterling", "section", "bullet", "paragraph",
251    "germandbls", "registered", "copyright", "trademark", "acute", "dieresis", "notequal", "AE",
252    "Oslash", "infinity", "plusminus", "lessequal", "greaterequal", "yen", "mu", "partialdiff",
253    "summation", "product", "pi", "integral", "ordfeminine", "ordmasculine", "Omega", "ae",
254    "oslash", "questiondown", "exclamdown", "logicalnot", "radical", "florin", "approxequal",
255    "Delta", "guillemotleft", "guillemotright", "ellipsis", "nonbreakingspace", "Agrave", "Atilde",
256    "Otilde", "OE", "oe", "endash", "emdash", "quotedblleft", "quotedblright", "quoteleft",
257    "quoteright", "divide", "lozenge", "ydieresis", "Ydieresis", "fraction", "currency",
258    "guilsinglleft", "guilsinglright", "fi", "fl", "daggerdbl", "periodcentered", "quotesinglbase",
259    "quotedblbase", "perthousand", "Acircumflex", "Ecircumflex", "Aacute", "Edieresis", "Egrave",
260    "Iacute", "Icircumflex", "Idieresis", "Igrave", "Oacute", "Ocircumflex", "apple", "Ograve",
261    "Uacute", "Ucircumflex", "Ugrave", "dotlessi", "circumflex", "tilde", "macron", "breve",
262    "dotaccent", "ring", "cedilla", "hungarumlaut", "ogonek", "caron", "Lslash", "lslash",
263    "Scaron", "scaron", "Zcaron", "zcaron", "brokenbar", "Eth", "eth", "Yacute", "yacute", "Thorn",
264    "thorn", "minus", "multiply", "onesuperior", "twosuperior", "threesuperior", "onehalf",
265    "onequarter", "threequarters", "franc", "Gbreve", "gbreve", "Idotaccent", "Scedilla",
266    "scedilla", "Cacute", "cacute", "Ccaron", "ccaron", "dcroat",
267];
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use font_test_data::{bebuffer::BeBuffer, post as test_data};
273
274    #[test]
275    fn test_post() {
276        let table = Post::read(test_data::SIMPLE.into()).unwrap();
277        assert_eq!(table.version(), Version16Dot16::VERSION_2_0);
278        assert_eq!(table.underline_position(), FWord::new(-75));
279        assert_eq!(table.glyph_name(GlyphId16::new(1)), Some(".notdef"));
280        assert_eq!(table.glyph_name(GlyphId16::new(2)), Some("space"));
281        assert_eq!(table.glyph_name(GlyphId16::new(7)), Some("hello"));
282        assert_eq!(table.glyph_name(GlyphId16::new(8)), Some("hi"));
283        assert_eq!(table.glyph_name(GlyphId16::new(9)), Some("hola"));
284    }
285
286    fn make_basic_post(version: Version16Dot16, include_num_glyphs: bool) -> BeBuffer {
287        let buf = BeBuffer::new()
288            .push(version)
289            .push(Fixed::from_i32(5))
290            .extend([FWord::new(6), FWord::new(7)]) //underline pos/thickness
291            .push(0u32) // isFixedPitch
292            .extend([7u32, 8, 9, 10]); // min/max mem x
293        if include_num_glyphs {
294            buf.push(0u16)
295        } else {
296            buf
297        }
298    }
299
300    #[test]
301    fn parse_versioned_fields_v1() {
302        // v1, even if it has the extra field will not read it:
303
304        let buf = make_basic_post(Version16Dot16::VERSION_1_0, true);
305        let postv1 = Post::read(buf.data().into()).unwrap();
306        assert!(postv1.num_glyphs().is_none());
307    }
308
309    #[test]
310    fn parse_versioned_fields_v2() {
311        let buf = make_basic_post(Version16Dot16::VERSION_2_0, false);
312        let postv2 = Post::read(buf.data().into()).unwrap();
313        // v2 will fail to read if data is missing
314        assert!(postv2.num_glyphs().is_none());
315
316        // but read if data is present
317        let buf = make_basic_post(Version16Dot16::VERSION_2_0, true);
318        let postv2 = Post::read(buf.data().into()).unwrap();
319        // v2 will fail to read if data is missing
320        assert_eq!(postv2.num_glyphs(), Some(0));
321    }
322
323    #[test]
324    fn parse_versioned_fields_v3() {
325        // v3 will again not read since this field is not compatible
326        let buf = make_basic_post(Version16Dot16::VERSION_3_0, true);
327        let postv3 = Post::read(buf.data().into()).unwrap();
328        assert!(postv3.num_glyphs().is_none());
329    }
330
331    #[test]
332    fn num_names_defaults_to_zero_without_num_glyphs() {
333        let buf = make_basic_post(Version16Dot16::VERSION_2_0, false);
334        let post = Post::read(buf.data().into()).unwrap();
335        // Just don't panic
336        assert_eq!(post.num_names(), 0);
337    }
338
339    #[test]
340    fn glyph_name_missing_string_data_returns_none() {
341        let buf = BeBuffer::new()
342            .push(Version16Dot16::VERSION_2_0)
343            .push(Fixed::from_i32(5))
344            .extend([FWord::new(6), FWord::new(7)])
345            .push(0u32)
346            .extend([7u32, 8, 9, 10])
347            .push(1u16)
348            .push(258u16);
349        let post = Post::read(buf.data().into()).unwrap();
350        // Just don't panic
351        assert_eq!(post.glyph_name(GlyphId16::new(0)), None);
352    }
353
354    #[test]
355    fn glyph_names_matches_naive_on_varied_synthetic_v2_data() {
356        let num_glyphs = 2_000u16;
357        let orders = [
358            test_data::GlyphNameOrder::Monotonic,
359            test_data::GlyphNameOrder::MostlyMonotonicWithBackrefs,
360            test_data::GlyphNameOrder::AllPointToLast,
361        ];
362        for order in orders {
363            let (bytes, expected_custom_names) =
364                test_data::v2_with_varied_glyph_names(num_glyphs, 63, order);
365            let post = Post::read(bytes.as_slice().into()).unwrap();
366            let from_naive: Vec<_> = (0..num_glyphs)
367                .map(|gid| post.glyph_name(GlyphId16::new(gid)).unwrap())
368                .collect();
369            let from_iter: Vec<_> = post.glyph_names().map(|(_, name)| name).collect();
370            assert_eq!(from_iter, from_naive);
371            for (name, expected) in from_iter.iter().zip(expected_custom_names.iter()) {
372                assert_eq!(*name, expected.as_str());
373            }
374        }
375    }
376}