Skip to main content

read_fonts/model/name/
glyph.rs

1//! Names of individual glyphs.
2
3use super::super::{Font, Kind};
4use crate::{
5    ps::{
6        cff::{charset, CffFontRef},
7        type1::Type1Font,
8    },
9    tables::{cff::Cff, post},
10    types::GlyphId16,
11    TableProvider,
12};
13use alloc::{boxed::Box, vec::Vec};
14use core::{fmt, iter::Peekable, ops::Deref};
15use types::GlyphId;
16
17// The `post` table recommends names of at most 63 bytes. Keep names inline
18// so a lookup needs no allocation, including when a fallback name is
19// synthesized.
20const MAX_GLYPH_NAME_LEN: usize = 63;
21
22/// The source of a glyph name.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum GlyphNameSource {
25    /// The `post` table.
26    Post,
27    /// The `CFF` charset and string index.
28    Cff,
29    /// A Type 1 charstring name.
30    Type1,
31    /// A generated `gidNNN` name.
32    Synthesized,
33}
34
35/// The name of one glyph and the source that supplied it.
36///
37/// A name longer than 63 bytes is truncated at a character boundary.
38/// [`Font::glyph_by_name`](crate::model::Font::glyph_by_name) still finds a
39/// glyph by its full name.
40#[derive(Clone)]
41pub struct GlyphName {
42    name: [u8; MAX_GLYPH_NAME_LEN],
43    len: u8,
44    source: GlyphNameSource,
45}
46
47impl GlyphName {
48    /// Returns the name as a string.
49    pub fn as_str(&self) -> &str {
50        core::str::from_utf8(&self.name[..self.len as usize]).unwrap_or_default()
51    }
52
53    /// Returns the source of this glyph's name.
54    pub fn source(&self) -> GlyphNameSource {
55        self.source
56    }
57
58    /// Returns whether this name was generated because the font supplied none.
59    pub fn is_synthesized(&self) -> bool {
60        self.source == GlyphNameSource::Synthesized
61    }
62
63    fn from_bytes(bytes: &[u8], source: GlyphNameSource) -> Option<Self> {
64        let value = core::str::from_utf8(bytes).ok()?;
65        if value.is_empty() {
66            return None;
67        }
68        let mut name = Self {
69            name: [0; MAX_GLYPH_NAME_LEN],
70            len: 0,
71            source,
72        };
73        name.append(value);
74        Some(name)
75    }
76
77    fn synthesized(glyph: GlyphId) -> Self {
78        use fmt::Write;
79        let mut name = Self {
80            name: [0; MAX_GLYPH_NAME_LEN],
81            len: 0,
82            source: GlyphNameSource::Synthesized,
83        };
84        let _ = write!(GlyphNameWrite(&mut name), "gid{}", glyph.to_u32());
85        name
86    }
87
88    /// Appends as much of `value` as fits, truncated at a character boundary.
89    fn append(&mut self, value: &str) {
90        let start = self.len as usize;
91        let mut count = (MAX_GLYPH_NAME_LEN - start).min(value.len());
92        while !value.is_char_boundary(count) {
93            count -= 1;
94        }
95        self.name[start..start + count].copy_from_slice(&value.as_bytes()[..count]);
96        self.len = (start + count) as u8;
97    }
98}
99
100impl fmt::Debug for GlyphName {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.debug_struct("GlyphName")
103            .field("name", &self.as_str())
104            .field("source", &self.source)
105            .finish()
106    }
107}
108
109impl fmt::Display for GlyphName {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        f.write_str(self.as_str())
112    }
113}
114
115impl Deref for GlyphName {
116    type Target = str;
117
118    fn deref(&self) -> &Self::Target {
119        self.as_str()
120    }
121}
122
123impl PartialEq<&str> for GlyphName {
124    fn eq(&self, other: &&str) -> bool {
125        self.as_str() == *other
126    }
127}
128
129struct GlyphNameWrite<'a>(&'a mut GlyphName);
130
131impl fmt::Write for GlyphNameWrite<'_> {
132    fn write_str(&mut self, value: &str) -> fmt::Result {
133        self.0.append(value);
134        Ok(())
135    }
136}
137
138enum NameSource<'a> {
139    Post(post::Post<'a>),
140    SfntCff(Cff<'a>, charset::Charset<'a>),
141    StandaloneCff(CffFontRef<'a>, charset::Charset<'a>),
142    Type1(&'a Type1Font),
143    Synthesized,
144}
145
146fn source(font: &Font) -> NameSource<'_> {
147    if let Kind::Type1(type1) = font.kind() {
148        return NameSource::Type1(type1);
149    }
150    if let Kind::Cff(cff, _) = font.kind() {
151        if cff.is_cid() {
152            return NameSource::Synthesized;
153        }
154        return cff.charset().map_or(NameSource::Synthesized, |charset| {
155            NameSource::StandaloneCff(cff, charset)
156        });
157    }
158    if let Ok(post) = font.tables().post() {
159        if post.num_names() != 0 {
160            return NameSource::Post(post);
161        }
162    }
163    if let Ok(cff) = font.tables().cff() {
164        if font.cff().is_some_and(|parsed| parsed.is_cid()) {
165            return NameSource::Synthesized;
166        }
167        if let Some(charset) = cff.charset(0) {
168            return NameSource::SfntCff(cff, charset);
169        }
170    }
171    NameSource::Synthesized
172}
173
174pub(crate) fn glyph_name(font: &Font, glyph: GlyphId) -> Option<GlyphName> {
175    if glyph.to_u32() >= font.num_glyphs() {
176        return None;
177    }
178    let name = match source(font) {
179        NameSource::Post(post) => GlyphId16::try_from(glyph)
180            .ok()
181            .and_then(|id| post.glyph_name(id))
182            .and_then(|value| GlyphName::from_bytes(value.as_bytes(), GlyphNameSource::Post)),
183        NameSource::SfntCff(cff, charset) => charset
184            .string_id(glyph)
185            .and_then(|id| cff.string(id))
186            .and_then(|value| GlyphName::from_bytes(value, GlyphNameSource::Cff)),
187        NameSource::StandaloneCff(cff, charset) => charset
188            .string_id(glyph)
189            .and_then(|id| cff.string(id))
190            .and_then(|value| GlyphName::from_bytes(value, GlyphNameSource::Cff)),
191        NameSource::Type1(type1) => type1
192            .glyph_name(glyph)
193            .and_then(|value| GlyphName::from_bytes(value.as_bytes(), GlyphNameSource::Type1)),
194        NameSource::Synthesized => None,
195    };
196    Some(name.unwrap_or_else(|| GlyphName::synthesized(glyph)))
197}
198
199/// A font's glyph names, sorted so a name can be found by binary search.
200pub(crate) struct GlyphNameIndex {
201    // Every name, one after another, in glyph order.
202    names: Box<[u8]>,
203    // The start and end of a name in `names`, and its glyph, sorted by name
204    // and then glyph.
205    entries: Box<[(u32, u32, u32)]>,
206}
207
208impl GlyphNameIndex {
209    pub(crate) fn new(font: &Font) -> Self {
210        let mut names = Vec::new();
211        let mut entries = Vec::new();
212        // Index the names as the font stores them, so a name longer than a
213        // `GlyphName` holds is found by its full spelling.
214        let mut iter = name_iter(font);
215        while let Some((glyph, name)) = iter.next_stored() {
216            let Some((name, _)) = name else {
217                continue;
218            };
219            let start = names.len() as u32;
220            names.extend_from_slice(name.as_bytes());
221            entries.push((start, names.len() as u32, glyph.to_u32()));
222        }
223        let name = |entry: &(u32, u32, u32)| &names[entry.0 as usize..entry.1 as usize];
224        entries.sort_unstable_by(|a, b| name(a).cmp(name(b)).then(a.2.cmp(&b.2)));
225        Self {
226            names: names.into_boxed_slice(),
227            entries: entries.into_boxed_slice(),
228        }
229    }
230
231    pub(crate) fn get(&self, name: &str) -> Option<GlyphId> {
232        let key = name.as_bytes();
233        let index = self.entries.partition_point(|entry| self.name(entry) < key);
234        let entry = self.entries.get(index)?;
235        (self.name(entry) == key).then(|| GlyphId::new(entry.2))
236    }
237
238    fn name(&self, entry: &(u32, u32, u32)) -> &[u8] {
239        &self.names[entry.0 as usize..entry.1 as usize]
240    }
241}
242
243enum NameIterSource<'a> {
244    /// Names read from the `post` table, skipping any that cannot be read.
245    Post(Peekable<post::GlyphNames<'a>>),
246    SfntCff(Cff<'a>, charset::Iter<'a>),
247    StandaloneCff(CffFontRef<'a>, charset::Iter<'a>),
248    Type1(&'a Type1Font),
249    Synthesized,
250}
251
252struct NameIter<'a> {
253    source: NameIterSource<'a>,
254    next: u32,
255    count: u32,
256}
257
258pub(crate) fn glyph_names(font: &Font) -> impl ExactSizeIterator<Item = (GlyphId, GlyphName)> + '_ {
259    name_iter(font)
260}
261
262fn name_iter(font: &Font) -> NameIter<'_> {
263    let source = match source(font) {
264        NameSource::Post(post) => NameIterSource::Post(post.glyph_names().peekable()),
265        NameSource::SfntCff(cff, charset) => NameIterSource::SfntCff(cff, charset.iter()),
266        NameSource::StandaloneCff(cff, charset) => {
267            NameIterSource::StandaloneCff(cff, charset.iter())
268        }
269        NameSource::Type1(type1) => NameIterSource::Type1(type1),
270        NameSource::Synthesized => NameIterSource::Synthesized,
271    };
272    NameIter {
273        source,
274        next: 0,
275        count: font.num_glyphs(),
276    }
277}
278
279impl<'a> NameIter<'a> {
280    /// Returns the next glyph and its name as the font stores it, or `None`
281    /// for the name if the font has no usable one.
282    fn next_stored(&mut self) -> Option<(GlyphId, Option<(&'a str, GlyphNameSource)>)> {
283        if self.next >= self.count {
284            return None;
285        }
286        let glyph = GlyphId::new(self.next);
287        self.next += 1;
288        let cff_string = |cff_string: Option<&'a [u8]>| {
289            cff_string
290                .and_then(|value| core::str::from_utf8(value).ok())
291                .map(|value| (value, GlyphNameSource::Cff))
292        };
293        let name = match &mut self.source {
294            NameIterSource::Post(names) => names
295                .next_if(|(id, _)| *id == glyph)
296                .map(|(_, value)| (value, GlyphNameSource::Post)),
297            NameIterSource::SfntCff(cff, names) => names
298                .next()
299                .and_then(|(id, sid)| cff_string((id == glyph).then(|| cff.string(sid)).flatten())),
300            NameIterSource::StandaloneCff(cff, names) => names
301                .next()
302                .and_then(|(id, sid)| cff_string((id == glyph).then(|| cff.string(sid)).flatten())),
303            NameIterSource::Type1(type1) => type1
304                .glyph_name(glyph)
305                .map(|value| (value, GlyphNameSource::Type1)),
306            NameIterSource::Synthesized => None,
307        };
308        Some((glyph, name.filter(|(value, _)| !value.is_empty())))
309    }
310}
311
312impl Iterator for NameIter<'_> {
313    type Item = (GlyphId, GlyphName);
314
315    fn size_hint(&self) -> (usize, Option<usize>) {
316        let remaining = (self.count - self.next) as usize;
317        (remaining, Some(remaining))
318    }
319
320    fn next(&mut self) -> Option<Self::Item> {
321        let (glyph, name) = self.next_stored()?;
322        let name = name.and_then(|(value, source)| GlyphName::from_bytes(value.as_bytes(), source));
323        Some((glyph, name.unwrap_or_else(|| GlyphName::synthesized(glyph))))
324    }
325}
326
327impl ExactSizeIterator for NameIter<'_> {}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332    use crate::{model::Blob, FontRef};
333    use alloc::sync::Arc;
334    use types::Tag;
335
336    #[test]
337    fn post_names_agree_between_lookup_and_iteration() {
338        let font = Font::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP, 0).unwrap();
339        let mut iter = font.glyph_names();
340        let count = font.num_glyphs() as usize;
341        assert_eq!(iter.size_hint(), (count, Some(count)));
342        assert!(iter.next().is_some());
343        assert_eq!(iter.by_ref().count(), count - 1);
344
345        let names: Vec<_> = font.glyph_names().collect();
346        assert_eq!(names.len(), font.num_glyphs() as usize);
347        assert_eq!(names[0].1, ".notdef");
348        assert_eq!(names[1].1, "space");
349        assert_eq!(names[2].1, "A");
350        for (glyph, name) in names {
351            let looked_up = font.glyph_name(glyph).unwrap();
352            assert_eq!(name.as_str(), looked_up.as_str());
353            assert_eq!(name.source(), looked_up.source());
354        }
355        assert!(font.glyph_name(GlyphId::new(font.num_glyphs())).is_none());
356    }
357
358    #[test]
359    fn a_partial_post_table_synthesizes_only_missing_names() {
360        let source: Arc<dyn Fn(Tag) -> Option<Blob> + Send + Sync> = Arc::new(|tag| {
361            if tag == Tag::new(b"post") {
362                return Some(Blob::from(font_test_data::post::SIMPLE.to_vec()));
363            }
364            let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).ok()?;
365            font.table_data(tag)
366                .map(|data| Blob::from(data.as_bytes().to_vec()))
367        });
368        let font = Font::new(source, 0).unwrap();
369        let names: Vec<_> = font.glyph_names().collect();
370        assert!(font.num_glyphs() > 10);
371        assert_eq!(names[9].1, "hola");
372        assert_eq!(names[9].1.source(), GlyphNameSource::Post);
373        assert_eq!(names[10].1, "gid10");
374        assert_eq!(names[10].1.source(), GlyphNameSource::Synthesized);
375        assert_eq!(
376            font.glyph_name(GlyphId::new(9)).unwrap().source(),
377            GlyphNameSource::Post
378        );
379        assert_eq!(
380            font.glyph_name(GlyphId::new(10)).unwrap().source(),
381            GlyphNameSource::Synthesized
382        );
383    }
384
385    // A post entry that cannot be read costs that glyph its name, not the
386    // glyphs after it.
387    #[test]
388    fn post_names_survive_a_bad_entry() {
389        let long = "a".repeat(70);
390        let mut post = alloc::vec![0, 2, 0, 0];
391        post.extend_from_slice(&[0; 28]);
392        // Glyph 2 names string 5, which the table does not have.
393        post.extend_from_slice(&4u16.to_be_bytes());
394        for index in [0u16, 258, 258 + 5, 258 + 1] {
395            post.extend_from_slice(&index.to_be_bytes());
396        }
397        for name in ["ok", long.as_str()] {
398            post.push(name.len() as u8);
399            post.extend_from_slice(name.as_bytes());
400        }
401        let source: Arc<dyn Fn(Tag) -> Option<Blob> + Send + Sync> = Arc::new(move |tag| {
402            if tag == Tag::new(b"post") {
403                return Some(Blob::from(post.clone()));
404            }
405            let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).ok()?;
406            font.table_data(tag)
407                .map(|data| Blob::from(data.as_bytes().to_vec()))
408        });
409        let font = Font::new(source, 0).unwrap();
410        let names: Vec<_> = font.glyph_names().collect();
411        assert_eq!(names[1].1, "ok");
412        assert_eq!(names[2].1, "gid2");
413        assert_eq!(names[3].1.as_str(), "a".repeat(63));
414        assert_eq!(names[3].1.source(), GlyphNameSource::Post);
415        for (glyph, name) in names {
416            let looked_up = font.glyph_name(glyph).unwrap();
417            assert_eq!(name.as_str(), looked_up.as_str(), "glyph {glyph}");
418            assert_eq!(name.source(), looked_up.source());
419        }
420    }
421
422    // CFF and Type 1 names may hold any UTF-8, so the cut at the length
423    // limit falls where a character ends.
424    #[test]
425    fn a_long_name_is_cut_between_characters() {
426        let name = GlyphName::from_bytes("é".repeat(35).as_bytes(), GlyphNameSource::Cff).unwrap();
427        assert_eq!(name.as_str(), "é".repeat(31));
428    }
429
430    // Every stated name finds the first glyph that has it, and a synthesized
431    // name finds nothing.
432    #[test]
433    fn glyph_by_name_finds_the_first_glyph_with_each_name() {
434        let sfnt = FontRef::new(font_test_data::NOTO_SANS_JP_CFF).unwrap();
435        let cid_cff = sfnt.cff().unwrap().offset_data().as_bytes();
436        let fonts = [
437            Font::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP, 0).unwrap(),
438            Font::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED, 0).unwrap(),
439            Font::new(font_test_data::type1::NOTO_SERIF_REGULAR_SUBSET_PFA, 0).unwrap(),
440            Font::new(cid_cff, 0).unwrap(),
441        ];
442        for font in &fonts {
443            let names: Vec<_> = font.glyph_names().collect();
444            for (glyph, name) in &names {
445                let first = names
446                    .iter()
447                    .find(|(_, other)| !other.is_synthesized() && other.as_str() == name.as_str())
448                    .map(|(glyph, _)| *glyph);
449                assert_eq!(font.glyph_by_name(name.as_str()), first, "glyph {glyph}");
450            }
451            assert_eq!(font.glyph_by_name("no.such.glyph"), None);
452            assert_eq!(font.glyph_by_name(""), None);
453        }
454        assert_eq!(fonts[0].glyph_by_name("A"), Some(GlyphId::new(2)));
455        assert_eq!(fonts[3].glyph_by_name("gid1"), None);
456    }
457
458    #[test]
459    fn cff_names_agree_between_lookup_and_iteration() {
460        let font = Font::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED, 0).unwrap();
461        let names: Vec<_> = font.glyph_names().collect();
462        for (gid, expected) in [".notdef", "i", "j", "k", "l"].iter().enumerate() {
463            let (glyph, name) = &names[gid];
464            assert_eq!(*glyph, GlyphId::new(gid as u32));
465            assert_eq!(name, expected);
466            assert_eq!(name.source(), GlyphNameSource::Cff);
467            assert_eq!(font.glyph_name(*glyph).unwrap().as_str(), *expected);
468        }
469    }
470
471    #[test]
472    fn standalone_cid_cff_synthesizes_gid_names() {
473        let sfnt = FontRef::new(font_test_data::NOTO_SANS_JP_CFF).unwrap();
474        let cff = sfnt.cff().unwrap().offset_data().as_bytes();
475        let font = Font::new(cff, 0).unwrap();
476        let Kind::Cff(raw, _) = font.kind() else {
477            panic!("expected standalone CFF font");
478        };
479        assert!(raw.is_cid());
480        let names: Vec<_> = font.glyph_names().collect();
481        assert_eq!(names.len(), font.num_glyphs() as usize);
482        for (glyph, name) in names {
483            assert_eq!(name.as_str(), format!("gid{}", glyph.to_u32()));
484            assert!(name.is_synthesized());
485            assert_eq!(font.glyph_name(glyph).unwrap().as_str(), name.as_str());
486        }
487        assert!(font.glyph_name(GlyphId::new(font.num_glyphs())).is_none());
488    }
489
490    #[test]
491    fn sfnt_cid_cff_without_post_names_synthesizes_gid_names() {
492        let font = Font::new(font_test_data::NOTO_SANS_JP_CFF, 0).unwrap();
493        assert!(font.cff().unwrap().is_cid());
494        let names: Vec<_> = font.glyph_names().take(3).collect();
495        for (glyph, name) in names {
496            assert_eq!(name.as_str(), format!("gid{}", glyph.to_u32()));
497            assert!(name.is_synthesized());
498        }
499    }
500
501    #[test]
502    fn type1_names_agree_between_lookup_and_iteration() {
503        let font = Font::new(font_test_data::type1::NOTO_SERIF_REGULAR_SUBSET_PFA, 0).unwrap();
504        let Kind::Type1(type1) = font.kind() else {
505            panic!("expected Type 1 font");
506        };
507        let names: Vec<_> = font.glyph_names().collect();
508        assert_eq!(names.len(), font.num_glyphs() as usize);
509        for (glyph, name) in names {
510            assert_eq!(name.as_str(), type1.glyph_name(glyph).unwrap());
511            assert_eq!(name.source(), GlyphNameSource::Type1);
512            assert_eq!(font.glyph_name(glyph).unwrap().as_str(), name.as_str());
513        }
514    }
515
516    // A name past the inline limit is found by its full spelling, and two
517    // names that share the truncated prefix stay distinct.
518    #[test]
519    fn long_names_are_found_in_full() {
520        let prefix = "a".repeat(63);
521        let (x, y) = (prefix.clone() + "xxxxxxx", prefix.clone() + "yyyyyyy");
522        let mut post = alloc::vec![0, 2, 0, 0];
523        post.extend_from_slice(&[0; 28]);
524        post.extend_from_slice(&3u16.to_be_bytes());
525        for index in [0u16, 258, 259] {
526            post.extend_from_slice(&index.to_be_bytes());
527        }
528        for name in [&x, &y] {
529            post.push(name.len() as u8);
530            post.extend_from_slice(name.as_bytes());
531        }
532        let source: Arc<dyn Fn(Tag) -> Option<Blob> + Send + Sync> = Arc::new(move |tag| {
533            if tag == Tag::new(b"post") {
534                return Some(Blob::from(post.clone()));
535            }
536            let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).ok()?;
537            font.table_data(tag)
538                .map(|data| Blob::from(data.as_bytes().to_vec()))
539        });
540        let font = Font::new(source, 0).unwrap();
541        assert_eq!(font.glyph_by_name(&x), Some(GlyphId::new(1)));
542        assert_eq!(font.glyph_by_name(&y), Some(GlyphId::new(2)));
543        assert_eq!(font.glyph_by_name(&prefix), None);
544        assert_eq!(font.glyph_name(GlyphId::new(1)).unwrap(), prefix.as_str());
545    }
546
547    #[test]
548    fn missing_name_tables_synthesize_each_name() {
549        let source: Arc<dyn Fn(Tag) -> Option<Blob> + Send + Sync> = Arc::new(|tag| {
550            if tag == Tag::new(b"post") || tag == Tag::new(b"CFF ") {
551                return None;
552            }
553            let font = FontRef::new(font_test_data::TINOS_SUBSET).ok()?;
554            font.table_data(tag)
555                .map(|data| Blob::from(data.as_bytes().to_vec()))
556        });
557        let font = Font::new(source, 0).unwrap();
558        let names: Vec<_> = font.glyph_names().collect();
559        assert_eq!(names.len(), font.num_glyphs() as usize);
560        for (glyph, name) in names {
561            assert_eq!(name.as_str(), format!("gid{}", glyph.to_u32()));
562            assert!(name.is_synthesized());
563            assert_eq!(font.glyph_name(glyph).unwrap().source(), name.source());
564        }
565    }
566}