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