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