oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

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