read_fonts/tables/
cff.rs

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