skrifa/
glyph_name.rs

1//! Support for accessing glyph names.
2
3use core::ops::Range;
4use raw::{
5    tables::{
6        cff::Cff,
7        post::Post,
8        postscript::{Charset, CharsetIter, StringId as Sid},
9    },
10    types::GlyphId,
11    FontRef, TableProvider,
12};
13
14/// "Names must be no longer than 63 characters; some older implementations
15/// can assume a length limit of 31 characters."
16/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/post#version-20>
17const MAX_GLYPH_NAME_LEN: usize = 63;
18
19/// Mapping from glyph identifiers to names.
20///
21/// This sources glyph names from the `post` and `CFF` tables in that order.
22/// If glyph names are not available in either, then they are synthesized
23/// as `gidDDD` where `DDD` is the glyph identifier in decimal. Use the
24/// [`source`](Self::source) to determine which source was chosen.
25#[derive(Clone)]
26pub struct GlyphNames<'a> {
27    inner: Inner<'a>,
28}
29
30#[derive(Clone)]
31enum Inner<'a> {
32    // Second field is num_glyphs
33    Post(Post<'a>, u32),
34    Cff(Cff<'a>, Charset<'a>),
35    Synthesized(u32),
36}
37
38impl<'a> GlyphNames<'a> {
39    /// Creates a new object for accessing glyph names from the given font.
40    pub fn new(font: &FontRef<'a>) -> Self {
41        let num_glyphs = font
42            .maxp()
43            .map(|maxp| maxp.num_glyphs() as u32)
44            .unwrap_or_default();
45        if let Ok(post) = font.post() {
46            if post.num_names() != 0 {
47                return Self {
48                    inner: Inner::Post(post, num_glyphs),
49                };
50            }
51        }
52        if let Some((cff, charset)) = font
53            .cff()
54            .ok()
55            .and_then(|cff| Some((cff.clone(), cff.charset(0).ok()??)))
56        {
57            return Self {
58                inner: Inner::Cff(cff, charset),
59            };
60        }
61        Self {
62            inner: Inner::Synthesized(num_glyphs),
63        }
64    }
65
66    /// Returns the chosen source for glyph names.
67    pub fn source(&self) -> GlyphNameSource {
68        match &self.inner {
69            Inner::Post(..) => GlyphNameSource::Post,
70            Inner::Cff(..) => GlyphNameSource::Cff,
71            Inner::Synthesized(..) => GlyphNameSource::Synthesized,
72        }
73    }
74
75    /// Returns the number of glyphs in the font.
76    pub fn num_glyphs(&self) -> u32 {
77        match &self.inner {
78            Inner::Post(_, n) | Inner::Synthesized(n) => *n,
79            Inner::Cff(_, charset) => charset.num_glyphs(),
80        }
81    }
82
83    /// Returns the name for the given glyph identifier.
84    pub fn get(&self, glyph_id: GlyphId) -> Option<GlyphName> {
85        if glyph_id.to_u32() >= self.num_glyphs() {
86            return None;
87        }
88        let name = match &self.inner {
89            Inner::Post(post, _) => GlyphName::from_post(post, glyph_id),
90            Inner::Cff(cff, charset) => charset
91                .string_id(glyph_id)
92                .ok()
93                .and_then(|sid| GlyphName::from_cff_sid(cff, sid)),
94            _ => None,
95        };
96        // If name is empty string, synthesize it
97        if !name.as_ref().is_some_and(|s| !s.is_empty()) {
98            return Some(GlyphName::synthesize(glyph_id));
99        }
100        Some(name.unwrap_or_else(|| GlyphName::synthesize(glyph_id)))
101    }
102
103    /// Returns an iterator yielding the identifier and name for all glyphs in
104    /// the font.
105    pub fn iter(&self) -> impl Iterator<Item = (GlyphId, GlyphName)> + 'a + Clone {
106        match &self.inner {
107            Inner::Post(post, n) => Iter::Post(0..*n, post.clone()),
108            Inner::Cff(cff, charset) => Iter::Cff(cff.clone(), charset.iter()),
109            Inner::Synthesized(n) => Iter::Synthesized(0..*n),
110        }
111    }
112}
113
114/// Specifies the chosen source for glyph names.
115#[derive(Copy, Clone, PartialEq, Eq, Debug)]
116pub enum GlyphNameSource {
117    /// Glyph names are sourced from the `post` table.
118    Post,
119    /// Glyph names are sourced from the `CFF` table.
120    Cff,
121    /// Glyph names are synthesized in the format `gidDDD` where `DDD` is
122    /// the glyph identifier in decimal.
123    Synthesized,
124}
125
126/// The name of a glyph.
127#[derive(Clone)]
128pub struct GlyphName {
129    name: [u8; MAX_GLYPH_NAME_LEN],
130    len: u8,
131    is_synthesized: bool,
132}
133
134impl GlyphName {
135    /// Returns the underlying name as a string.
136    pub fn as_str(&self) -> &str {
137        let bytes = &self.name[..self.len as usize];
138        core::str::from_utf8(bytes).unwrap_or_default()
139    }
140
141    /// Returns true if the glyph name was synthesized, i.e. not found in any
142    /// source.
143    pub fn is_synthesized(&self) -> bool {
144        self.is_synthesized
145    }
146
147    fn from_bytes(bytes: &[u8]) -> Self {
148        let mut name = Self::default();
149        name.append(bytes);
150        name
151    }
152
153    fn from_post(post: &Post, glyph_id: GlyphId) -> Option<Self> {
154        glyph_id
155            .try_into()
156            .ok()
157            .and_then(|id| post.glyph_name(id))
158            .map(|s| s.as_bytes())
159            .map(Self::from_bytes)
160    }
161
162    fn from_cff_sid(cff: &Cff, sid: Sid) -> Option<Self> {
163        cff.string(sid)
164            .and_then(|s| core::str::from_utf8(s.bytes()).ok())
165            .map(|s| s.as_bytes())
166            .map(Self::from_bytes)
167    }
168
169    fn synthesize(glyph_id: GlyphId) -> Self {
170        use core::fmt::Write;
171        let mut name = Self {
172            is_synthesized: true,
173            ..Self::default()
174        };
175        let _ = write!(GlyphNameWrite(&mut name), "gid{}", glyph_id.to_u32());
176        name
177    }
178
179    /// Appends the given bytes to `self` while keeping the maximum length
180    /// at 63 bytes.
181    ///
182    /// This exists primarily to support the [`core::fmt::Write`] impl
183    /// (which is used for generating synthesized glyph names) because
184    /// we have no guarantee of how many times `write_str` might be called
185    /// for a given format.
186    fn append(&mut self, bytes: &[u8]) {
187        // We simply truncate when length exceeds the max since glyph names
188        // are expected to be <= 63 chars
189        let start = self.len as usize;
190        let available = MAX_GLYPH_NAME_LEN - start;
191        let copy_len = available.min(bytes.len());
192        self.name[start..start + copy_len].copy_from_slice(&bytes[..copy_len]);
193        self.len = (start + copy_len) as u8;
194    }
195}
196
197impl Default for GlyphName {
198    fn default() -> Self {
199        Self {
200            name: [0; MAX_GLYPH_NAME_LEN],
201            len: 0,
202            is_synthesized: false,
203        }
204    }
205}
206
207impl core::fmt::Debug for GlyphName {
208    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
209        f.debug_struct("GlyphName")
210            .field("name", &self.as_str())
211            .field("is_synthesized", &self.is_synthesized)
212            .finish()
213    }
214}
215
216impl core::fmt::Display for GlyphName {
217    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
218        write!(f, "{}", self.as_str())
219    }
220}
221
222impl core::ops::Deref for GlyphName {
223    type Target = str;
224
225    fn deref(&self) -> &Self::Target {
226        self.as_str()
227    }
228}
229
230impl PartialEq<&str> for GlyphName {
231    fn eq(&self, other: &&str) -> bool {
232        self.as_str() == *other
233    }
234}
235
236struct GlyphNameWrite<'a>(&'a mut GlyphName);
237
238impl core::fmt::Write for GlyphNameWrite<'_> {
239    fn write_str(&mut self, s: &str) -> core::fmt::Result {
240        self.0.append(s.as_bytes());
241        Ok(())
242    }
243}
244
245#[derive(Clone)]
246enum Iter<'a> {
247    Post(Range<u32>, Post<'a>),
248    Cff(Cff<'a>, CharsetIter<'a>),
249    Synthesized(Range<u32>),
250}
251
252impl Iter<'_> {
253    fn next_name(&mut self) -> Option<Result<(GlyphId, GlyphName), GlyphId>> {
254        match self {
255            Self::Post(range, post) => {
256                let gid = GlyphId::new(range.next()?);
257                Some(
258                    GlyphName::from_post(post, gid)
259                        .map(|name| (gid, name))
260                        .ok_or(gid),
261                )
262            }
263            Self::Cff(cff, iter) => {
264                let (gid, sid) = iter.next()?;
265                Some(
266                    GlyphName::from_cff_sid(cff, sid)
267                        .map(|name| (gid, name))
268                        .ok_or(gid),
269                )
270            }
271            Self::Synthesized(range) => {
272                let gid = GlyphId::new(range.next()?);
273                Some(Ok((gid, GlyphName::synthesize(gid))))
274            }
275        }
276    }
277}
278
279impl Iterator for Iter<'_> {
280    type Item = (GlyphId, GlyphName);
281
282    fn next(&mut self) -> Option<Self::Item> {
283        match self.next_name()? {
284            Ok((gid, name)) if name.is_empty() => Some((gid, GlyphName::synthesize(gid))),
285            Ok(gid_name) => Some(gid_name),
286            Err(gid) => Some((gid, GlyphName::synthesize(gid))),
287        }
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use raw::{FontData, FontRead};
295
296    #[test]
297    fn synthesized_glyph_names() {
298        let count = 58;
299        let names = GlyphNames {
300            inner: Inner::Synthesized(58),
301        };
302        let names_buf = (0..count).map(|i| format!("gid{i}")).collect::<Vec<_>>();
303        let expected_names = names_buf.iter().map(|s| s.as_str()).collect::<Vec<_>>();
304        for (_, name) in names.iter() {
305            assert!(name.is_synthesized())
306        }
307        check_names(&names, &expected_names, GlyphNameSource::Synthesized);
308    }
309
310    #[test]
311    fn synthesize_for_empty_names() {
312        let mut post_data = font_test_data::post::SIMPLE.to_vec();
313        // last name in this post data is "hola" so pop 5 bytes and then
314        // push a 0 to simulate an empty name
315        post_data.truncate(post_data.len() - 5);
316        post_data.push(0);
317        let post = Post::read(FontData::new(&post_data)).unwrap();
318        let gid = GlyphId::new(9);
319        assert!(post.glyph_name(gid.try_into().unwrap()).unwrap().is_empty());
320        let names = GlyphNames {
321            inner: Inner::Post(post, 10),
322        };
323        assert_eq!(names.get(gid).unwrap(), "gid9");
324        assert_eq!(names.iter().last().unwrap().1, "gid9");
325    }
326
327    #[test]
328    fn cff_glyph_names() {
329        let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
330        let names = GlyphNames::new(&font);
331        assert_eq!(names.source(), GlyphNameSource::Cff);
332        let expected_names = [".notdef", "i", "j", "k", "l"];
333        check_names(&names, &expected_names, GlyphNameSource::Cff);
334    }
335
336    #[test]
337    fn post_glyph_names() {
338        let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
339        let names = GlyphNames::new(&font);
340        let expected_names = [
341            ".notdef",
342            "space",
343            "A",
344            "I",
345            "T",
346            "Aacute",
347            "Agrave",
348            "Iacute",
349            "Igrave",
350            "Amacron",
351            "Imacron",
352            "acutecomb",
353            "gravecomb",
354            "macroncomb",
355            "A.001",
356            "A.002",
357            "A.003",
358            "A.004",
359            "A.005",
360            "A.006",
361            "A.007",
362            "A.008",
363            "A.009",
364            "A.010",
365        ];
366        check_names(&names, &expected_names, GlyphNameSource::Post);
367    }
368
369    #[test]
370    fn post_glyph_names_partial() {
371        let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
372        let mut names = GlyphNames::new(&font);
373        let Inner::Post(_, len) = &mut names.inner else {
374            panic!("it's a post table!");
375        };
376        // Increase count by 4 so we synthesize the remaining names
377        *len += 4;
378        let expected_names = [
379            ".notdef",
380            "space",
381            "A",
382            "I",
383            "T",
384            "Aacute",
385            "Agrave",
386            "Iacute",
387            "Igrave",
388            "Amacron",
389            "Imacron",
390            "acutecomb",
391            "gravecomb",
392            "macroncomb",
393            "A.001",
394            "A.002",
395            "A.003",
396            "A.004",
397            "A.005",
398            "A.006",
399            "A.007",
400            "A.008",
401            "A.009",
402            "A.010",
403            // synthesized names...
404            "gid24",
405            "gid25",
406            "gid26",
407            "gid27",
408        ];
409        check_names(&names, &expected_names, GlyphNameSource::Post);
410    }
411
412    fn check_names(names: &GlyphNames, expected_names: &[&str], expected_source: GlyphNameSource) {
413        assert_eq!(names.source(), expected_source);
414        let iter_names = names.iter().collect::<Vec<_>>();
415        assert_eq!(iter_names.len(), expected_names.len());
416        for (i, expected) in expected_names.iter().enumerate() {
417            let gid = GlyphId::new(i as u32);
418            let name = names.get(gid).unwrap();
419            assert_eq!(name, expected);
420            assert_eq!(iter_names[i].0, gid);
421            assert_eq!(iter_names[i].1, expected);
422        }
423    }
424}