Diff
diff --git a/font/main.go b/font/main.go
index 3ffc6ec..2f2455a 100644
--- a/font/main.go
+++ b/font/main.go
@@ -6 +5,0 @@ import (
- "grim/canvas"
@@ -8 +6,0 @@ import (
- "grim/utils"
@@ -11,3 +9,3 @@ import (
- "path/filepath"
- "regexp"
- "strconv"
+ "image/draw"
+ "runtime"
+ "sort"
@@ -16,2 +13,0 @@ import (
- cc "grim/color"
-
@@ -19,0 +16 @@ import (
+ "golang.org/x/image/math/fixed"
@@ -22,28 +19,5 @@ import (
-type MetaData struct {
- Font *font.Face
- Color color.RGBA
- Text string
- Underlined bool
- Overlined bool
- LineThrough bool
- DecorationColor color.RGBA
- DecorationThickness int
- Align string
- Indent int // very low priority
- LetterSpacing int
- LineHeight int
- WordSpacing int
- WhiteSpace string
- Shadows []Shadow // need
- Width int
- WordBreak string
- EM int
- X int
- UnderlineOffset int
-}
-
-type Shadow struct {
- X int
- Y int
- Blur int
- Color color.RGBA
+func sortByLength(strings []string) []string {
+ sort.Slice(strings, func(i, j int) bool {
+ return len(strings[i]) < len(strings[j])
+ })
+ return strings
@@ -53 +27 @@ type Shadow struct {
-func GetFontPath(fontName string, bold string, italic bool, fs *adapter.FileSystem) string {
+func GetFontPath(fontName string, bold int, italic bool, fs *adapter.FileSystem) string {
@@ -59 +33 @@ func GetFontPath(fontName string, bold string, italic bool, fs *adapter.FileSyst
- paths := fs.Paths
+ paths := sortByLength(fs.Paths)
@@ -62 +36,5 @@ func GetFontPath(fontName string, bold string, italic bool, fs *adapter.FileSyst
- var fontPath string
+ fontPath := tryLoadSystemFontWithBestWeight(font, bold, italic, paths)
+
+ if fontPath != "" {
+ return fontPath
+ }
@@ -67 +45 @@ func GetFontPath(fontName string, bold string, italic bool, fs *adapter.FileSyst
- fontPath = findFont("Arial", bold, italic, paths)
+ fontPath = tryLoadSystemFontWithBestWeight("Arial", bold, italic, paths)
@@ -69 +47 @@ func GetFontPath(fontName string, bold string, italic bool, fs *adapter.FileSyst
- fontPath = findFont("Andale Mono", bold, italic, paths)
+ fontPath = tryLoadSystemFontWithBestWeight("Andale Mono", bold, italic, paths)
@@ -71,3 +49 @@ func GetFontPath(fontName string, bold string, italic bool, fs *adapter.FileSyst
- fontPath = findFont("Georgia", bold, italic, paths)
- default:
- fontPath = findFont(font, bold, italic, paths)
+ fontPath = tryLoadSystemFontWithBestWeight("Georgia", bold, italic, paths)
@@ -75,0 +52,2 @@ func GetFontPath(fontName string, bold string, italic bool, fs *adapter.FileSyst
+ fmt.Println(fontPath)
+
@@ -77 +54,0 @@ func GetFontPath(fontName string, bold string, italic bool, fs *adapter.FileSyst
- fmt.Println(fontPath)
@@ -82 +59 @@ func GetFontPath(fontName string, bold string, italic bool, fs *adapter.FileSyst
- fmt.Println(findFont("Georgia", bold, italic, paths))
+
@@ -84 +61 @@ func GetFontPath(fontName string, bold string, italic bool, fs *adapter.FileSyst
- return findFont("Georgia", bold, italic, paths)
+ return tryLoadSystemFontWithBestWeight("Georgia", bold, italic, paths)
@@ -87,2 +64,22 @@ func GetFontPath(fontName string, bold string, italic bool, fs *adapter.FileSyst
-func findFont(name string, bold string, italic bool, paths []string) string {
- namePattern := `(?i)\b` + regexp.QuoteMeta(strings.ToLower(name)) + `\b` // Match 'name' as a word, case-insensitive
+func tryLoadSystemFontWithBestWeight(fontName string, desiredWeight int, italic bool, paths []string) string {
+ font := fontName
+ if italic {
+ font += " Italic"
+ }
+ slash := "/"
+
+ if runtime.GOOS == "windows" {
+ slash = "\\"
+ }
+
+ availableWeights := getAvailableWeights(fontName, italic, paths)
+ closestWeight := findClosestWeight(availableWeights, desiredWeight)
+
+ // Get the closest weight name and attempt to load the font
+ weightName := getWeightName(closestWeight)
+ fontWithWeight := fontName + " " + weightName
+ fontWithWeight = strings.TrimSpace(fontWithWeight)
+ if italic {
+ fontWithWeight += " Italic"
+ }
+
@@ -90,35 +87,2 @@ func findFont(name string, bold string, italic bool, paths []string) string {
- fileName := filepath.Base(strings.ToLower(v))
- matched, _ := regexp.MatchString(namePattern, fileName)
- if matched {
- weightName := GetWeightName(bold)
- if bold == "" {
- wns := []string{"thin",
- "extralight",
- "light",
- "medium",
- "semibold",
- "bold",
- "extrabold",
- "black"}
- doesContain := false
- for _, wn := range wns {
- if strings.Contains(fileName, wn) {
- doesContain = true
- }
- }
- if doesContain {
- continue
- }
- }
- if !strings.Contains(fileName, weightName) {
- continue
- }
- if italic {
- if strings.Contains(fileName, "italic") {
- return v
- }
- } else {
- if !strings.Contains(fileName, "italic") {
- return v
- }
- }
+ if strings.Contains(strings.ToLower(v), strings.ToLower(slash+fontWithWeight)) {
+ return v
@@ -126,0 +91 @@ func findFont(name string, bold string, italic bool, paths []string) string {
+
@@ -130 +95,50 @@ func findFont(name string, bold string, italic bool, paths []string) string {
-func GetWeightName(weight string) string {
+func getAvailableWeights(fontName string, italic bool, paths []string) []int {
+ weightMappings := []int{100, 200, 300, 400, 500, 600, 700, 800, 900}
+ availableWeights := []int{}
+
+ for _, weight := range weightMappings {
+ weightName := getWeightName(weight)
+ fontWithWeight := strings.TrimSpace(fontName + " " + weightName)
+ if italic {
+ fontWithWeight += " Italic"
+ }
+ if tryLoadFontExists(fontWithWeight, paths) {
+ availableWeights = append(availableWeights, weight)
+ }
+ }
+
+ return availableWeights
+}
+
+// var allFonts []string
+
+func tryLoadFontExists(fontWithWeight string, paths []string) bool {
+ slash := "/"
+ if runtime.GOOS == "windows" {
+ slash = "\\"
+ }
+
+ for _, v := range paths {
+ if strings.Contains(strings.ToLower(v), strings.ToLower(slash+fontWithWeight)) {
+ return true
+ }
+ }
+
+ return false
+}
+
+func findClosestWeight(availableWeights []int, desiredWeight int) int {
+ if len(availableWeights) == 0 {
+ return 400 // Default to "Regular" if no weights are available
+ }
+
+ closest := availableWeights[0]
+ for _, weight := range availableWeights {
+ if abs(weight-desiredWeight) < abs(closest-desiredWeight) {
+ closest = weight
+ }
+ }
+ return closest
+}
+
+func getWeightName(weight int) string {
@@ -132,7 +146,7 @@ func GetWeightName(weight string) string {
- case "100":
- return "thin"
- case "200":
- return "extralight"
- case "300":
- return "light"
- case "400":
+ case 100:
+ return "Thin"
+ case 200:
+ return "ExtraLight"
+ case 300:
+ return "Light"
+ case 400:
@@ -140,10 +154,10 @@ func GetWeightName(weight string) string {
- case "500":
- return "medium"
- case "600":
- return "semibold"
- case "700":
- return "bold"
- case "800":
- return "extrabold"
- case "900":
- return "black"
+ case 500:
+ return "Medium"
+ case 600:
+ return "SemiBold"
+ case 700:
+ return "Bold"
+ case 800:
+ return "ExtraBold"
+ case 900:
+ return "Black"
@@ -151 +165 @@ func GetWeightName(weight string) string {
- return weight
+ return ""
@@ -155 +169 @@ func GetWeightName(weight string) string {
-func LoadFont(fontName string, fontSize int, bold string, italic bool, fs *adapter.FileSystem) (font.Face, error) {
+func LoadFont(fontName string, fontSize int, bold int, italic bool, fs *adapter.FileSystem) (font.Face, error) {
@@ -174,3 +188,2 @@ func LoadFont(fontName string, fontSize int, bold string, italic bool, fs *adapt
- Size: float64(fontSize),
- // !ISSUE: Play with this
- DPI: 70,
+ Size: float64(fontSize),
+ DPI: 72,
@@ -184,6 +197,13 @@ func LoadFont(fontName string, fontSize int, bold string, italic bool, fs *adapt
-func MeasureText(t *MetaData, text string) (int, int) {
- ctx := canvas.NewCanvas(0, 0)
- ctx.Context.SetFontFace(*t.Font)
- w, h := ctx.MeasureText(text)
- return int(w), int(h)
-}
+func MeasureText(t *element.Text, text string) int {
+ var width fixed.Int26_6
+
+ for _, runeValue := range text {
+ if runeValue == ' ' {
+ // Handle spaces separately, add word spacing
+ width += fixed.I(t.WordSpacing)
+ } else {
+ fnt := *t.Font
+ adv, ok := fnt.GlyphAdvance(runeValue)
+ if !ok {
+ continue
+ }
@@ -191,6 +211,4 @@ func MeasureText(t *MetaData, text string) (int, int) {
-func MeasureSpace(t *MetaData) (int, int) {
- ctx := canvas.NewCanvas(0, 0)
- ctx.Context.SetFontFace(*t.Font)
- w, h := ctx.MeasureText(" ")
- return int(w), int(h)
-}
+ // Update the total width with the glyph advance and bounds
+ width += adv + fixed.I(t.LetterSpacing)
+ }
+ }
@@ -198,4 +216 @@ func MeasureSpace(t *MetaData) (int, int) {
-func Key(text *MetaData) string {
- key := text.Text + utils.RGBAtoString(text.Color) + utils.RGBAtoString(text.DecorationColor) + text.Align + text.WordBreak + strconv.Itoa(text.WordSpacing) + strconv.Itoa(text.LetterSpacing) + text.WhiteSpace + strconv.Itoa(text.DecorationThickness) + strconv.Itoa(text.EM)
- key += strconv.FormatBool(text.Overlined) + strconv.FormatBool(text.Underlined) + strconv.FormatBool(text.LineThrough)
- return key
+ return width.Round()
@@ -204,13 +219,5 @@ func Key(text *MetaData) string {
-func GetMetaData(n *element.Node, state *map[string]element.State, font *font.Face) *MetaData {
- s := *state
- self := s[n.Properties.Id]
- parent := s[n.Parent.Properties.Id]
-
- // self.Textures = []string{}
-
- text := MetaData{}
- text.Font = font
- letterSpacing := utils.ConvertToPixels(n.Style["letter-spacing"], self.EM, parent.Width)
- wordSpacing := utils.ConvertToPixels(n.Style["word-spacing"], self.EM, parent.Width)
- lineHeight := utils.ConvertToPixels(n.Style["line-height"], self.EM, parent.Width)
- underlineoffset := utils.ConvertToPixels(n.Style["text-underline-offset"], self.EM, parent.Width)
+func MeasureSpace(t *element.Text) int {
+ fnt := *t.Font
+ adv, _ := fnt.GlyphAdvance(' ')
+ return adv.Round()
+}
@@ -218,2 +225,3 @@ func GetMetaData(n *element.Node, state *map[string]element.State, font *font.Fa
- if lineHeight == 0 {
- lineHeight = self.EM + 3
+func Render(t *element.Text) (*image.RGBA, int) {
+ if t.LineHeight == 0 {
+ t.LineHeight = t.EM + 3
@@ -222,4 +230 @@ func GetMetaData(n *element.Node, state *map[string]element.State, font *font.Fa
- text.LineHeight = int(lineHeight)
- text.WordSpacing = int(wordSpacing)
- text.LetterSpacing = int(letterSpacing)
- wb := " "
+ width := MeasureText(t, t.Text+" ")
@@ -227,3 +232,2 @@ func GetMetaData(n *element.Node, state *map[string]element.State, font *font.Fa
- if n.Style["word-wrap"] == "break-word" {
- wb = ""
- }
+ // Use fully transparent color for the background
+ img := image.NewRGBA(image.Rect(0, 0, width, t.LineHeight))
@@ -231,3 +235 @@ func GetMetaData(n *element.Node, state *map[string]element.State, font *font.Fa
- if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
- wb = ""
- }
+ // fmt.Println(t.Width, t.LineHeight, (len(lines)))
@@ -235 +237 @@ func GetMetaData(n *element.Node, state *map[string]element.State, font *font.Fa
- var dt float32
+ r, g, b, a := t.Color.RGBA()
@@ -237,4 +239,9 @@ func GetMetaData(n *element.Node, state *map[string]element.State, font *font.Fa
- if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
- dt = self.EM / 7
- } else {
- dt = utils.ConvertToPixels(n.Style["text-decoration-thickness"], self.EM, parent.Width)
+ draw.Draw(img, img.Bounds(), &image.Uniform{color.RGBA{uint8(r), uint8(g), uint8(b), uint8(0)}}, image.Point{}, draw.Over)
+ // fmt.Println(int(t.Font.Metrics().Ascent))
+ dot := fixed.Point26_6{X: fixed.I(0), Y: (fixed.I(t.LineHeight+(t.EM/2)) / 2)}
+
+ dr := &font.Drawer{
+ Dst: img,
+ Src: &image.Uniform{color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)}},
+ Face: *t.Font,
+ Dot: dot,
@@ -243 +250 @@ func GetMetaData(n *element.Node, state *map[string]element.State, font *font.Fa
- col := cc.Parse(n.Style, "font")
+ drawn := drawString(*t, dr, t.Text, width, img)
@@ -245,3 +252,2 @@ func GetMetaData(n *element.Node, state *map[string]element.State, font *font.Fa
- if n.Style["text-decoration-color"] == "" {
- n.Style["text-decoration-color"] = n.Style["color"]
- }
+ return drawn, width
+}
@@ -249,18 +255,10 @@ func GetMetaData(n *element.Node, state *map[string]element.State, font *font.Fa
- text.Color = col
- text.DecorationColor = cc.Parse(n.Style, "decoration")
- text.Align = n.Style["text-align"]
- text.WordBreak = wb
- text.WordSpacing = int(wordSpacing)
- text.LetterSpacing = int(letterSpacing)
- text.WhiteSpace = n.Style["white-space"]
- text.DecorationThickness = int(dt)
- text.Overlined = n.Style["text-decoration"] == "overline"
- text.Underlined = n.Style["text-decoration"] == "underline"
- text.LineThrough = n.Style["text-decoration"] == "line-through"
- text.EM = int(self.EM)
- text.Width = int(parent.Width)
- text.Text = n.InnerText
- text.UnderlineOffset = int(underlineoffset)
-
- if n.Style["text-underline-offset"] == "" {
- text.UnderlineOffset = 2
+func drawString(t element.Text, dr *font.Drawer, v string, lineWidth int, img *image.RGBA) *image.RGBA {
+ underlinePosition := dr.Dot
+ for _, ch := range v {
+ if ch == ' ' {
+ // Handle spaces separately, add word spacing
+ dr.Dot.X += fixed.I(t.WordSpacing)
+ } else {
+ dr.DrawString(string(ch))
+ dr.Dot.X += fixed.I(t.LetterSpacing)
+ }
@@ -268,4 +266,19 @@ func GetMetaData(n *element.Node, state *map[string]element.State, font *font.Fa
-
- if n.Style["word-spacing"] == "" {
- // !ISSUE: is word spacing actually impleamented
- text.WordSpacing, _ = MeasureSpace(&text)
+ if t.Underlined || t.Overlined || t.LineThrough {
+
+ underlinePosition.X = 0
+ baseLineY := underlinePosition.Y
+ fnt := *t.Font
+ descent := fnt.Metrics().Descent
+ if t.Underlined {
+ underlinePosition.Y = baseLineY + descent
+ underlinePosition.Y = (underlinePosition.Y / 100) * 97
+ drawLine(img, underlinePosition, fixed.Int26_6(lineWidth), t.DecorationThickness, t.DecorationColor)
+ }
+ if t.LineThrough {
+ underlinePosition.Y = baseLineY - (descent)
+ drawLine(img, underlinePosition, fixed.Int26_6(lineWidth), t.DecorationThickness, t.DecorationColor)
+ }
+ if t.Overlined {
+ underlinePosition.Y = baseLineY - descent*3
+ drawLine(img, underlinePosition, fixed.Int26_6(lineWidth), t.DecorationThickness, t.DecorationColor)
+ }
@@ -273 +286 @@ func GetMetaData(n *element.Node, state *map[string]element.State, font *font.Fa
- return &text
+ return img
@@ -276,4 +289,14 @@ func GetMetaData(n *element.Node, state *map[string]element.State, font *font.Fa
-func Render(text *MetaData) (*image.RGBA, int) {
- // var data *image.RGBA
- if text.LineHeight == 0 {
- text.LineHeight = text.EM + 3
+func drawLine(img draw.Image, start fixed.Point26_6, width fixed.Int26_6, thickness int, col color.Color) {
+ // Bresenham's line algorithm
+ x0, y0 := start.X.Round(), start.Y.Round()
+ x1 := x0 + int(width)
+ y1 := y0
+ dx := abs(x1 - x0)
+ dy := abs(y1 - y0)
+ sx, sy := 1, 1
+
+ if x0 > x1 {
+ sx = -1
+ }
+ if y0 > y1 {
+ sy = -1
@@ -282,5 +305 @@ func Render(text *MetaData) (*image.RGBA, int) {
- width, _ := MeasureText(text, text.Text+" ")
-
- ctx := canvas.NewCanvas(width, text.LineHeight)
- ctx.Context.Clear()
- r, g, b, a := text.Color.RGBA()
+ err := dx - dy
@@ -288,3 +307,4 @@ func Render(text *MetaData) (*image.RGBA, int) {
- ctx.SetFillStyle(uint8(r), uint8(g), uint8(b), uint8(a))
- ctx.Context.SetFontFace(*text.Font)
- ctx.Context.DrawStringAnchored(text.Text, 0, float64(text.LineHeight)/2, 0, 0.3)
+ for {
+ for i := 0; i < thickness; i++ {
+ img.Set(x0, (y0-(thickness/2))+i, col)
+ }
@@ -292,8 +312,2 @@ func Render(text *MetaData) (*image.RGBA, int) {
- if text.Underlined || text.Overlined || text.LineThrough {
- ctx.SetLineWidth(float64(text.DecorationThickness))
- r, g, b, a = text.DecorationColor.RGBA()
- ctx.SetStrokeStyle(uint8(r), uint8(g), uint8(b), uint8(a))
- ctx.BeginPath()
- var y float64
- if text.Underlined {
- y = (float64(text.LineHeight) / 2) + (float64(text.EM) / 2.5) + float64(text.UnderlineOffset)
+ if x0 == x1 && y0 == y1 {
+ break
@@ -301,2 +315,5 @@ func Render(text *MetaData) (*image.RGBA, int) {
- if text.LineThrough {
- y = (float64(text.LineHeight) / 2)
+
+ e2 := 2 * err
+ if e2 > -dy {
+ err -= dy
+ x0 += sx
@@ -304,2 +321,3 @@ func Render(text *MetaData) (*image.RGBA, int) {
- if text.Overlined {
- y = (float64(text.LineHeight) / 2) - (float64(text.EM) / 2) - (float64(text.DecorationThickness) / 2)
+ if e2 < dx {
+ err += dx
+ y0 += sy
@@ -307,3 +324,0 @@ func Render(text *MetaData) (*image.RGBA, int) {
- ctx.MoveTo(0, y)
- ctx.LineTo(float64(width), y)
- ctx.Stroke()
@@ -311 +326,15 @@ func Render(text *MetaData) (*image.RGBA, int) {
- return ctx.RGBA, width
+}
+
+func abs(x int) int {
+ if x < 0 {
+ return -x
+ }
+ return x
+}
+
+func Min(a, b float32) float32 {
+ if a < b {
+ return a
+ } else {
+ return b
+ }