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
-260 if t.LineHeight == 0 {
-261 t.LineHeight = t.EM + 3
-262 }
-263 // Use fully transparent color for the background
-264 img := image.NewRGBA(image.Rect(0, 0, t.Width, t.LineHeight*(len(lines))))
-265
-266 // fmt.Println(t.Width, t.LineHeight, (len(lines)))
-267
-268 r, g, b, a := t.Color.RGBA()
-269
-270 draw.Draw(img, img.Bounds(), &image.Uniform{color.RGBA{uint8(r), uint8(g), uint8(b), uint8(0)}}, image.Point{}, draw.Over)
-271 // fmt.Println(int(t.Font.Metrics().Ascent))
-272 dot := fixed.Point26_6{X: fixed.I(0), Y: (fixed.I(t.LineHeight+(t.EM/2)) / 2)}
-273
-274 dr := &font.Drawer{
-275 Dst: img,
-276 Src: &image.Uniform{color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)}},
-277 Face: t.Font,
-278 Dot: dot,
-279 }
-280 t.Image = img
-281
-282 fh := fixed.I(t.LineHeight)
-283
-284 for _, v := range lines {
-285 lineWidth := MeasureText(s, v)
-286 if t.Align == "justify" {
-287 dr.Dot.X = 0
-288 spaces := strings.Count(v, " ")
-289 if spaces > 1 {
-290 spacing := fixed.I((t.Width - MeasureText(s, v)) / spaces)
-291
-292 if spacing > 0 {
-293 for _, word := range strings.Fields(v) {
-294 dr.DrawString(word)
-295 dr.Dot.X += spacing
-296 }
-297 } else {
-298 dr.Dot.X = 0
-299 drawString(*t, dr, v, lineWidth)
-300 }
-301 } else {
-302 dr.Dot.X = 0
-303 drawString(*t, dr, v, lineWidth)
-304 }
-305
-306 } else {
-307 if t.Align == "left" || t.Align == "" {
-308 dr.Dot.X = 0
-309 } else if t.Align == "center" {
-310 dr.Dot.X = fixed.I((t.Width - MeasureText(s, v)) / 2)
-311 } else if t.Align == "right" {
-312 dr.Dot.X = fixed.I(t.Width - MeasureText(s, v))
-313 }
-314 // dr.Dot.X = 0
-315 drawString(*t, dr, v, lineWidth)
-316 }
-317 dr.Dot.Y += fh
-318 }
-319 s.Text.X = MeasureText(s, lines[len(lines)-1])
-320 return float32(t.LineHeight * len(lines))
-321}
-322
-323func drawString(t element.Text, dr *font.Drawer, v string, lineWidth int) {
-324 underlinePosition := dr.Dot
-325 for _, ch := range v {
-326 if ch == ' ' {
-327 // Handle spaces separately, add word spacing
-328 dr.Dot.X += fixed.I(t.WordSpacing)
-329 } else {
-330 dr.DrawString(string(ch))
-331 dr.Dot.X += fixed.I(t.LetterSpacing)
-332 }
-333 }
-334 if t.Underlined || t.Overlined || t.LineThrough {
-335
-336 underlinePosition.X = 0
-337 baseLineY := underlinePosition.Y
-338
-339 if t.Underlined {
-340 underlinePosition.Y = baseLineY + t.Font.Metrics().Descent
-341 drawLine(t.Image, underlinePosition, fixed.Int26_6(lineWidth), t.DecorationThickness, t.DecorationColor)
-342 }
-343 if t.LineThrough {
-344 underlinePosition.Y = baseLineY - (t.Font.Metrics().Descent)
-345 drawLine(t.Image, underlinePosition, fixed.Int26_6(lineWidth), t.DecorationThickness, t.DecorationColor)
-346 }
-347 if t.Overlined {
-348 underlinePosition.Y = baseLineY - t.Font.Metrics().Descent*3
-349 drawLine(t.Image, underlinePosition, fixed.Int26_6(lineWidth), t.DecorationThickness, t.DecorationColor)
-350 }
-351 }
-352}
-353
-354func wrap(s *element.State, breaker string, breakNewLines bool) []string {
-355 var start int = 0
-356 strngs := []string{}
-357 var text []string
-358 broken := strings.Split(s.Text.Text, breaker)
-359 re := regexp.MustCompile(`[\r\n]+`)
-360 if breakNewLines {
-361 for _, v := range broken {
-362 text = append(text, re.Split(v, -1)...)
-363 }
-364 } else {
-365 text = append(text, broken...)
-366 }
-367 for i := 0; i < len(text); i++ {
-368 text[i] = re.ReplaceAllString(text[i], "")
-369 }
-370 for i := 0; i < len(text); i++ {
-371 seg := strings.Join(text[start:int(Min(float32(i+1), float32(len(text))))], breaker)
-372 if MeasureText(s, seg) > s.Text.Width {
-373 strngs = append(strngs, strings.Join(text[start:i], breaker))
-374 start = i
-375 }
-376 }
-377 if len(strngs) > 0 {
-378 strngs = append(strngs, strings.Join(text[start:], breaker))
-379 } else {
-380 strngs = append(strngs, strings.Join(text[start:], breaker))
-381 }
-382 return strngs
-383}
-384
-385func drawLine(img draw.Image, start fixed.Point26_6, width fixed.Int26_6, thickness int, col color.Color) {
-386 // Bresenham's line algorithm
-387 x0, y0 := start.X.Round(), start.Y.Round()
-388 x1 := x0 + int(width)
-389 y1 := y0
-390 dx := abs(x1 - x0)
-391 dy := abs(y1 - y0)
-392 sx, sy := 1, 1
-393
-394 if x0 > x1 {
-395 sx = -1
-396 }
-397 if y0 > y1 {
-398 sy = -1
-399 }
-400
-401 err := dx - dy
-402
-403 for {
-404 for i := 0; i < thickness; i++ {
-405 img.Set(x0, (y0-(thickness/2))+i, col)
-406 }
-407
-408 if x0 == x1 && y0 == y1 {
-409 break
-410 }
-411
-412 e2 := 2 * err
-413 if e2 > -dy {
-414 err -= dy
-415 x0 += sx
-416 }
-417 if e2 < dx {
-418 err += dx
-419 y0 += sy
-420 }
-421 }
-422}
-423
-424func abs(x int) int {
-425 if x < 0 {
-426 return -x
-427 }
-428 return x
-429}
-430
-431func getLines(s *element.State) []string {
-432 t := s.Text
-433 text := s.Text.Text
-434 var lines []string
-435 if t.WhiteSpace == "nowrap" {
-436 re := regexp.MustCompile(`\s+`)
-437 s.Text.Text = re.ReplaceAllString(text, " ")
-438 lines = wrap(s, "<br />", false)
-439 } else {
-440 if t.WhiteSpace == "pre" {
-441 re := regexp.MustCompile("\t")
-442 s.Text.Text = re.ReplaceAllString(text, " ")
-443 nl := regexp.MustCompile(`[\r\n]+`)
-444 lines = nl.Split(text, -1)
-445 } else if t.WhiteSpace == "pre-line" {
-446 re := regexp.MustCompile(`\s+`)
-447 s.Text.Text = re.ReplaceAllString(text, " ")
-448 lines = wrap(s, " ", true)
-449 } else if t.WhiteSpace == "pre-wrap" {
-450 lines = wrap(s, " ", true)
-451 } else {
-452 re := regexp.MustCompile(`\s+`)
-453 s.Text.Text = re.ReplaceAllString(text, " ")
-454 nl := regexp.MustCompile(`[\r\n]+`)
-455 s.Text.Text = nl.ReplaceAllString(text, "")
-456 // n.InnerText = strings.TrimSpace(text)
-457 lines = wrap(s, t.WordBreak, false)
-458 }
-459 for i, v := range lines {
-460 lines[i] = v + t.WordBreak
-461 }
-462 }
-463 return lines
-464}
-465
-466func Min(a, b float32) float32 {
-467 if a < b {
-468 return a
-469 } else {
-470 return b
-471 }
-472}
-