1package text
2
3import (
4 "crypto/sha256"
5 "encoding/binary"
6 "errors"
7 "image"
8 "image/draw"
9 "math"
10
11 "github.com/go-text/typesetting/font"
12 "golang.org/x/image/font/sfnt"
13 "golang.org/x/image/math/fixed"
14 "golang.org/x/image/vector"
15)
16
17// Key identifies a grayscale glyph mask. Size is physical px in 26.6 units;
18// Phase is one of four horizontal quarter-pixel origins (vertical phase is zero).
19type Key struct {
20 FaceID string
21 Glyph font.GID
22 Size fixed.Int26_6
23 Variations [32]byte
24 Phase uint8
25}
26type Mask struct {
27 Alpha *image.Alpha
28 Origin image.Point
29}
30
31// VariationHash hashes the axis coordinates without allocating for up to 16
32// axes (fonts rarely have more).
33func VariationHash(v []font.Variation) [32]byte {
34 var buf [16 * 8]byte
35 b := buf[:0]
36 if len(v) > 16 {
37 b = make([]byte, 0, len(v)*8)
38 }
39 for _, coord := range v {
40 b = binary.BigEndian.AppendUint32(b, uint32(coord.Tag))
41 b = binary.BigEndian.AppendUint32(b, math.Float32bits(coord.Value))
42 }
43 return sha256.Sum256(b)
44}
45func GlyphKey(face *Face, id font.GID, physicalSize, originX float64, variations []font.Variation) (Key, error) {
46 if face == nil || VariationHash(variations) != VariationHash(face.Variations) || physicalSize <= 0 || physicalSize > 4096 || math.IsNaN(physicalSize) || math.IsInf(physicalSize, 0) || math.IsNaN(originX) || math.IsInf(originX, 0) {
47 return Key{}, errors.New("invalid glyph key")
48 }
49 frac := originX - math.Floor(originX)
50 return Key{FaceID: face.ID, Glyph: id, Size: fixed.Int26_6(math.Round(physicalSize * 64)), Phase: uint8(math.Floor(frac * 4)), Variations: VariationHash(variations)}, nil
51}
52
53// Rasterize uses sfnt for static faces and go-text outlines for variable faces.
54// Neither path hints glyphs. Masks have one pixel of transparent padding for filtering.
55func Rasterize(face *Face, key Key) (Mask, error) {
56 if face == nil || key.FaceID != face.ID || key.Size <= 0 || key.Size > 4096*64 || key.Phase > 3 {
57 return Mask{}, errors.New("invalid raster request")
58 }
59 if VariationHash(face.Variations) != key.Variations {
60 return Mask{}, errors.New("variation key does not match face")
61 }
62 if key.Glyph == font.EmptyGlyph {
63 return Mask{Alpha: image.NewAlpha(image.Rect(0, 0, 0, 0))}, nil
64 }
65 var segments []outlineSegment
66 if face.Variable {
67 outline, ok := face.Shape.GlyphData(key.Glyph).(font.GlyphOutline)
68 if !ok {
69 return Mask{}, errors.New("outline unavailable")
70 }
71 scale := float64(key.Size) / 64 / float64(face.Shape.Upem())
72 for _, s := range outline.Segments {
73 seg := outlineSegment{op: int(s.Op)}
74 for i, p := range s.ArgsSlice() {
75 seg.points[i] = point{float64(p.X) * scale, -float64(p.Y) * scale}
76 }
77 segments = append(segments, seg)
78 }
79 } else {
80 if key.Glyph > 65535 {
81 return Mask{}, errors.New("glyph id out of sfnt range")
82 }
83 var buf sfnt.Buffer
84 segs, err := face.Raster.LoadGlyph(&buf, sfnt.GlyphIndex(key.Glyph), key.Size, nil)
85 if err != nil {
86 return Mask{}, err
87 }
88 for _, s := range segs {
89 seg := outlineSegment{op: int(s.Op)}
90 n := 1
91 if s.Op == sfnt.SegmentOpQuadTo {
92 n = 2
93 } else if s.Op == sfnt.SegmentOpCubeTo {
94 n = 3
95 }
96 for i, p := range s.Args[:n] {
97 seg.points[i] = point{float64(p.X) / 64, float64(p.Y) / 64}
98 }
99 segments = append(segments, seg)
100 }
101 }
102 if len(segments) == 0 {
103 return Mask{Alpha: image.NewAlpha(image.Rect(0, 0, 0, 0))}, nil
104 }
105 phase := float64(key.Phase) / 4
106 loX, loY := math.Inf(1), math.Inf(1)
107 hiX, hiY := math.Inf(-1), math.Inf(-1)
108 for _, s := range segments {
109 n := 1
110 if s.op == 2 {
111 n = 2
112 }
113 if s.op == 3 {
114 n = 3
115 }
116 for _, p := range s.points[:n] {
117 loX = math.Min(loX, p.x+phase)
118 hiX = math.Max(hiX, p.x+phase)
119 loY = math.Min(loY, p.y)
120 hiY = math.Max(hiY, p.y)
121 }
122 }
123 x0, y0 := int(math.Floor(loX))-1, int(math.Floor(loY))-1
124 x1, y1 := int(math.Ceil(hiX))+1, int(math.Ceil(hiY))+1
125 if x1-x0 > 8192 || y1-y0 > 8192 || x1 <= x0 || y1 <= y0 {
126 return Mask{}, errors.New("invalid glyph bounds")
127 }
128 dst := image.NewAlpha(image.Rect(0, 0, x1-x0, y1-y0))
129 v := vector.NewRasterizer(dst.Rect.Dx(), dst.Rect.Dy())
130 v.DrawOp = draw.Src
131 for _, s := range segments {
132 p := s.points
133 a := func(i int) (float32, float32) {
134 return float32(p[i].x + phase - float64(x0)), float32(p[i].y - float64(y0))
135 }
136 switch s.op {
137 case 0:
138 x, y := a(0)
139 v.MoveTo(x, y)
140 case 1:
141 x, y := a(0)
142 v.LineTo(x, y)
143 case 2:
144 x, y := a(0)
145 xx, yy := a(1)
146 v.QuadTo(x, y, xx, yy)
147 case 3:
148 x, y := a(0)
149 xx, yy := a(1)
150 xxx, yyy := a(2)
151 v.CubeTo(x, y, xx, yy, xxx, yyy)
152 }
153 }
154 v.Draw(dst, dst.Bounds(), image.Opaque, image.Point{})
155 return Mask{Alpha: dst, Origin: image.Pt(x0, y0)}, nil
156}
157
158type point struct{ x, y float64 }
159type outlineSegment struct {
160 op int
161 points [3]point
162}