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