Font
Font rasterizes all text in a document and handles the wrapping and positionion of text within a texture.
flowchart LR; LoadFont-->State.Text.Font; State.Text.Font-->State; State-->Render;
# GetFontPath?(go)
# tryLoadSystemFont?(go)
# sortByLength?(go)
# GetFontSize?(go)
# LoadFont?(go)
# MeasureText?(go)
# MeasureSpace?(go)
# MeasureLongest?(go)
# getSystemFonts?(go)
# getWindowsFontPaths?(go)
# getMacFontPaths?(go)
# getLinuxFontPaths?(go)
# getFontsRecursively?(go)
# Render?(go)
# drawString?(go)
# wrap?(go)
# drawLine?(go)
# getLines?(go)
1package font
2
3import (
4 "fmt"
5 "gui/element"
6 "image"
7 "image/color"
8 "image/draw"
9 "io/ioutil"
10 "os"
11 "path/filepath"
12 "regexp"
13 "runtime"
14 "sort"
15 "strconv"
16 "strings"
17
18 "github.com/golang/freetype/truetype"
19 "golang.org/x/image/font"
20 "golang.org/x/image/math/fixed"
21)
22
23// LoadSystemFont loads a font from the system fonts directory or loads a specific font by name
24func GetFontPath(fontName string, bold, italic bool) string {
25
26 if len(fontName) == 0 {
27 fontName = "serif"
28 }
29
30 // Check if a special font family is requested
31 switch fontName {
32 case "sans-serif":
33 return tryLoadSystemFont("Arial", bold, italic)
34 case "monospace":
35 return tryLoadSystemFont("Andle Mono", bold, italic)
36 case "serif":
37 return tryLoadSystemFont("Georgia", bold, italic)
38 }
39
40 // Use the default font if the specified font is not found
41 return tryLoadSystemFont(fontName, bold, italic)
42}
43
44var allFonts, _ = getSystemFonts()
45
46func tryLoadSystemFont(fontName string, bold, italic bool) string {
47 font := fontName
48 if bold {
49 font += " Bold"
50 }
51 if italic {
52 font += " Italic"
53 }
54 for _, v := range allFonts {
55 if strings.Contains(v, "/"+font) {
56 return v
57 }
58 }
59
60 return ""
61}
62
63func sortByLength(strings []string) {
64 sort.Slice(strings, func(i, j int) bool {
65 return len(strings[i]) < len(strings[j])
66 })
67}
68
69func GetFontSize(css map[string]string) float32 {
70 fL := len(css["font-size"])
71
72 var fs float32 = 16
73
74 if fL > 0 {
75 if css["font-size"][fL-2:] == "px" {
76 fs64, _ := strconv.ParseFloat(css["font-size"][0:fL-2], 32)
77 fs = float32(fs64)
78 }
79 if css["font-size"][fL-2:] == "em" {
80 fs64, _ := strconv.ParseFloat(css["font-size"][0:fL-2], 32)
81 fs = float32(fs64)
82 }
83 }
84
85 return fs
86}
87
88func LoadFont(fontName string, fontSize int, bold, italic bool) (font.Face, error) {
89 // Use a TrueType font file for the specified font name
90 fontFile := GetFontPath(fontName, bold, italic)
91
92 // Read the font file
93 fontData, err := os.ReadFile(fontFile)
94 if err != nil {
95 return nil, err
96 }
97
98 // Parse the TrueType font data
99 fnt, err := truetype.Parse(fontData)
100 if err != nil {
101 return nil, err
102 }
103
104 options := truetype.Options{
105 Size: float64(fontSize),
106 DPI: 72,
107 Hinting: font.HintingNone,
108 }
109
110 // Create a new font face with the specified size
111 return truetype.NewFace(fnt, &options), nil
112}
113
114// func MeasureLine(n *element.Node, state *element.State) (int, int) {
115// passed := false
116// lineOffset, nodeOffset := 0, 0
117// for _, v := range n.Parent.Children {
118// l := MeasureText(state, v.InnerText)
119// if v.Properties.Id == n.Properties.Id {
120// passed = true
121// lineOffset += l
122// } else {
123// if !passed {
124// nodeOffset += l
125// }
126// lineOffset += l
127// }
128// }
129// return lineOffset, nodeOffset
130// }
131
132func MeasureText(s *element.State, text string) int {
133 t := s.Text
134 var width fixed.Int26_6
135
136 for _, runeValue := range text {
137 if runeValue == ' ' {
138 // Handle spaces separately, add word spacing
139 width += fixed.I(t.WordSpacing)
140 } else {
141 adv, ok := t.Font.GlyphAdvance(runeValue)
142 if !ok {
143 continue
144 }
145
146 // Update the total width with the glyph advance and bounds
147 width += adv + fixed.I(t.LetterSpacing)
148 }
149 }
150
151 return width.Round()
152}
153
154func MeasureSpace(t *element.Text) int {
155 adv, _ := t.Font.GlyphAdvance(' ')
156 return adv.Round()
157}
158
159func MeasureLongest(s *element.State) int {
160 lines := getLines(s)
161 var longestLine string
162 maxLength := 0
163
164 for _, line := range lines {
165 length := len(line)
166 if length > maxLength {
167 maxLength = length
168 longestLine = line
169 }
170 }
171 return MeasureText(s, longestLine)
172}
173
174func getSystemFonts() ([]string, error) {
175 var fontPaths []string
176
177 switch runtime.GOOS {
178 case "windows":
179 fontPaths = append(fontPaths, getWindowsFontPaths()...)
180 case "darwin":
181 fontPaths = append(fontPaths, getMacFontPaths()...)
182 case "linux":
183 fontPaths = append(fontPaths, getLinuxFontPaths()...)
184 default:
185 return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
186 }
187
188 sortByLength(fontPaths)
189
190 return fontPaths, nil
191}
192
193func getWindowsFontPaths() []string {
194 var fontPaths []string
195
196 // System Fonts
197 systemFontsDir := "C:\\Windows\\Fonts"
198 getFontsRecursively(systemFontsDir, &fontPaths)
199
200 // User Fonts
201 userFontsDir := os.ExpandEnv("%APPDATA%\\Microsoft\\Windows\\Fonts")
202 getFontsRecursively(userFontsDir, &fontPaths)
203
204 return fontPaths
205}
206
207func getMacFontPaths() []string {
208 var fontPaths []string
209
210 // System Fonts
211 systemFontsDirs := []string{"/System/Library/Fonts", "/Library/Fonts"}
212 for _, dir := range systemFontsDirs {
213 getFontsRecursively(dir, &fontPaths)
214 }
215
216 // User Fonts
217 userFontsDir := filepath.Join(os.Getenv("HOME"), "Library/Fonts")
218 getFontsRecursively(userFontsDir, &fontPaths)
219
220 return fontPaths
221}
222
223func getLinuxFontPaths() []string {
224 var fontPaths []string
225
226 // System Fonts
227 systemFontsDirs := []string{"/usr/share/fonts", "/usr/local/share/fonts"}
228 for _, dir := range systemFontsDirs {
229 getFontsRecursively(dir, &fontPaths)
230 }
231
232 // User Fonts
233 userFontsDir := filepath.Join(os.Getenv("HOME"), ".fonts")
234 getFontsRecursively(userFontsDir, &fontPaths)
235
236 return fontPaths
237}
238
239func getFontsRecursively(dir string, fontPaths *[]string) {
240 files, err := ioutil.ReadDir(dir)
241 if err != nil {
242 fmt.Println("Error reading directory:", err)
243 return
244 }
245
246 for _, file := range files {
247 path := filepath.Join(dir, file.Name())
248 if file.IsDir() {
249 getFontsRecursively(path, fontPaths)
250 } else if strings.HasSuffix(strings.ToLower(file.Name()), ".ttf") {
251 *fontPaths = append(*fontPaths, path)
252 }
253 }
254}
255
256func Render(s *element.State) float32 {
257 t := &s.Text
258 lines := getLines(s)
259 // fmt.Println(lines)
260
261 if t.LineHeight == 0 {
262 t.LineHeight = t.EM + 3
263 }
264 // Use fully transparent color for the background
265 img := image.NewRGBA(image.Rect(0, 0, t.Width, t.LineHeight*(len(lines))))
266
267 // fmt.Println(t.Width, t.LineHeight, (len(lines)))
268
269 r, g, b, a := t.Color.RGBA()
270
271 draw.Draw(img, img.Bounds(), &image.Uniform{color.RGBA{uint8(r), uint8(g), uint8(b), uint8(0)}}, image.Point{}, draw.Over)
272 // fmt.Println(int(t.Font.Metrics().Ascent))
273 dot := fixed.Point26_6{X: fixed.I(0), Y: (fixed.I(t.LineHeight+(t.EM/2)) / 2)}
274
275 dr := &font.Drawer{
276 Dst: img,
277 Src: &image.Uniform{color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)}},
278 Face: t.Font,
279 Dot: dot,
280 }
281 t.Image = img
282
283 fh := fixed.I(t.LineHeight)
284
285 for _, v := range lines {
286 lineWidth := MeasureText(s, v)
287 if t.Align == "justify" {
288 dr.Dot.X = 0
289 spaces := strings.Count(v, " ")
290 if spaces > 1 {
291 spacing := fixed.I((t.Width - MeasureText(s, v)) / spaces)
292
293 if spacing > 0 {
294 for _, word := range strings.Fields(v) {
295 dr.DrawString(word)
296 dr.Dot.X += spacing
297 }
298 } else {
299 dr.Dot.X = 0
300 drawString(*t, dr, v, lineWidth)
301 }
302 } else {
303 dr.Dot.X = 0
304 drawString(*t, dr, v, lineWidth)
305 }
306
307 } else {
308 if t.Align == "left" || t.Align == "" {
309 dr.Dot.X = 0
310 } else if t.Align == "center" {
311 dr.Dot.X = fixed.I((t.Width - MeasureText(s, v)) / 2)
312 } else if t.Align == "right" {
313 dr.Dot.X = fixed.I(t.Width - MeasureText(s, v))
314 }
315 // dr.Dot.X = 0
316 drawString(*t, dr, v, lineWidth)
317 }
318 dr.Dot.Y += fh
319 }
320 s.Text.X = MeasureText(s, lines[len(lines)-1])
321 return float32(t.LineHeight * len(lines))
322}
323
324func drawString(t element.Text, dr *font.Drawer, v string, lineWidth int) {
325 underlinePosition := dr.Dot
326 for _, ch := range v {
327 if ch == ' ' {
328 // Handle spaces separately, add word spacing
329 dr.Dot.X += fixed.I(t.WordSpacing)
330 } else {
331 dr.DrawString(string(ch))
332 dr.Dot.X += fixed.I(t.LetterSpacing)
333 }
334 }
335 if t.Underlined || t.Overlined || t.LineThrough {
336
337 underlinePosition.X = 0
338 baseLineY := underlinePosition.Y
339
340 if t.Underlined {
341 underlinePosition.Y = baseLineY + t.Font.Metrics().Descent
342 drawLine(t.Image, underlinePosition, fixed.Int26_6(lineWidth), t.DecorationThickness, t.DecorationColor)
343 }
344 if t.LineThrough {
345 underlinePosition.Y = baseLineY - (t.Font.Metrics().Descent)
346 drawLine(t.Image, underlinePosition, fixed.Int26_6(lineWidth), t.DecorationThickness, t.DecorationColor)
347 }
348 if t.Overlined {
349 underlinePosition.Y = baseLineY - t.Font.Metrics().Descent*3
350 drawLine(t.Image, underlinePosition, fixed.Int26_6(lineWidth), t.DecorationThickness, t.DecorationColor)
351 }
352 }
353}
354
355func drawLine(img draw.Image, start fixed.Point26_6, width fixed.Int26_6, thickness int, col color.Color) {
356 // Bresenham's line algorithm
357 x0, y0 := start.X.Round(), start.Y.Round()
358 x1 := x0 + int(width)
359 y1 := y0
360 dx := abs(x1 - x0)
361 dy := abs(y1 - y0)
362 sx, sy := 1, 1
363
364 if x0 > x1 {
365 sx = -1
366 }
367 if y0 > y1 {
368 sy = -1
369 }
370
371 err := dx - dy
372
373 for {
374 for i := 0; i < thickness; i++ {
375 img.Set(x0, (y0-(thickness/2))+i, col)
376 }
377
378 if x0 == x1 && y0 == y1 {
379 break
380 }
381
382 e2 := 2 * err
383 if e2 > -dy {
384 err -= dy
385 x0 += sx
386 }
387 if e2 < dx {
388 err += dx
389 y0 += sy
390 }
391 }
392}
393
394func wrap(s *element.State, breaker string, breakNewLines bool) []string {
395 var start int = 0
396 strngs := []string{}
397 var text []string
398 broken := strings.Split(s.Text.Text, breaker)
399 re := regexp.MustCompile(`[\r\n]+`)
400 if breakNewLines {
401 for _, v := range broken {
402 text = append(text, re.Split(v, -1)...)
403 }
404 } else {
405 text = append(text, broken...)
406 }
407 for i := 0; i < len(text); i++ {
408 text[i] = re.ReplaceAllString(text[i], "")
409 }
410 for i := 0; i < len(text); i++ {
411 seg := strings.Join(text[start:int(Min(float32(i+1), float32(len(text))))], breaker)
412 if MeasureText(s, seg) > s.Text.Width {
413 strngs = append(strngs, strings.Join(text[start:i], breaker))
414 start = i
415 }
416 }
417 if len(strngs) > 0 {
418 strngs = append(strngs, strings.Join(text[start:], breaker))
419 } else {
420 strngs = append(strngs, strings.Join(text[start:], breaker))
421 }
422 return strngs
423}
424
425func getLines(s *element.State) []string {
426 t := s.Text
427 text := s.Text.Text
428 var lines []string
429 if t.WhiteSpace == "nowrap" {
430 re := regexp.MustCompile(`\s+`)
431 s.Text.Text = re.ReplaceAllString(text, " ")
432 lines = wrap(s, "<br />", false)
433 } else {
434 if t.WhiteSpace == "pre" {
435 re := regexp.MustCompile("\t")
436 s.Text.Text = re.ReplaceAllString(text, " ")
437 nl := regexp.MustCompile(`[\r\n]+`)
438 lines = nl.Split(text, -1)
439 } else if t.WhiteSpace == "pre-line" {
440 re := regexp.MustCompile(`\s+`)
441 s.Text.Text = re.ReplaceAllString(text, " ")
442 lines = wrap(s, " ", true)
443 } else if t.WhiteSpace == "pre-wrap" {
444 lines = wrap(s, " ", true)
445 } else {
446 re := regexp.MustCompile(`\s+`)
447 s.Text.Text = re.ReplaceAllString(text, " ")
448 nl := regexp.MustCompile(`[\r\n]+`)
449 s.Text.Text = nl.ReplaceAllString(text, "")
450 // n.InnerText = strings.TrimSpace(text)
451 lines = wrap(s, t.WordBreak, false)
452 }
453 for i, v := range lines {
454 lines[i] = v + t.WordBreak
455 }
456 }
457 return lines
458}
459
460func abs(x int) int {
461 if x < 0 {
462 return -x
463 }
464 return x
465}
466
467func Min(a, b float32) float32 {
468 if a < b {
469 return a
470 } else {
471 return b
472 }
473}