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 "runtime"
13 "sort"
14 "strconv"
15 "strings"
16
17 "github.com/golang/freetype/truetype"
18 "golang.org/x/image/font"
19 "golang.org/x/image/math/fixed"
20)
21
22// LoadSystemFont loads a font from the system fonts directory or loads a specific font by name
23func GetFontPath(fontName string, bold, italic bool) string {
24 if len(fontName) == 0 {
25 fontName = "serif"
26 }
27
28 fonts := strings.Split(fontName, ",")
29 for _, font := range fonts {
30 font = strings.TrimSpace(font)
31 fontPath := tryLoadSystemFont(font, bold, italic)
32 if fontPath != "" {
33 return fontPath
34 }
35
36 // Check special font families only if it's the first font in the list
37
38 switch font {
39 case "sans-serif":
40 fontPath = tryLoadSystemFont("Arial", bold, italic)
41 case "monospace":
42 fontPath = tryLoadSystemFont("Andale Mono", bold, italic)
43 case "serif":
44 fontPath = tryLoadSystemFont("Georgia", bold, italic)
45 }
46
47 if fontPath != "" {
48 return fontPath
49 }
50
51 }
52
53 // Default to serif if none of the specified fonts are found
54 return tryLoadSystemFont("Georgia", bold, italic)
55}
56
57var allFonts = getSystemFonts()
58
59func tryLoadSystemFont(fontName string, bold, italic bool) string {
60 font := fontName
61 if bold {
62 font += " Bold"
63 }
64 if italic {
65 font += " Italic"
66 }
67
68 for _, v := range allFonts {
69 if strings.Contains(v, "/"+font) {
70 return v
71 }
72 }
73
74 return ""
75}
76
77func sortByLength(strings []string) {
78 sort.Slice(strings, func(i, j int) bool {
79 return len(strings[i]) < len(strings[j])
80 })
81}
82
83func GetFontSize(css map[string]string) float32 {
84 fL := len(css["font-size"])
85
86 var fs float32 = 16
87
88 if fL > 0 {
89 if css["font-size"][fL-2:] == "px" {
90 fs64, _ := strconv.ParseFloat(css["font-size"][0:fL-2], 32)
91 fs = float32(fs64)
92 }
93 if css["font-size"][fL-2:] == "em" {
94 fs64, _ := strconv.ParseFloat(css["font-size"][0:fL-2], 32)
95 fs = float32(fs64)
96 }
97 }
98
99 return fs
100}
101
102func LoadFont(fontName string, fontSize int, bold, italic bool) (font.Face, error) {
103 // Use a TrueType font file for the specified font name
104 fontFile := GetFontPath(fontName, bold, italic)
105 fmt.Println("fontFile", fontFile)
106
107 // Read the font file
108 fontData, err := os.ReadFile(fontFile)
109 if err != nil {
110 return nil, err
111 }
112
113 // Parse the TrueType font data
114 fnt, err := truetype.Parse(fontData)
115 if err != nil {
116 return nil, err
117 }
118
119 options := truetype.Options{
120 Size: float64(fontSize),
121 DPI: 72,
122 Hinting: font.HintingNone,
123 }
124
125 // Create a new font face with the specified size
126 return truetype.NewFace(fnt, &options), nil
127}
128
129func MeasureText(t *element.Text, text string) int {
130 var width fixed.Int26_6
131
132 for _, runeValue := range text {
133 if runeValue == ' ' {
134 // Handle spaces separately, add word spacing
135 width += fixed.I(t.WordSpacing)
136 } else {
137 fnt := *t.Font
138 adv, ok := fnt.GlyphAdvance(runeValue)
139 if !ok {
140 continue
141 }
142
143 // Update the total width with the glyph advance and bounds
144 width += adv + fixed.I(t.LetterSpacing)
145 }
146 }
147
148 return width.Round()
149}
150
151func MeasureSpace(t *element.Text) int {
152 fnt := *t.Font
153 adv, _ := fnt.GlyphAdvance(' ')
154 return adv.Round()
155}
156
157func getSystemFonts() []string {
158 var fontPaths []string
159
160 switch runtime.GOOS {
161 case "windows":
162 fontPaths = append(fontPaths, getWindowsFontPaths()...)
163 case "darwin":
164 fontPaths = append(fontPaths, getMacFontPaths()...)
165 case "linux":
166 fontPaths = append(fontPaths, getLinuxFontPaths()...)
167 default:
168 return nil
169 }
170
171 sortByLength(fontPaths)
172
173 return fontPaths
174}
175
176func getWindowsFontPaths() []string {
177 var fontPaths []string
178
179 // System Fonts
180 systemFontsDir := "C:\\Windows\\Fonts"
181 getFontsRecursively(systemFontsDir, &fontPaths)
182
183 // User Fonts
184 userFontsDir := os.ExpandEnv("%APPDATA%\\Microsoft\\Windows\\Fonts")
185 getFontsRecursively(userFontsDir, &fontPaths)
186
187 return fontPaths
188}
189
190func getMacFontPaths() []string {
191 var fontPaths []string
192
193 // System Fonts
194 systemFontsDirs := []string{"/System/Library/Fonts", "/Library/Fonts"}
195 for _, dir := range systemFontsDirs {
196 getFontsRecursively(dir, &fontPaths)
197 }
198
199 // User Fonts
200 userFontsDir := filepath.Join(os.Getenv("HOME"), "Library/Fonts")
201 getFontsRecursively(userFontsDir, &fontPaths)
202
203 return fontPaths
204}
205
206func getLinuxFontPaths() []string {
207 var fontPaths []string
208
209 // System Fonts
210 systemFontsDirs := []string{"/usr/share/fonts", "/usr/local/share/fonts"}
211 for _, dir := range systemFontsDirs {
212 getFontsRecursively(dir, &fontPaths)
213 }
214
215 // User Fonts
216 userFontsDir := filepath.Join(os.Getenv("HOME"), ".fonts")
217 getFontsRecursively(userFontsDir, &fontPaths)
218
219 return fontPaths
220}
221
222func getFontsRecursively(dir string, fontPaths *[]string) {
223 files, err := ioutil.ReadDir(dir)
224 if err != nil {
225 return
226 }
227
228 for _, file := range files {
229 path := filepath.Join(dir, file.Name())
230 if file.IsDir() {
231 getFontsRecursively(path, fontPaths)
232 } else if strings.HasSuffix(strings.ToLower(file.Name()), ".ttf") {
233 *fontPaths = append(*fontPaths, path)
234 }
235 }
236}
237
238func Render(t *element.Text) (*image.RGBA, int) {
239 // fmt.Println(lines)
240
241 if t.LineHeight == 0 {
242 t.LineHeight = t.EM + 3
243 }
244 var width int
245 if t.Last {
246 width = MeasureText(t, t.Text)
247 } else {
248 width = MeasureText(t, t.Text+" ")
249 }
250
251 // Use fully transparent color for the background
252 img := image.NewRGBA(image.Rect(0, 0, width, t.LineHeight))
253
254 // fmt.Println(t.Width, t.LineHeight, (len(lines)))
255
256 r, g, b, a := t.Color.RGBA()
257
258 draw.Draw(img, img.Bounds(), &image.Uniform{color.RGBA{uint8(r), uint8(g), uint8(b), uint8(0)}}, image.Point{}, draw.Over)
259 // fmt.Println(int(t.Font.Metrics().Ascent))
260 dot := fixed.Point26_6{X: fixed.I(0), Y: (fixed.I(t.LineHeight+(t.EM/2)) / 2)}
261
262 dr := &font.Drawer{
263 Dst: img,
264 Src: &image.Uniform{color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)}},
265 Face: *t.Font,
266 Dot: dot,
267 }
268
269 drawn := drawString(*t, dr, t.Text, width, img)
270
271 return drawn, width
272}
273
274func drawString(t element.Text, dr *font.Drawer, v string, lineWidth int, img *image.RGBA) *image.RGBA {
275 underlinePosition := dr.Dot
276 for _, ch := range v {
277 if ch == ' ' {
278 // Handle spaces separately, add word spacing
279 dr.Dot.X += fixed.I(t.WordSpacing)
280 } else {
281 dr.DrawString(string(ch))
282 dr.Dot.X += fixed.I(t.LetterSpacing)
283 }
284 }
285 if t.Underlined || t.Overlined || t.LineThrough {
286
287 underlinePosition.X = 0
288 baseLineY := underlinePosition.Y
289 fnt := *t.Font
290 descent := fnt.Metrics().Descent
291 if t.Underlined {
292 underlinePosition.Y = baseLineY + descent
293 underlinePosition.Y = (underlinePosition.Y / 100) * 97
294 drawLine(img, underlinePosition, fixed.Int26_6(lineWidth), t.DecorationThickness, t.DecorationColor)
295 }
296 if t.LineThrough {
297 underlinePosition.Y = baseLineY - (descent)
298 drawLine(img, underlinePosition, fixed.Int26_6(lineWidth), t.DecorationThickness, t.DecorationColor)
299 }
300 if t.Overlined {
301 underlinePosition.Y = baseLineY - descent*3
302 drawLine(img, underlinePosition, fixed.Int26_6(lineWidth), t.DecorationThickness, t.DecorationColor)
303 }
304 }
305 return img
306}
307
308func drawLine(img draw.Image, start fixed.Point26_6, width fixed.Int26_6, thickness int, col color.Color) {
309 // Bresenham's line algorithm
310 x0, y0 := start.X.Round(), start.Y.Round()
311 x1 := x0 + int(width)
312 y1 := y0
313 dx := abs(x1 - x0)
314 dy := abs(y1 - y0)
315 sx, sy := 1, 1
316
317 if x0 > x1 {
318 sx = -1
319 }
320 if y0 > y1 {
321 sy = -1
322 }
323
324 err := dx - dy
325
326 for {
327 for i := 0; i < thickness; i++ {
328 img.Set(x0, (y0-(thickness/2))+i, col)
329 }
330
331 if x0 == x1 && y0 == y1 {
332 break
333 }
334
335 e2 := 2 * err
336 if e2 > -dy {
337 err -= dy
338 x0 += sx
339 }
340 if e2 < dx {
341 err += dx
342 y0 += sy
343 }
344 }
345}
346
347func abs(x int) int {
348 if x < 0 {
349 return -x
350 }
351 return x
352}
353
354func Min(a, b float32) float32 {
355 if a < b {
356 return a
357 } else {
358 return b
359 }
360}