1
2
3
4 package textmeasure
5
6 import (
7 "math"
8 "strings"
9 "unicode"
10 "unicode/utf8"
11
12 "github.com/golang/freetype/truetype"
13 "github.com/rivo/uniseg"
14
15 "oss.terrastruct.com/d2/d2renderers/d2fonts"
16 "oss.terrastruct.com/d2/lib/geo"
17 )
18
19 const TAB_SIZE = 4
20 const SIZELESS_FONT_SIZE = 0
21 const CODE_LINE_HEIGHT = 1.3
22
23
24 var ASCII []rune
25
26 func init() {
27 ASCII = make([]rune, unicode.MaxASCII-32)
28 for i := range ASCII {
29 ASCII[i] = rune(32 + i)
30 }
31 }
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62 type Ruler struct {
63
64
65 Orig *geo.Point
66
67
68
69 Dot *geo.Point
70
71
72
73
74
75 LineHeightFactor float64
76 lineHeights map[d2fonts.Font]float64
77
78
79
80
81
82
83 tabWidths map[d2fonts.Font]float64
84
85 atlases map[d2fonts.Font]*atlas
86
87 ttfs map[d2fonts.Font]*truetype.Font
88
89 buf []byte
90 prevR rune
91 bounds *rect
92
93
94 boundsWithDot bool
95 }
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110 func NewRuler() (*Ruler, error) {
111 origin := geo.NewPoint(0, 0)
112 r := &Ruler{
113 Orig: origin,
114 Dot: origin.Copy(),
115 LineHeightFactor: 1.,
116 lineHeights: make(map[d2fonts.Font]float64),
117 tabWidths: make(map[d2fonts.Font]float64),
118 atlases: make(map[d2fonts.Font]*atlas),
119 ttfs: make(map[d2fonts.Font]*truetype.Font),
120 }
121
122 for _, fontFamily := range d2fonts.FontFamilies {
123 for _, fontStyle := range d2fonts.FontStyles {
124 font := d2fonts.Font{
125 Family: fontFamily,
126 Style: fontStyle,
127 }
128
129 face, has := d2fonts.FontFaces.Lookup(font)
130 if !has {
131 continue
132 }
133 if _, loaded := r.ttfs[font]; !loaded {
134 ttf, err := truetype.Parse(face)
135 if err != nil {
136 return nil, err
137 }
138 r.ttfs[font] = ttf
139 }
140 }
141 }
142
143 r.clear()
144
145 return r, nil
146 }
147
148 func (r *Ruler) HasFontFamilyLoaded(fontFamily *d2fonts.FontFamily) bool {
149 for _, fontStyle := range d2fonts.FontStyles {
150 font := d2fonts.Font{
151 Family: *fontFamily,
152 Style: fontStyle,
153 Size: SIZELESS_FONT_SIZE,
154 }
155 _, ok := r.ttfs[font]
156 if !ok {
157 return false
158 }
159 }
160
161 return true
162 }
163
164 func (r *Ruler) addFontSize(font d2fonts.Font) {
165 sizeless := font
166 sizeless.Size = SIZELESS_FONT_SIZE
167 face := truetype.NewFace(r.ttfs[sizeless], &truetype.Options{
168 Size: float64(font.Size),
169 })
170 atlas := NewAtlas(face, ASCII)
171 r.atlases[font] = atlas
172 r.lineHeights[font] = atlas.lineHeight
173 r.tabWidths[font] = atlas.glyph(' ').advance * TAB_SIZE
174 }
175
176 func (t *Ruler) scaleUnicode(w float64, font d2fonts.Font, s string) float64 {
177
178
179
180
181
182 if uniseg.GraphemeClusterCount(s) != len(s) {
183 for _, line := range strings.Split(s, "\n") {
184 lineW, _ := t.MeasurePrecise(font, line)
185 gr := uniseg.NewGraphemes(line)
186
187 mono := d2fonts.SourceCodePro.Font(font.Size, font.Style)
188 for gr.Next() {
189 if gr.Width() == 1 {
190 continue
191 }
192
193
194 var prevRune rune
195 dot := t.Orig.Copy()
196 b := newRect()
197 for _, r := range gr.Runes() {
198 var control bool
199 dot, control = t.controlRune(r, dot, font)
200 if control {
201 continue
202 }
203
204 var bounds *rect
205 _, _, bounds, dot = t.atlases[font].DrawRune(prevRune, r, dot)
206 b = b.union(bounds)
207
208 prevRune = r
209 }
210 lineW -= b.w()
211 lineW += t.spaceWidth(mono) * float64(gr.Width())
212 }
213 w = math.Max(w, lineW)
214 }
215 }
216 return w
217 }
218
219 func (t *Ruler) MeasureMono(font d2fonts.Font, s string) (width, height int) {
220 originalBoundsWithDot := t.boundsWithDot
221 t.boundsWithDot = true
222 width, height = t.Measure(font, s)
223 t.boundsWithDot = originalBoundsWithDot
224 return width, height
225 }
226
227 func (t *Ruler) Measure(font d2fonts.Font, s string) (width, height int) {
228 w, h := t.MeasurePrecise(font, s)
229 w = t.scaleUnicode(w, font, s)
230 return int(math.Ceil(w)), int(math.Ceil(h))
231 }
232
233 func (t *Ruler) MeasurePrecise(font d2fonts.Font, s string) (width, height float64) {
234 if _, ok := t.atlases[font]; !ok {
235 t.addFontSize(font)
236 }
237 t.clear()
238 t.buf = append(t.buf, s...)
239 t.drawBuf(font)
240 b := t.bounds
241 return b.w(), b.h()
242 }
243
244
245 func (txt *Ruler) clear() {
246 txt.prevR = -1
247 txt.bounds = newRect()
248 txt.Dot = txt.Orig.Copy()
249 }
250
251
252
253 func (txt *Ruler) controlRune(r rune, dot *geo.Point, font d2fonts.Font) (newDot *geo.Point, control bool) {
254 switch r {
255 case '\n':
256 dot.X = txt.Orig.X
257 dot.Y -= txt.LineHeightFactor * txt.lineHeights[font]
258 case '\r':
259 dot.X = txt.Orig.X
260 case '\t':
261 rem := math.Mod(dot.X-txt.Orig.X, txt.tabWidths[font])
262 rem = math.Mod(rem, rem+txt.tabWidths[font])
263 if rem == 0 {
264 rem = txt.tabWidths[font]
265 }
266 dot.X += rem
267 default:
268 return dot, false
269 }
270 return dot, true
271 }
272
273 func (txt *Ruler) drawBuf(font d2fonts.Font) {
274 if !utf8.FullRune(txt.buf) {
275 return
276 }
277
278 for utf8.FullRune(txt.buf) {
279 r, l := utf8.DecodeRune(txt.buf)
280 txt.buf = txt.buf[l:]
281
282 var control bool
283 txt.Dot, control = txt.controlRune(r, txt.Dot, font)
284 if control {
285 continue
286 }
287
288 var bounds *rect
289 _, _, bounds, txt.Dot = txt.atlases[font].DrawRune(txt.prevR, r, txt.Dot)
290
291 txt.prevR = r
292
293 if txt.boundsWithDot {
294 txt.bounds = txt.bounds.union(&rect{txt.Dot, txt.Dot})
295 txt.bounds = txt.bounds.union(bounds)
296 } else {
297 if txt.bounds.w()*txt.bounds.h() == 0 {
298 txt.bounds = bounds
299 } else {
300 txt.bounds = txt.bounds.union(bounds)
301 }
302 }
303 }
304 }
305
306 func (ruler *Ruler) spaceWidth(font d2fonts.Font) float64 {
307 if _, has := ruler.atlases[font]; !has {
308 ruler.addFontSize(font)
309 }
310 spaceRune, _ := utf8.DecodeRuneInString(" ")
311 return ruler.atlases[font].glyph(spaceRune).advance
312 }
313
View as plain text