oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1package fonts
2
3import (
4 "encoding/binary"
5 "errors"
6 "fmt"
7 "sort"
8 "sync"
9)
10
11// A minimal TrueType reader and subsetter — enough to measure text and
12// to embed a face in a PDF, and deliberately not one byte more.
13//
14// It reads the six tables a CIDFontType2 embedding actually requires
15// (head, hhea, maxp, hmtx, loca, glyf) plus cmap to find glyphs by
16// rune, and post/OS2 for the two descriptor fields a PDF reader wants.
17// It does not read GPOS, so it does not kern; text measured here is
18// the sum of advance widths, which is what an unkerned browser layout
19// of the same string also is. It does not read GSUB, so no ligatures
20// and no complex-script shaping — a document in a script that needs
21// shaping lays out wrongly here, which is the same limitation §11.1
22// already records for the scripts outside the core set.
23//
24// # Why subsetting keeps the original glyph ids
25//
26// The usual subsetter renumbers glyphs densely and rewrites every
27// reference to them. This one keeps every glyph id exactly where it
28// was and simply empties the glyphs nothing uses: `loca` still has
29// numGlyphs+1 entries, but an unused glyph's two entries are equal, so
30// it occupies no bytes in `glyf`.
31//
32// That is a real subset — a document using eighty glyphs of Liberation
33// Serif embeds about 25KB rather than 385KB — and it removes the two
34// places renumbering goes wrong: composite glyphs referring to their
35// components by id (no remapping needed, because ids do not move), and
36// the PDF's CIDToGIDMap (Identity, because a CID *is* the glyph id).
37// What it costs is `hmtx` and `loca`, which stay proportional to the
38// font's full glyph count rather than the subset's. For a 2,500-glyph
39// face that is about 15KB — worth paying to not have a subsetter whose
40// failure mode is a document that renders with the wrong letters.
41
42var (
43 errShortTable = errors.New("fonts: truetype table is shorter than its own header claims")
44 errNoTable = errors.New("fonts: truetype table missing")
45)
46
47// Font is a parsed face.
48type Font struct {
49 data []byte
50 tables map[string][]byte
51
52 UnitsPerEm uint16
53 NumGlyphs uint16
54
55 // Ascender, Descender and LineGap are hhea's, in font units.
56 // Descender is negative, as the table stores it.
57 Ascender int16
58 Descender int16
59 LineGap int16
60
61 // CapHeight and ItalicAngle are for the PDF font descriptor. Both
62 // have defaults that are wrong-but-harmless if the source tables
63 // are absent (a reader uses them for substitution decisions it
64 // will never have to make, because the font is embedded).
65 CapHeight int16
66 ItalicAngle float64
67 BBox [4]int16
68
69 indexToLocFormat int16
70 numberOfHMetrics uint16
71
72 advances []uint16 // one per glyph id, font units
73 cmap map[rune]uint16
74}
75
76func be16(b []byte, off int) (uint16, bool) {
77 if off < 0 || off+2 > len(b) {
78 return 0, false
79 }
80 return binary.BigEndian.Uint16(b[off:]), true
81}
82
83func be32(b []byte, off int) (uint32, bool) {
84 if off < 0 || off+4 > len(b) {
85 return 0, false
86 }
87 return binary.BigEndian.Uint32(b[off:]), true
88}
89
90// Parse reads the tables this package needs out of a TrueType file.
91//
92// Every read is bounds-checked even though the only fonts reaching it
93// are the twelve embedded in this binary: a parser that is careful
94// only about untrusted input is a parser nobody may ever point at
95// untrusted input, and this one is one `//go:embed` away from being
96// handed a font a person uploaded.
97func Parse(data []byte) (*Font, error) {
98 if len(data) < 12 {
99 return nil, errShortTable
100 }
101 switch binary.BigEndian.Uint32(data) {
102 case 0x00010000, 0x74727565: // 'true'
103 default:
104 return nil, fmt.Errorf("fonts: not a TrueType outline font (sfnt version %#x)", binary.BigEndian.Uint32(data))
105 }
106 numTables := int(binary.BigEndian.Uint16(data[4:]))
107 tables := make(map[string][]byte, numTables)
108 for i := 0; i < numTables; i++ {
109 rec := 12 + i*16
110 if rec+16 > len(data) {
111 return nil, errShortTable
112 }
113 tag := string(data[rec : rec+4])
114 off := int(binary.BigEndian.Uint32(data[rec+8:]))
115 length := int(binary.BigEndian.Uint32(data[rec+12:]))
116 if off < 0 || length < 0 || off+length > len(data) {
117 return nil, fmt.Errorf("fonts: table %q runs past the end of the file", tag)
118 }
119 tables[tag] = data[off : off+length]
120 }
121
122 f := &Font{data: data, tables: tables}
123
124 head, ok := tables["head"]
125 if !ok || len(head) < 54 {
126 return nil, fmt.Errorf("%w: head", errNoTable)
127 }
128 f.UnitsPerEm = binary.BigEndian.Uint16(head[18:])
129 if f.UnitsPerEm == 0 {
130 return nil, errors.New("fonts: head.unitsPerEm is zero")
131 }
132 for i := 0; i < 4; i++ {
133 f.BBox[i] = int16(binary.BigEndian.Uint16(head[36+i*2:]))
134 }
135 f.indexToLocFormat = int16(binary.BigEndian.Uint16(head[50:]))
136
137 hhea, ok := tables["hhea"]
138 if !ok || len(hhea) < 36 {
139 return nil, fmt.Errorf("%w: hhea", errNoTable)
140 }
141 f.Ascender = int16(binary.BigEndian.Uint16(hhea[4:]))
142 f.Descender = int16(binary.BigEndian.Uint16(hhea[6:]))
143 f.LineGap = int16(binary.BigEndian.Uint16(hhea[8:]))
144 f.numberOfHMetrics = binary.BigEndian.Uint16(hhea[34:])
145
146 maxp, ok := tables["maxp"]
147 if !ok || len(maxp) < 6 {
148 return nil, fmt.Errorf("%w: maxp", errNoTable)
149 }
150 f.NumGlyphs = binary.BigEndian.Uint16(maxp[4:])
151
152 if os2, ok := tables["OS/2"]; ok && len(os2) >= 90 {
153 f.CapHeight = int16(binary.BigEndian.Uint16(os2[88:]))
154 }
155 if f.CapHeight == 0 {
156 // sCapHeight is only present from OS/2 version 2. The ascender
157 // overstates cap height slightly; a PDF reader uses this for
158 // substitution metrics it never needs for an embedded font.
159 f.CapHeight = f.Ascender
160 }
161 if post, ok := tables["post"]; ok && len(post) >= 12 {
162 // Fixed 16.16 signed.
163 f.ItalicAngle = float64(int32(binary.BigEndian.Uint32(post[4:]))) / 65536.0
164 }
165
166 if err := f.parseHmtx(); err != nil {
167 return nil, err
168 }
169 if err := f.parseCmap(); err != nil {
170 return nil, err
171 }
172 return f, nil
173}
174
175func (f *Font) parseHmtx() error {
176 hmtx, ok := f.tables["hmtx"]
177 if !ok {
178 return fmt.Errorf("%w: hmtx", errNoTable)
179 }
180 n := int(f.numberOfHMetrics)
181 if n == 0 || n > int(f.NumGlyphs) {
182 return errors.New("fonts: hhea.numberOfHMetrics disagrees with maxp.numGlyphs")
183 }
184 f.advances = make([]uint16, f.NumGlyphs)
185 var last uint16
186 for i := 0; i < int(f.NumGlyphs); i++ {
187 if i < n {
188 v, ok := be16(hmtx, i*4)
189 if !ok {
190 return errShortTable
191 }
192 last = v
193 }
194 // Past numberOfHMetrics every glyph repeats the last advance —
195 // that is what the table's compressed tail means, not "no
196 // advance".
197 f.advances[i] = last
198 }
199 return nil
200}
201
202// parseCmap picks the best Unicode subtable it understands and expands
203// it into a map. Expanding eagerly rather than searching per lookup:
204// a face has a few thousand mapped runes, a document has thousands of
205// characters, and the map is built once per face per process.
206func (f *Font) parseCmap() error {
207 cm, ok := f.tables["cmap"]
208 if !ok || len(cm) < 4 {
209 // Not an error. A font whose glyphs are addressed by id rather
210 // than by rune needs no cmap and is required to carry none —
211 // which is exactly what Subset produces, since a PDF
212 // CIDFontType2 under Identity-H looks glyphs up by id. Parse
213 // has to be able to read its own output back, which is how the
214 // subsetter is tested at all.
215 //
216 // The risk this accepts is a *primary* face arriving without a
217 // usable cmap and measuring every character as .notdef in
218 // silence. That is caught where it can actually be judged:
219 // fonts_test.go's TestEveryFaceParses asserts each of the
220 // twelve embedded faces resolves ordinary prose to real glyph
221 // ids, which a cmap-less face cannot do.
222 f.cmap = map[rune]uint16{}
223 return nil
224 }
225 n := int(binary.BigEndian.Uint16(cm[2:]))
226
227 // Preference order: a full-repertoire subtable first, then a BMP
228 // one. (3,10) and (0,4)/(0,6) are format 12; (3,1) and (0,3) are
229 // format 4.
230 best, bestScore := -1, -1
231 for i := 0; i < n; i++ {
232 rec := 4 + i*8
233 plat, ok1 := be16(cm, rec)
234 enc, ok2 := be16(cm, rec+2)
235 off, ok3 := be32(cm, rec+4)
236 if !ok1 || !ok2 || !ok3 || int(off) >= len(cm) {
237 continue
238 }
239 score := -1
240 switch {
241 case plat == 3 && enc == 10:
242 score = 4
243 case plat == 0 && (enc == 4 || enc == 6):
244 score = 3
245 case plat == 3 && enc == 1:
246 score = 2
247 case plat == 0:
248 score = 1
249 }
250 if score > bestScore {
251 best, bestScore = int(off), score
252 }
253 }
254 if best < 0 {
255 return errors.New("fonts: cmap has no Unicode subtable this build understands")
256 }
257 sub := cm[best:]
258 format, ok := be16(sub, 0)
259 if !ok {
260 return errShortTable
261 }
262 f.cmap = make(map[rune]uint16, 1024)
263 switch format {
264 case 4:
265 return f.parseCmap4(sub)
266 case 12:
267 return f.parseCmap12(sub)
268 default:
269 return fmt.Errorf("fonts: cmap subtable format %d is not supported", format)
270 }
271}
272
273func (f *Font) parseCmap4(sub []byte) error {
274 segX2, ok := be16(sub, 6)
275 if !ok || segX2%2 != 0 {
276 return errShortTable
277 }
278 seg := int(segX2 / 2)
279 endBase := 14
280 startBase := endBase + seg*2 + 2
281 deltaBase := startBase + seg*2
282 rangeBase := deltaBase + seg*2
283 if rangeBase+seg*2 > len(sub) {
284 return errShortTable
285 }
286 for i := 0; i < seg; i++ {
287 end := binary.BigEndian.Uint16(sub[endBase+i*2:])
288 start := binary.BigEndian.Uint16(sub[startBase+i*2:])
289 delta := binary.BigEndian.Uint16(sub[deltaBase+i*2:])
290 ro := binary.BigEndian.Uint16(sub[rangeBase+i*2:])
291 if start > end {
292 continue
293 }
294 for c := uint32(start); c <= uint32(end); c++ {
295 if c == 0xFFFF {
296 continue // the required terminating segment, not a character
297 }
298 var gid uint16
299 if ro == 0 {
300 gid = uint16(c) + delta
301 } else {
302 // The idRangeOffset trick: the offset is measured from
303 // the address of the idRangeOffset entry itself.
304 at := rangeBase + i*2 + int(ro) + int(c-uint32(start))*2
305 g, ok := be16(sub, at)
306 if !ok {
307 continue
308 }
309 if g == 0 {
310 continue
311 }
312 gid = g + delta
313 }
314 if gid != 0 && gid < f.NumGlyphs {
315 f.cmap[rune(c)] = gid
316 }
317 }
318 }
319 return nil
320}
321
322func (f *Font) parseCmap12(sub []byte) error {
323 nGroups, ok := be32(sub, 12)
324 if !ok {
325 return errShortTable
326 }
327 // A group table that does not fit is the end of the table, not a
328 // row to skip: nGroups is a number from the file, and a hostile one
329 // of four billion against a short table would otherwise spin four
330 // billion no-op iterations. Bounded by the bytes actually present
331 // rather than by the count the file claims.
332 if max := (len(sub) - 16) / 12; max >= 0 && nGroups > uint32(max) {
333 nGroups = uint32(max)
334 }
335 for i := uint32(0); i < nGroups; i++ {
336 at := 16 + int(i)*12
337 start, ok1 := be32(sub, at)
338 end, ok2 := be32(sub, at+4)
339 gid, ok3 := be32(sub, at+8)
340 if !ok1 || !ok2 || !ok3 {
341 break // the table ended mid-group; nothing after it is readable
342 }
343 if start > end {
344 continue
345 }
346 // A pathological group spanning the whole of Unicode would
347 // otherwise build a map with a million entries.
348 if end-start > 0x10FFFF {
349 continue
350 }
351 for c := start; c <= end; c++ {
352 g := gid + (c - start)
353 if g != 0 && g < uint32(f.NumGlyphs) {
354 f.cmap[rune(c)] = uint16(g)
355 }
356 }
357 }
358 return nil
359}
360
361// glyphData returns the bytes of one glyph's outline, or nil for a
362// glyph with none (a space, or one this subsetter emptied).
363func (f *Font) glyphData(gid uint16) []byte {
364 loca, err := f.locaOffsets()
365 if err != nil {
366 return nil
367 }
368 glyf, ok := f.tables["glyf"]
369 if !ok || int(gid)+1 >= len(loca) {
370 return nil
371 }
372 start, end := loca[gid], loca[gid+1]
373 if start >= end || int(end) > len(glyf) {
374 return nil
375 }
376 return glyf[start:end]
377}
378
379// GlyphLen is how many bytes glyph gid occupies — zero for a glyph
380// with no outline, which is what a subsetted-away glyph looks like.
381func (f *Font) GlyphLen(gid uint16) int { return len(f.glyphData(gid)) }
382
383// GlyphIsComposite reports whether gid is assembled from other glyphs.
384func (f *Font) GlyphIsComposite(gid uint16) bool {
385 g := f.glyphData(gid)
386 return len(g) >= 2 && int16(binary.BigEndian.Uint16(g)) < 0
387}
388
389// GlyphIndex maps a rune to a glyph id, or 0 (.notdef) if the face has
390// no glyph for it. Callers decide what a 0 means: internal/pdf reports
391// it as a substitution in the export rather than silently drawing an
392// empty box, which is §11.2's degrade-loudly rule.
393func (f *Font) GlyphIndex(r rune) uint16 { return f.cmap[r] }
394
395// Advance is the glyph's advance width in font units.
396func (f *Font) Advance(gid uint16) uint16 {
397 if int(gid) >= len(f.advances) {
398 return 0
399 }
400 return f.advances[gid]
401}
402
403// WidthEm is the advance of a run of text as a multiple of the em, so
404// a caller multiplies by a point size and gets points. Returns the
405// glyph ids it used, so the caller can accumulate a subset without
406// walking the string twice.
407func (f *Font) WidthEm(s string) (width float64, gids []uint16) {
408 upm := float64(f.UnitsPerEm)
409 for _, r := range s {
410 gid := f.GlyphIndex(r)
411 gids = append(gids, gid)
412 width += float64(f.Advance(gid)) / upm
413 }
414 return width, gids
415}
416
417// locaOffsets reads loca into a plain slice of byte offsets into glyf.
418func (f *Font) locaOffsets() ([]uint32, error) {
419 loca, ok := f.tables["loca"]
420 if !ok {
421 return nil, fmt.Errorf("%w: loca", errNoTable)
422 }
423 n := int(f.NumGlyphs) + 1
424 out := make([]uint32, n)
425 if f.indexToLocFormat == 0 {
426 if len(loca) < n*2 {
427 return nil, errShortTable
428 }
429 for i := 0; i < n; i++ {
430 // The short format stores offset/2.
431 out[i] = uint32(binary.BigEndian.Uint16(loca[i*2:])) * 2
432 }
433 return out, nil
434 }
435 if len(loca) < n*4 {
436 return nil, errShortTable
437 }
438 for i := 0; i < n; i++ {
439 out[i] = binary.BigEndian.Uint32(loca[i*4:])
440 }
441 return out, nil
442}
443
444// componentsOf returns the glyph ids a composite glyph is built from.
445// A subset that keeps "é" but drops the "e" and the acute it is
446// assembled from renders as nothing at all, which is the failure this
447// exists to prevent.
448func componentsOf(glyph []byte) []uint16 {
449 if len(glyph) < 10 {
450 return nil
451 }
452 if int16(binary.BigEndian.Uint16(glyph)) >= 0 {
453 return nil // simple glyph, no components
454 }
455 var out []uint16
456 at := 10
457 for {
458 flags, ok1 := be16(glyph, at)
459 gid, ok2 := be16(glyph, at+2)
460 if !ok1 || !ok2 {
461 return out
462 }
463 out = append(out, gid)
464 at += 4
465 if flags&0x0001 != 0 { // ARG_1_AND_2_ARE_WORDS
466 at += 4
467 } else {
468 at += 2
469 }
470 switch {
471 case flags&0x0008 != 0: // WE_HAVE_A_SCALE
472 at += 2
473 case flags&0x0040 != 0: // WE_HAVE_AN_X_AND_Y_SCALE
474 at += 4
475 case flags&0x0080 != 0: // WE_HAVE_A_TWO_BY_TWO
476 at += 8
477 }
478 if flags&0x0020 == 0 { // MORE_COMPONENTS
479 return out
480 }
481 }
482}
483
484// Subset returns a TrueType file containing only the glyphs in keep
485// (plus .notdef and, transitively, the components of any composite
486// glyph kept). Glyph ids are unchanged — see this file's header for
487// why that is the whole design.
488func (f *Font) Subset(keep map[uint16]bool) ([]byte, error) {
489 loca, err := f.locaOffsets()
490 if err != nil {
491 return nil, err
492 }
493 glyf, ok := f.tables["glyf"]
494 if !ok {
495 return nil, fmt.Errorf("%w: glyf", errNoTable)
496 }
497
498 glyphAt := func(gid uint16) []byte {
499 if int(gid)+1 >= len(loca) {
500 return nil
501 }
502 start, end := loca[gid], loca[gid+1]
503 if start >= end || int(end) > len(glyf) {
504 return nil // an empty glyph, e.g. the space
505 }
506 return glyf[start:end]
507 }
508
509 // Close over composites. A worklist rather than recursion: the
510 // component graph in a hostile font can be cyclic, and `seen`
511 // terminates either way.
512 seen := map[uint16]bool{0: true} // .notdef always ships
513 work := []uint16{0}
514 for gid := range keep {
515 if !seen[gid] {
516 seen[gid] = true
517 work = append(work, gid)
518 }
519 }
520 for len(work) > 0 {
521 gid := work[len(work)-1]
522 work = work[:len(work)-1]
523 for _, c := range componentsOf(glyphAt(gid)) {
524 if !seen[c] && int(c) < int(f.NumGlyphs) {
525 seen[c] = true
526 work = append(work, c)
527 }
528 }
529 }
530
531 // Rebuild glyf and loca. Every glyph keeps its id; the ones not in
532 // `seen` get a zero-length entry.
533 var newGlyf []byte
534 newLoca := make([]uint32, int(f.NumGlyphs)+1)
535 for gid := 0; gid < int(f.NumGlyphs); gid++ {
536 newLoca[gid] = uint32(len(newGlyf))
537 if seen[uint16(gid)] {
538 g := glyphAt(uint16(gid))
539 newGlyf = append(newGlyf, g...)
540 for len(newGlyf)%4 != 0 { // glyphs are 4-byte aligned
541 newGlyf = append(newGlyf, 0)
542 }
543 }
544 }
545 newLoca[f.NumGlyphs] = uint32(len(newGlyf))
546
547 // Long loca throughout: the short format cannot express an offset
548 // above 131,070 and cannot express an odd one at all, and choosing
549 // per-font would mean two code paths where one is barely exercised.
550 locaBytes := make([]byte, len(newLoca)*4)
551 for i, off := range newLoca {
552 binary.BigEndian.PutUint32(locaBytes[i*4:], off)
553 }
554
555 head := append([]byte(nil), f.tables["head"]...)
556 binary.BigEndian.PutUint16(head[50:], 1) // indexToLocFormat: long
557 binary.BigEndian.PutUint32(head[8:], 0) // checkSumAdjustment, set below
558
559 out := map[string][]byte{
560 "head": head,
561 "hhea": f.tables["hhea"],
562 "maxp": f.tables["maxp"],
563 "hmtx": f.tables["hmtx"],
564 "loca": locaBytes,
565 "glyf": newGlyf,
566 }
567
568 // The hinting tables travel with the glyphs, when the font has
569 // them. ISO 32000-1 §9.9 requires `cvt `, `fpgm` and `prep` to be
570 // present when the glyph instructions need them, and Liberation's
571 // do: every glyph here carries bytecode that calls functions
572 // defined in `fpgm` and reads values out of `cvt `. Shipping the
573 // outlines without them leaves a reader executing instructions
574 // against definitions that are not in the file — most fall back to
575 // unhinted rendering and it merely looks softer, but a strict
576 // rasterizer or a printer RIP is entitled to do worse, and the
577 // file is not conformant either way.
578 //
579 // About 15KB a face, which is the cheapest correctness in this
580 // package. They are copied verbatim: they are indexed by nothing
581 // this subsetter renumbers, because glyph ids do not move.
582 for _, tag := range []string{"cvt ", "fpgm", "prep"} {
583 if b, ok := f.tables[tag]; ok && len(b) > 0 {
584 out[tag] = b
585 }
586 }
587 for tag, b := range out {
588 if b == nil {
589 return nil, fmt.Errorf("%w: %s", errNoTable, tag)
590 }
591 }
592 return buildSfnt(out), nil
593}
594
595// buildSfnt assembles a TrueType file from a tag→bytes map.
596func buildSfnt(tables map[string][]byte) []byte {
597 tags := make([]string, 0, len(tables))
598 for t := range tables {
599 tags = append(tags, t)
600 }
601 sort.Strings(tags) // the table directory must be in tag order
602
603 n := len(tags)
604 // searchRange/entrySelector/rangeShift are a binary-search hint a
605 // modern reader ignores but a strict validator checks.
606 entrySelector := 0
607 for 1<<(entrySelector+1) <= n {
608 entrySelector++
609 }
610 searchRange := 16 * (1 << entrySelector)
611 rangeShift := 16*n - searchRange
612
613 header := make([]byte, 12+16*n)
614 binary.BigEndian.PutUint32(header, 0x00010000)
615 binary.BigEndian.PutUint16(header[4:], uint16(n))
616 binary.BigEndian.PutUint16(header[6:], uint16(searchRange))
617 binary.BigEndian.PutUint16(header[8:], uint16(entrySelector))
618 binary.BigEndian.PutUint16(header[10:], uint16(rangeShift))
619
620 body := []byte{}
621 offset := len(header)
622 for i, tag := range tags {
623 b := tables[tag]
624 rec := 12 + i*16
625 copy(header[rec:], tag)
626 binary.BigEndian.PutUint32(header[rec+4:], tableChecksum(b))
627 binary.BigEndian.PutUint32(header[rec+8:], uint32(offset+len(body)))
628 binary.BigEndian.PutUint32(header[rec+12:], uint32(len(b)))
629 body = append(body, b...)
630 for len(body)%4 != 0 {
631 body = append(body, 0)
632 }
633 }
634
635 file := append(header, body...)
636
637 // head.checkSumAdjustment: 0xB1B0AFBA minus the checksum of the
638 // whole file computed with this field zeroed, which it is.
639 for i, tag := range tags {
640 if tag != "head" {
641 continue
642 }
643 headOff := int(binary.BigEndian.Uint32(file[12+i*16+8:]))
644 if headOff+12 <= len(file) {
645 adj := 0xB1B0AFBA - tableChecksum(file)
646 binary.BigEndian.PutUint32(file[headOff+8:], adj)
647 }
648 }
649 return file
650}
651
652func tableChecksum(b []byte) uint32 {
653 var sum uint32
654 for i := 0; i+4 <= len(b); i += 4 {
655 sum += binary.BigEndian.Uint32(b[i:])
656 }
657 // A table's length is padded to four bytes for checksum purposes
658 // even when the bytes themselves are not present.
659 if rem := len(b) % 4; rem != 0 {
660 var tail [4]byte
661 copy(tail[:], b[len(b)-rem:])
662 sum += binary.BigEndian.Uint32(tail[:])
663 }
664 return sum
665}
666
667// --- parsed-face cache ---
668
669var (
670 parsedMu sync.Mutex
671 parsed = map[string]*Font{}
672)
673
674// Parsed returns this face's parsed font, cached per process. Parsing
675// expands the cmap into a map of a few thousand entries, which is
676// cheap once and pointless twelve times a request.
677func (f Face) Parsed() (*Font, error) {
678 parsedMu.Lock()
679 defer parsedMu.Unlock()
680 if font, ok := parsed[f.File]; ok {
681 return font, nil
682 }
683 font, err := Parse(f.Data)
684 if err != nil {
685 return nil, fmt.Errorf("fonts: parse %s: %w", f.File, err)
686 }
687 parsed[f.File] = font
688 return font, nil
689}
690