Skip to main content

read_fonts/ps/cff/
v1.rs

1//! CFF version 1.
2
3include!("../../../generated/generated_cff.rs");
4
5use crate::ps::{
6    cff::{charset::Charset, dict},
7    string::Sid,
8};
9
10/// The [Compact Font Format](https://learn.microsoft.com/en-us/typography/opentype/spec/cff) table.
11#[derive(Clone)]
12pub struct Cff<'a> {
13    header: CffHeader<'a>,
14    names: Index<'a>,
15    top_dicts: Index<'a>,
16    strings: Index<'a>,
17    global_subrs: Index<'a>,
18}
19
20impl<'a> Cff<'a> {
21    pub fn offset_data(&self) -> FontData<'a> {
22        self.header.offset_data()
23    }
24
25    pub fn header(&self) -> CffHeader<'a> {
26        self.header.clone()
27    }
28
29    /// Returns the name index.
30    ///
31    /// This contains the PostScript names of all fonts in the font set.
32    ///
33    /// See "Name INDEX" at <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=13>
34    pub fn names(&self) -> Index<'a> {
35        self.names.clone()
36    }
37
38    /// Returns the PostScript name for the font in the font set at the
39    /// given index.
40    pub fn name(&self, index: usize) -> Option<&'a [u8]> {
41        self.names.get(index)
42    }
43
44    /// Returns the top dict index.
45    ///
46    /// This contains the top-level DICTs of all fonts in the font set. The
47    /// objects here correspond to those in the name index.
48    ///
49    /// See "Top DICT INDEX" at <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=14>
50    pub fn top_dicts(&self) -> Index<'a> {
51        self.top_dicts.clone()
52    }
53
54    /// Returns the string index.
55    ///
56    /// This contains all of the strings used by fonts within the font set.
57    /// They are referenced by string identifiers represented by the
58    /// [`Sid`] type.
59    ///
60    /// See "String INDEX" at <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=17>
61    pub fn strings(&self) -> Index<'a> {
62        self.strings.clone()
63    }
64
65    /// Returns the associated string for the given identifier.
66    ///
67    /// If the identifier does not represent a standard string, the result is
68    /// looked up in the string index.
69    pub fn string(&self, id: Sid) -> Option<&'a [u8]> {
70        match id.resolve_standard() {
71            Ok(name) => Some(name),
72            Err(ix) => self.strings.get(ix),
73        }
74    }
75
76    /// Returns the global subroutine index.
77    ///
78    /// This contains sub-programs that are referenced by one or more
79    /// charstrings in the font set.
80    ///
81    /// See "Local/Global Subrs INDEXes" at <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=25>
82    pub fn global_subrs(&self) -> Index<'a> {
83        self.global_subrs.clone()
84    }
85
86    /// Returns the character set associated with the top dict at the given
87    /// index.
88    ///
89    /// See "Charsets" at <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=21>
90    pub fn charset(&self, top_dict_index: usize) -> Option<Charset<'a>> {
91        let top_dict = self.top_dicts().get(top_dict_index)?;
92        let offset_data = self.offset_data();
93        let mut charset_offset: Option<usize> = None;
94        let mut num_glyphs: Option<u32> = None;
95        for entry in dict::entries(top_dict, None) {
96            match entry {
97                Ok(dict::Entry::Charset(offset)) => {
98                    charset_offset = Some(offset);
99                }
100                Ok(dict::Entry::CharstringsOffset(offset)) => {
101                    num_glyphs =
102                        Some(Index::read(offset_data.split_off(offset)?).ok()?.count() as u32);
103                }
104                // The ROS operator signifies a CID-keyed font and the charset
105                // maps to CIDs rather than SIDs which we don't parse for
106                // glyph names.
107                // <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=28>
108                Ok(dict::Entry::Ros { .. }) => {
109                    return None;
110                }
111                _ => {}
112            }
113        }
114        let (offset, num_glyphs) = charset_offset.zip(num_glyphs)?;
115        Charset::new(offset_data, offset, num_glyphs)
116    }
117}
118
119impl TopLevelTable for Cff<'_> {
120    const TAG: Tag = Tag::new(b"CFF ");
121}
122
123impl ReadArgs for Cff<'_> {
124    type Args = ();
125}
126
127impl<'a> FontRead<'a> for Cff<'a> {
128    fn read_with_args(data: FontData<'a>, _: ()) -> Result<Self, ReadError> {
129        let header = CffHeader::read(data)?;
130        let mut data = FontData::new(header.trailing_data());
131        let names = Index::read(data)?;
132        data = data
133            .split_off(names.size_in_bytes().ok_or(ReadError::OutOfBounds)?)
134            .ok_or(ReadError::OutOfBounds)?;
135        let top_dicts = Index::read(data)?;
136        data = data
137            .split_off(top_dicts.size_in_bytes().ok_or(ReadError::OutOfBounds)?)
138            .ok_or(ReadError::OutOfBounds)?;
139        let strings = Index::read(data)?;
140        data = data
141            .split_off(strings.size_in_bytes().ok_or(ReadError::OutOfBounds)?)
142            .ok_or(ReadError::OutOfBounds)?;
143        let global_subrs = Index::read(data)?;
144        Ok(Self {
145            header,
146            names,
147            top_dicts,
148            strings,
149            global_subrs,
150        })
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::{FontRef, TableProvider};
158
159    #[test]
160    fn read_noto_serif_display_cff() {
161        let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
162        let cff = font.cff().unwrap();
163        assert_eq!(cff.header().major(), 1);
164        assert_eq!(cff.header().minor(), 0);
165        assert_eq!(cff.top_dicts().count(), 1);
166        assert_eq!(cff.names().count(), 1);
167        assert_eq!(cff.global_subrs.count(), 17);
168        let name = cff.names().get(0).unwrap();
169        assert_eq!(name, b"NotoSerifDisplay-Regular");
170        assert_eq!(cff.strings().count(), 5);
171        // Version
172        assert_eq!(cff.string(Sid::new(391)).unwrap(), b"2.9");
173        // Notice
174        assert_eq!(
175            cff.string(Sid::new(392)).unwrap(),
176            b"Noto is a trademark of Google LLC."
177        );
178        // Copyright
179        assert_eq!(
180            cff.string(Sid::new(393)).unwrap(),
181            b"Copyright 2022 The Noto Project Authors https:github.comnotofontslatin-greek-cyrillic"
182        );
183        // FullName
184        assert_eq!(
185            cff.string(Sid::new(394)).unwrap(),
186            b"Noto Serif Display Regular"
187        );
188        // FamilyName
189        assert_eq!(cff.string(Sid::new(395)).unwrap(), b"Noto Serif Display");
190    }
191
192    #[test]
193    fn glyph_names() {
194        test_glyph_names(
195            font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
196            &[".notdef", "i", "j", "k", "l"],
197        );
198    }
199
200    #[test]
201    fn icons_glyph_names() {
202        test_glyph_names(font_test_data::MATERIAL_ICONS_SUBSET, &[".notdef", "_10k"]);
203    }
204
205    fn test_glyph_names(font_data: &[u8], expected_names: &[&str]) {
206        let font = FontRef::new(font_data).unwrap();
207        let cff = font.cff().unwrap();
208        let charset = cff.charset(0).unwrap();
209        let sid_to_string = |sid| std::str::from_utf8(cff.string(sid).unwrap()).unwrap();
210        let names_by_lookup = (0..charset.num_glyphs())
211            .map(|gid| sid_to_string(charset.string_id(GlyphId::new(gid)).unwrap()))
212            .collect::<Vec<_>>();
213        assert_eq!(names_by_lookup, expected_names);
214        let names_by_iter = charset
215            .iter()
216            .map(|(_gid, sid)| sid_to_string(sid))
217            .collect::<Vec<_>>();
218        assert_eq!(names_by_iter, expected_names);
219    }
220}