1 package textmeasure
2
3 import (
4 "sort"
5 "unicode"
6
7 "golang.org/x/image/font"
8 "golang.org/x/image/math/fixed"
9
10 "oss.terrastruct.com/d2/lib/geo"
11 )
12
13
14 type glyph struct {
15 dot *geo.Point
16 frame *rect
17 advance float64
18 }
19
20
21 type atlas struct {
22 face font.Face
23 mapping map[rune]glyph
24 ascent float64
25 descent float64
26 lineHeight float64
27 }
28
29
30
31
32
33
34
35 func NewAtlas(face font.Face, runeSets ...[]rune) *atlas {
36 seen := make(map[rune]bool)
37 runes := []rune{unicode.ReplacementChar}
38 for _, set := range runeSets {
39 for _, r := range set {
40 if !seen[r] {
41 runes = append(runes, r)
42 seen[r] = true
43 }
44 }
45 }
46
47 fixedMapping, fixedBounds := makeSquareMapping(face, runes, fixed.I(2))
48
49 bounds := &rect{
50 tl: geo.NewPoint(
51 i2f(fixedBounds.Min.X),
52 i2f(fixedBounds.Min.Y),
53 ),
54 br: geo.NewPoint(
55 i2f(fixedBounds.Max.X),
56 i2f(fixedBounds.Max.Y),
57 ),
58 }
59
60 mapping := make(map[rune]glyph)
61 for r, fg := range fixedMapping {
62 mapping[r] = glyph{
63 dot: geo.NewPoint(
64 i2f(fg.dot.X),
65 bounds.br.Y-(i2f(fg.dot.Y)-bounds.tl.Y),
66 ),
67 frame: rect{
68 tl: geo.NewPoint(
69 i2f(fg.frame.Min.X),
70 bounds.br.Y-(i2f(fg.frame.Min.Y)-bounds.tl.Y),
71 ),
72 br: geo.NewPoint(
73 i2f(fg.frame.Max.X),
74 bounds.br.Y-(i2f(fg.frame.Max.Y)-bounds.tl.Y),
75 ),
76 }.norm(),
77 advance: i2f(fg.advance),
78 }
79 }
80
81 return &atlas{
82 face: face,
83 mapping: mapping,
84 ascent: i2f(face.Metrics().Ascent),
85 descent: i2f(face.Metrics().Descent),
86 lineHeight: i2f(face.Metrics().Height),
87 }
88 }
89
90 func (a *atlas) contains(r rune) bool {
91 _, ok := a.mapping[r]
92 return ok
93 }
94
95
96 func (a *atlas) glyph(r rune) glyph {
97 return a.mapping[r]
98 }
99
100
101
102 func (a *atlas) Kern(r0, r1 rune) float64 {
103 return i2f(a.face.Kern(r0, r1))
104 }
105
106
107 func (a *atlas) Ascent() float64 {
108 return a.ascent
109 }
110
111
112 func (a *atlas) Descent() float64 {
113 return a.descent
114 }
115
116
117
118
119
120 func (a *atlas) DrawRune(prevR, r rune, dot *geo.Point) (rect2, frame, bounds *rect, newDot *geo.Point) {
121 if !a.contains(r) {
122 r = unicode.ReplacementChar
123 }
124 if !a.contains(unicode.ReplacementChar) {
125 return newRect(), newRect(), newRect(), dot
126 }
127 if !a.contains(prevR) {
128 prevR = unicode.ReplacementChar
129 }
130
131 if prevR >= 0 {
132 dot.X += a.Kern(prevR, r)
133 }
134
135 glyph := a.glyph(r)
136
137 subbed := geo.NewPoint(
138 dot.X-glyph.dot.X,
139 dot.Y-glyph.dot.Y,
140 )
141
142 rect2 = &rect{
143 tl: geo.NewPoint(
144 glyph.frame.tl.X+subbed.X,
145 glyph.frame.tl.Y+subbed.Y,
146 ),
147 br: geo.NewPoint(
148 glyph.frame.br.X+subbed.X,
149 glyph.frame.br.Y+subbed.Y,
150 ),
151 }
152 bounds = rect2
153
154 if bounds.w()*bounds.h() != 0 {
155 bounds = &rect{
156 tl: geo.NewPoint(
157 bounds.tl.X,
158 dot.Y-a.Descent(),
159 ),
160 br: geo.NewPoint(
161 bounds.br.X,
162 dot.Y+a.Ascent(),
163 ),
164 }
165 }
166
167 dot.X += glyph.advance
168
169 return rect2, glyph.frame, bounds, dot
170 }
171
172 type fixedGlyph struct {
173 dot fixed.Point26_6
174 frame fixed.Rectangle26_6
175 advance fixed.Int26_6
176 }
177
178
179
180 func makeSquareMapping(face font.Face, runes []rune, padding fixed.Int26_6) (map[rune]fixedGlyph, fixed.Rectangle26_6) {
181 width := sort.Search(int(fixed.I(1024*1024)), func(i int) bool {
182 width := fixed.Int26_6(i)
183 _, bounds := makeMapping(face, runes, padding, width)
184 return bounds.Max.X-bounds.Min.X >= bounds.Max.Y-bounds.Min.Y
185 })
186 return makeMapping(face, runes, padding, fixed.Int26_6(width))
187 }
188
189
190
191
192 func makeMapping(face font.Face, runes []rune, padding, width fixed.Int26_6) (map[rune]fixedGlyph, fixed.Rectangle26_6) {
193 mapping := make(map[rune]fixedGlyph)
194 bounds := fixed.Rectangle26_6{}
195
196 dot := fixed.P(0, 0)
197
198 for _, r := range runes {
199 b, advance, ok := face.GlyphBounds(r)
200 if !ok {
201 continue
202 }
203
204
205 frame := fixed.Rectangle26_6{
206 Min: fixed.P(b.Min.X.Floor(), b.Min.Y.Floor()),
207 Max: fixed.P(b.Max.X.Ceil(), b.Max.Y.Ceil()),
208 }
209
210 dot.X -= frame.Min.X
211 frame = frame.Add(dot)
212
213 mapping[r] = fixedGlyph{
214 dot: dot,
215 frame: frame,
216 advance: advance,
217 }
218 bounds = bounds.Union(frame)
219
220 dot.X = frame.Max.X
221
222
223 dot.X += padding
224 dot.X = fixed.I(dot.X.Ceil())
225
226
227 if frame.Max.X >= width {
228 dot.X = 0
229 dot.Y += face.Metrics().Ascent + face.Metrics().Descent
230
231
232 dot.Y += padding
233 dot.Y = fixed.I(dot.Y.Ceil())
234 }
235 }
236
237 return mapping, bounds
238 }
239
240 func i2f(i fixed.Int26_6) float64 {
241 return float64(i) / (1 << 6)
242 }
243
View as plain text