1package glyph
2
3import (
4 "bytes"
5 "encoding/binary"
6 "fmt"
7 "slices"
8
9 "golang.org/x/image/font/sfnt"
10 "golang.org/x/sys/unix"
11)
12
13// Names maps the glyph names stored in a font file to the runes that draw
14// them, such as "fa-steam" to U+F1B6 in a Nerd Font. Only the private use
15// areas are indexed: that is where icon fonts put their glyphs.
16//
17// The names stay in the mapped font file; a lookup does not allocate.
18type Names struct {
19 data []byte
20 idx []nameEntry // sorted by name
21}
22
23type nameEntry struct {
24 off, n uint32 // the name is data[off:off+n]
25 r rune
26}
27
28// iconRanges are the private use areas: the BMP one and plane 15.
29var iconRanges = [...][2]rune{{0xE000, 0xF8FF}, {0xF0000, 0xFFFFD}}
30
31// LoadNames indexes the glyph names of the font file at path. A font without
32// glyph names (a version 3 "post" table) gives an empty index, not an error.
33func LoadNames(path string) (*Names, error) {
34 data, err := mapFile(path)
35 if err != nil {
36 return nil, fmt.Errorf("glyph: %w", err)
37 }
38 n := &Names{data: data}
39 f, err := sfnt.Parse(data)
40 if err != nil {
41 n.Close()
42 return nil, fmt.Errorf("glyph: parse %s: %w", path, err)
43 }
44 spans := postNames(data)
45 var buf sfnt.Buffer
46 for _, rg := range iconRanges {
47 for r := rg[0]; r <= rg[1]; r++ {
48 gi, err := f.GlyphIndex(&buf, r)
49 if err != nil || gi == 0 || int(gi) >= len(spans) {
50 continue
51 }
52 if s := spans[gi]; s.n > 0 {
53 n.idx = append(n.idx, nameEntry{off: s.off, n: s.n, r: r})
54 }
55 }
56 }
57 slices.SortFunc(n.idx, func(a, b nameEntry) int {
58 return bytes.Compare(n.data[a.off:a.off+a.n], n.data[b.off:b.off+b.n])
59 })
60 return n, nil
61}
62
63// Lookup returns the rune of the glyph called name.
64func (n *Names) Lookup(name string) (rune, bool) {
65 if n == nil {
66 return 0, false
67 }
68 lo, hi := 0, len(n.idx)
69 for lo < hi {
70 m := int(uint(lo+hi) >> 1)
71 e := n.idx[m]
72 // string(bytes) in a comparison does not allocate.
73 switch s := n.data[e.off : e.off+e.n]; {
74 case string(s) < name:
75 lo = m + 1
76 case string(s) > name:
77 hi = m
78 default:
79 return e.r, true
80 }
81 }
82 return 0, false
83}
84
85// Len returns the number of indexed names.
86func (n *Names) Len() int { return len(n.idx) }
87
88// Close unmaps the font file. n must not be used afterwards.
89func (n *Names) Close() {
90 if n != nil && n.data != nil {
91 _ = unix.Munmap(n.data)
92 n.data, n.idx = nil, nil
93 }
94}
95
96type span struct{ off, n uint32 }
97
98// postNames returns, for each glyph index, where its custom name sits in
99// data, from a version 2 "post" table. Glyphs with a standard Macintosh name
100// (index < 258) or no table at all give empty spans. Every offset is checked
101// against the file, so a damaged font gives fewer names, never a panic.
102func postNames(data []byte) []span {
103 u16 := func(off int) (int, bool) {
104 if off < 0 || off+2 > len(data) {
105 return 0, false
106 }
107 return int(binary.BigEndian.Uint16(data[off:])), true
108 }
109 u32 := func(off int) (uint32, bool) {
110 if off < 0 || off+4 > len(data) {
111 return 0, false
112 }
113 return binary.BigEndian.Uint32(data[off:]), true
114 }
115 tables, ok := u16(4)
116 if !ok {
117 return nil
118 }
119 post, end := -1, 0
120 for i := range tables {
121 rec := 12 + 16*i
122 if rec+16 > len(data) {
123 return nil
124 }
125 if string(data[rec:rec+4]) == "post" {
126 off, _ := u32(rec + 8)
127 n, _ := u32(rec + 12)
128 if uint64(off)+uint64(n) > uint64(len(data)) {
129 return nil
130 }
131 post, end = int(off), int(off)+int(n)
132 break
133 }
134 }
135 if v, ok := u32(post); !ok || v != 0x00020000 {
136 return nil
137 }
138 glyphs, ok := u16(post + 32)
139 if !ok || post+34+2*glyphs > end {
140 return nil
141 }
142 // The custom names follow the index array as Pascal strings.
143 var custom []uint32
144 for o := post + 34 + 2*glyphs; o < end; o += 1 + int(data[o]) {
145 if o+1+int(data[o]) > end {
146 break
147 }
148 custom = append(custom, uint32(o))
149 }
150 spans := make([]span, glyphs)
151 for g := range glyphs {
152 k, _ := u16(post + 34 + 2*g)
153 if k < 258 || k-258 >= len(custom) {
154 continue
155 }
156 o := custom[k-258]
157 spans[g] = span{off: o + 1, n: uint32(data[o])}
158 }
159 return spans
160}