Skip to main content

skrifa/
glyph_name.rs

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