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    pub fn glyph_name(&self, glyph_id: GlyphId16) -> Option<&str> {
17        let glyph_id = glyph_id.to_u16() as usize;
18        match self.version() {
19            Version16Dot16::VERSION_1_0 => DEFAULT_GLYPH_NAMES.get(glyph_id).copied(),
20            Version16Dot16::VERSION_2_0 => {
21                let idx = self.glyph_name_index()?.get(glyph_id)?.get() as usize;
22                if idx < DEFAULT_GLYPH_NAMES.len() {
23                    return DEFAULT_GLYPH_NAMES.get(idx).copied();
24                }
25                let idx = idx - DEFAULT_GLYPH_NAMES.len();
26                self.string_data()?.get(idx)?.ok().map(|s| s.0)
27            }
28            _ => None,
29        }
30    }
31
32    //FIXME: how do we want to traverse this? I want to stop needing to
33    // add special cases for things...
34    #[cfg(feature = "experimental_traverse")]
35    fn traverse_string_data(&self) -> FieldType<'a> {
36        FieldType::I8(-42) // meaningless value
37    }
38}
39
40/// A string in the post table.
41///
42/// This is basically just a newtype that knows how to parse from a Pascal-style
43/// string.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct PString<'a>(&'a str);
46
47impl<'a> PString<'a> {
48    pub fn as_str(&self) -> &'a str {
49        self.0
50    }
51}
52
53impl std::ops::Deref for PString<'_> {
54    type Target = str;
55    fn deref(&self) -> &Self::Target {
56        self.0
57    }
58}
59
60impl PartialEq<&str> for PString<'_> {
61    fn eq(&self, other: &&str) -> bool {
62        self.0 == *other
63    }
64}
65
66impl<'a> FontRead<'a> for PString<'a> {
67    fn read(data: FontData<'a>) -> Result<Self, ReadError> {
68        let len: u8 = data.read_at(0)?;
69        let pstring = data
70            .as_bytes()
71            .get(1..len as usize + 1)
72            .ok_or(ReadError::OutOfBounds)?;
73
74        if pstring.is_ascii() {
75            Ok(PString(std::str::from_utf8(pstring).unwrap()))
76        } else {
77            //FIXME not really sure how we want to handle this?
78            Err(ReadError::MalformedData("Must be valid ascii"))
79        }
80    }
81}
82
83impl VarSize for PString<'_> {
84    type Size = u8;
85}
86
87/// The 258 glyph names defined for Macintosh TrueType fonts
88#[rustfmt::skip]
89pub static DEFAULT_GLYPH_NAMES: [&str; 258] = [
90    ".notdef", ".null", "nonmarkingreturn", "space", "exclam", "quotedbl", "numbersign", "dollar",
91    "percent", "ampersand", "quotesingle", "parenleft", "parenright", "asterisk", "plus", "comma",
92    "hyphen", "period", "slash", "zero", "one", "two", "three", "four", "five", "six", "seven",
93    "eight", "nine", "colon", "semicolon", "less", "equal", "greater", "question", "at", "A", "B",
94    "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U",
95    "V", "W", "X", "Y", "Z", "bracketleft", "backslash", "bracketright", "asciicircum",
96    "underscore", "grave", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n",
97    "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "braceleft", "bar", "braceright",
98    "asciitilde", "Adieresis", "Aring", "Ccedilla", "Eacute", "Ntilde", "Odieresis", "Udieresis",
99    "aacute", "agrave", "acircumflex", "adieresis", "atilde", "aring", "ccedilla", "eacute",
100    "egrave", "ecircumflex", "edieresis", "iacute", "igrave", "icircumflex", "idieresis", "ntilde",
101    "oacute", "ograve", "ocircumflex", "odieresis", "otilde", "uacute", "ugrave", "ucircumflex",
102    "udieresis", "dagger", "degree", "cent", "sterling", "section", "bullet", "paragraph",
103    "germandbls", "registered", "copyright", "trademark", "acute", "dieresis", "notequal", "AE",
104    "Oslash", "infinity", "plusminus", "lessequal", "greaterequal", "yen", "mu", "partialdiff",
105    "summation", "product", "pi", "integral", "ordfeminine", "ordmasculine", "Omega", "ae",
106    "oslash", "questiondown", "exclamdown", "logicalnot", "radical", "florin", "approxequal",
107    "Delta", "guillemotleft", "guillemotright", "ellipsis", "nonbreakingspace", "Agrave", "Atilde",
108    "Otilde", "OE", "oe", "endash", "emdash", "quotedblleft", "quotedblright", "quoteleft",
109    "quoteright", "divide", "lozenge", "ydieresis", "Ydieresis", "fraction", "currency",
110    "guilsinglleft", "guilsinglright", "fi", "fl", "daggerdbl", "periodcentered", "quotesinglbase",
111    "quotedblbase", "perthousand", "Acircumflex", "Ecircumflex", "Aacute", "Edieresis", "Egrave",
112    "Iacute", "Icircumflex", "Idieresis", "Igrave", "Oacute", "Ocircumflex", "apple", "Ograve",
113    "Uacute", "Ucircumflex", "Ugrave", "dotlessi", "circumflex", "tilde", "macron", "breve",
114    "dotaccent", "ring", "cedilla", "hungarumlaut", "ogonek", "caron", "Lslash", "lslash",
115    "Scaron", "scaron", "Zcaron", "zcaron", "brokenbar", "Eth", "eth", "Yacute", "yacute", "Thorn",
116    "thorn", "minus", "multiply", "onesuperior", "twosuperior", "threesuperior", "onehalf",
117    "onequarter", "threequarters", "franc", "Gbreve", "gbreve", "Idotaccent", "Scedilla",
118    "scedilla", "Cacute", "cacute", "Ccaron", "ccaron", "dcroat",
119];
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use font_test_data::{bebuffer::BeBuffer, post as test_data};
125
126    #[test]
127    fn test_post() {
128        let table = Post::read(test_data::SIMPLE.into()).unwrap();
129        assert_eq!(table.version(), Version16Dot16::VERSION_2_0);
130        assert_eq!(table.underline_position(), FWord::new(-75));
131        assert_eq!(table.glyph_name(GlyphId16::new(1)), Some(".notdef"));
132        assert_eq!(table.glyph_name(GlyphId16::new(2)), Some("space"));
133        assert_eq!(table.glyph_name(GlyphId16::new(7)), Some("hello"));
134        assert_eq!(table.glyph_name(GlyphId16::new(8)), Some("hi"));
135        assert_eq!(table.glyph_name(GlyphId16::new(9)), Some("hola"));
136    }
137
138    fn make_basic_post(version: Version16Dot16, include_num_glyphs: bool) -> BeBuffer {
139        let buf = BeBuffer::new()
140            .push(version)
141            .push(Fixed::from_i32(5))
142            .extend([FWord::new(6), FWord::new(7)]) //underline pos/thickness
143            .push(0u32) // isFixedPitch
144            .extend([7u32, 8, 9, 10]); // min/max mem x
145        if include_num_glyphs {
146            buf.push(0u16)
147        } else {
148            buf
149        }
150    }
151
152    #[test]
153    fn parse_versioned_fields_v1() {
154        // v1, even if it has the extra field will not read it:
155
156        let buf = make_basic_post(Version16Dot16::VERSION_1_0, true);
157        let postv1 = Post::read(buf.data().into()).unwrap();
158        assert!(postv1.num_glyphs().is_none());
159    }
160
161    #[test]
162    fn parse_versioned_fields_v2() {
163        let buf = make_basic_post(Version16Dot16::VERSION_2_0, false);
164        let postv2 = Post::read(buf.data().into()).unwrap();
165        // v2 will fail to read if data is missing
166        assert!(postv2.num_glyphs().is_none());
167
168        // but read if data is present
169        let buf = make_basic_post(Version16Dot16::VERSION_2_0, true);
170        let postv2 = Post::read(buf.data().into()).unwrap();
171        // v2 will fail to read if data is missing
172        assert_eq!(postv2.num_glyphs(), Some(0));
173    }
174
175    #[test]
176    fn parse_versioned_fields_v3() {
177        // v3 will again not read since this field is not compatible
178        let buf = make_basic_post(Version16Dot16::VERSION_3_0, true);
179        let postv3 = Post::read(buf.data().into()).unwrap();
180        assert!(postv3.num_glyphs().is_none());
181    }
182
183    #[test]
184    fn num_names_defaults_to_zero_without_num_glyphs() {
185        let buf = make_basic_post(Version16Dot16::VERSION_2_0, false);
186        let post = Post::read(buf.data().into()).unwrap();
187        // Just don't panic
188        assert_eq!(post.num_names(), 0);
189    }
190
191    #[test]
192    fn glyph_name_missing_string_data_returns_none() {
193        let buf = BeBuffer::new()
194            .push(Version16Dot16::VERSION_2_0)
195            .push(Fixed::from_i32(5))
196            .extend([FWord::new(6), FWord::new(7)])
197            .push(0u32)
198            .extend([7u32, 8, 9, 10])
199            .push(1u16)
200            .push(258u16);
201        let post = Post::read(buf.data().into()).unwrap();
202        // Just don't panic
203        assert_eq!(post.glyph_name(GlyphId16::new(0)), None);
204    }
205}