diff --git a/go.mod b/go.mod index ccaff6d..ece40dd 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/unix-streamdeck/gg v0.0.0-20260313120600-9d60d38ce9f9 go.uber.org/mock v0.6.0 - golang.org/x/image v0.37.0 + golang.org/x/image v0.38.0 ) require ( diff --git a/go.sum b/go.sum index 23e2616..701f6da 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA= golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= +golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE= +golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/img.go b/img.go index a3da89e..9bcb6d1 100644 --- a/img.go +++ b/img.go @@ -26,11 +26,11 @@ import ( "golang.org/x/image/font/gofont/goregular" "golang.org/x/image/font/gofont/gosmallcaps" "golang.org/x/image/font/gofont/gosmallcapsitalic" + "golang.org/x/image/math/fixed" ) const BorderClearance = 10 -// TODO replace use of gg with native font.Drawer type VerticalAlignment string const ( @@ -39,11 +39,30 @@ const ( Bottom VerticalAlignment = "BOTTOM" ) +type HorizontalAlignment string + +const ( + Left HorizontalAlignment = "LEFT" + Middle HorizontalAlignment = "MIDDLE" + Right HorizontalAlignment = "RIGHT" +) + +type Overflow string + +const ( + Wrap Overflow = "WRAP" + Fade Overflow = "FADE" +) + type DrawTextOptions struct { - FontSize int64 - VerticalAlignment VerticalAlignment - FontFace string - Colour string + FontSize int64 + VerticalAlignment VerticalAlignment + HorizontalAlignment HorizontalAlignment + FontFace string + Colour string + Overflow Overflow + // With anchor set, the alignments indicate the position of the text the anchor will be in, e.g BOTTOM & RIGHT will put the anchor in the bottom right of the text, so all text will be up and left of it + Anchor *image.Point } type IContext interface { @@ -59,22 +78,45 @@ type IContext interface { } func DrawText(img image.Image, text string, options DrawTextOptions) (image.Image, error) { - ggImg := gg.NewContextForImage(img) - return drawText(ggImg, text, options) -} -func drawText(img IContext, text string, options DrawTextOptions) (image.Image, error) { - width, height := img.Width(), img.Height() - img.SetRGB(1, 1, 1) + if options.Overflow == "" { + options.Overflow = Wrap + } + + drawImg, ok := img.(draw.Image) + + if !ok { + return img, errors.New("cannot convert") + } + + width, height := img.Bounds().Dx(), img.Bounds().Dy() + + availableWidth, availableHeight := width, height + + if options.Anchor != nil { + availableWidth, availableHeight = width-options.Anchor.X, height-options.Anchor.Y + + if options.VerticalAlignment == Center { + availableHeight = height - options.Anchor.Y + } + + if options.HorizontalAlignment == Middle { + availableWidth = width - options.Anchor.X + } + } + + col := color.RGBA{0xff, 0xff, 0xff, 0xff} + + //img.SetRGB(1, 1, 1) matched, _ := regexp.MatchString(`#?([0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3})`, options.Colour) if matched { - img.SetHexColor(options.Colour) + col = HexColor(options.Colour) } f, err := truetype.Parse(loadFontFace(options.FontFace)) if err != nil { return nil, err } - fSize := calculateFontSize(f, text, img) + fSize := calculateFontSize(f, text, availableWidth, availableHeight, options.Overflow) if options.FontSize != 0 { fSize = float64(options.FontSize) @@ -82,18 +124,69 @@ func drawText(img IContext, text string, options DrawTextOptions) (image.Image, face := truetype.NewFace(f, &truetype.Options{Size: fSize}) defer face.Close() - img.SetFontFace(face) - lines := img.WordWrap(text, float64(width-BorderClearance)) - lineCount := float64(len(lines)) + lines := text + + if options.Overflow == Wrap { + lines = wrapString(text, availableWidth, face) + } + + lineCount := strings.Count(lines, "\n") + 1 + + var y float64 + + var x int + + w, _ := getTextBounds(lines, face) + + if options.Anchor != nil { + x = calculateHorizonalAlignment(options.HorizontalAlignment, int(w), options.Anchor.X, true) + y = calculateVerticalAlignment(options.VerticalAlignment, options.Anchor.Y, lineCount, fSize, true) + } else { + x = calculateHorizonalAlignment(options.HorizontalAlignment, int(w), width, false) + y = calculateVerticalAlignment(options.VerticalAlignment, height, lineCount, fSize, false) + } + + d := &font.Drawer{ + Dst: drawImg, + Src: image.NewUniform(col), + Face: face, + } + + d.Dot = fixed.Point26_6{ + X: fixed.I(x), + Y: fixed.I(int(y) + (int(fSize) / 4)), + } + + linesSplit := strings.Split(lines, "\n") - if strings.Contains(text, "\n") { - lineCount += float64(strings.Count(text, "\n") + 1) + if len(linesSplit) == 1 { + d.DrawString(lines) + return img, nil } - valign, y := calculateVerticalAlignment(options.VerticalAlignment, height) - img.DrawStringWrapped(text, float64(width/2), y, 0.5, valign, float64(width-BorderClearance), 1, gg.AlignCenter) - return img.Image(), nil + linesAbove := float64(lineCount) / 2 + + linesAbove = linesAbove - 1 + + startingLineY := y - (linesAbove * fSize) + + for i, line := range linesSplit { + w, _ := getTextBounds(line, face) + + if options.Anchor != nil { + x = calculateHorizonalAlignment(options.HorizontalAlignment, int(w), options.Anchor.X, true) + } else { + x = calculateHorizonalAlignment(options.HorizontalAlignment, int(w), width, false) + } + + d.Dot = fixed.Point26_6{ + X: fixed.I(x), + Y: fixed.I(int(startingLineY + (float64(i) * fSize))), + } + d.DrawString(line) + } + return img, nil } // TODO Support loading fonts via fontconfig on linux and whatever the equivalent is on darwin @@ -128,29 +221,60 @@ func loadFontFace(fontName string) []byte { } } -func calculateVerticalAlignment(alignment VerticalAlignment, height int) (float64, float64) { +func calculateVerticalAlignment(alignment VerticalAlignment, height, lines int, fSize float64, anchor bool) float64 { + textMidPoint := (float64(lines) / 2.0) * fSize + if !anchor { + if alignment == Top { + return (BorderClearance / 2) + (textMidPoint) + } + if alignment == Bottom { + return float64(height) - (BorderClearance / 2) - textMidPoint + } + return float64(height) / 2 + } + if alignment == Top { - return 0.0, BorderClearance / 2 + return float64(height) + textMidPoint } if alignment == Bottom { - return 1.0, float64(height) - (BorderClearance / 2) + return float64(height) - textMidPoint + } + return float64(height) +} + +func calculateHorizonalAlignment(alignment HorizontalAlignment, textWidth, width int, anchor bool) int { + if !anchor { + if alignment == Left { + return BorderClearance / 2 + } + if alignment == Right { + return width - (BorderClearance / 2) - textWidth + } + return ((width) / 2) - (int(textWidth) / 2) } - return 0.5, float64(height) / 2 + + if alignment == Left { + return width + } + + if alignment == Right { + return width - textWidth + } + + return width - (textWidth / 2) } -func calculateFontSize(f *truetype.Font, text string, img IContext) float64 { - width := img.Width() +func calculateFontSize(f *truetype.Font, text string, width, height int, overflow Overflow) float64 { fontSize := float64(width) / 3 face := truetype.NewFace(f, &truetype.Options{Size: fontSize}) defer face.Close() - img.SetFontFace(face) - textWidth, _ := img.MeasureMultilineString(text, 1.0) + w, _ := getTextBounds(text, face) fSize := fontSize - if textWidth >= float64(width-BorderClearance) { - oversizeRatio := float64(width-BorderClearance) / textWidth + if w >= float64(width-BorderClearance) { + oversizeRatio := float64(width-BorderClearance) / w scaledFontSize := math.Min(oversizeRatio*fontSize, 12) for size := fontSize; size >= scaledFontSize; size -= 0.5 { - if attemptFontSize(f, text, img, size) { + if attemptFontSize(f, text, width, height, size, overflow) { return size } } @@ -159,16 +283,69 @@ func calculateFontSize(f *truetype.Font, text string, img IContext) float64 { return fSize } -func attemptFontSize(f *truetype.Font, text string, img IContext, fSize float64) bool { - width := img.Width() - height := img.Height() +func attemptFontSize(f *truetype.Font, text string, width, height int, fSize float64, overflow Overflow) bool { face := truetype.NewFace(f, &truetype.Options{Size: fSize}) defer face.Close() - img.SetFontFace(face) - wrappedGroups := img.WordWrap(text, float64(width-BorderClearance)) - wrappedText := strings.Join(wrappedGroups, "\n") - textWidth, textHeight := img.MeasureMultilineString(wrappedText, 1.0) - return textHeight < float64(height-BorderClearance) && textWidth < float64(width-BorderClearance) + w, h := getTextBounds(text, face) + if w <= float64(width-BorderClearance) { + return true + } + if h > float64(height) { + return false + } + if overflow != Wrap { + return false + } + lines := wrapString(text, width, face) + if lines == "" { + return false + } + maxTextWidth := 0.0 + for _, s := range strings.Split(lines, "\n") { + textWidth, _ := getTextBounds(s, face) + if textWidth > maxTextWidth { + maxTextWidth = textWidth + } + } + textHeight := float64(strings.Count(lines, "\n")+1) * fSize + if textHeight < float64(height-BorderClearance) && maxTextWidth < float64(width-BorderClearance) { + return true + } + return false +} + +func wrapString(text string, width int, face font.Face) string { + splitMessage := strings.Split(text, " ") + if len(splitMessage) == 1 { + return text + } + + var lines []string + nextWordIndex := 0 + for nextWordIndex < len(splitMessage) { + lineLength := 0.0 + var line []string + for lineLength <= float64(width-BorderClearance) && nextWordIndex < len(splitMessage) { + w, _ := getTextBounds(splitMessage[nextWordIndex], face) + if w > float64(width-BorderClearance) { + return "" + } + if w+lineLength > float64(width-BorderClearance) { + break + } + lineLength += w + line = append(line, splitMessage[nextWordIndex]) + nextWordIndex += 1 + } + lines = append(lines, strings.Join(line, " ")) + } + return strings.Join(lines, "\n") +} + +func getTextBounds(text string, face font.Face) (float64, float64) { + bounds, _ := font.BoundString(face, text) + + return (float64(bounds.Max.X.Round()) - float64(bounds.Min.X.Round())), (float64(bounds.Max.Y.Round()) - float64(bounds.Min.Y.Round())) } func ResizeImage(img image.Image, keySize int) image.Image { @@ -189,16 +366,9 @@ func DrawProgressBar(img image.Image, label string, x, y, h, w, progress float64 } func DrawProgressBarWithAccent(img image.Image, label string, x, y, h, w, progress float64, hex string) (image.Image, error) { - ggImg := gg.NewContextForImage(img) - - f, err := truetype.Parse(goregular.TTF) + var err error - if err != nil { - return nil, err - } - - face := truetype.NewFace(f, &truetype.Options{Size: h / 2}) - defer face.Close() + ggImg := gg.NewContextForImage(img) ggImg.SetFillRule(gg.FillRuleEvenOdd) @@ -216,9 +386,19 @@ func DrawProgressBarWithAccent(img image.Image, label string, x, y, h, w, progre ggImg.SetHexColor("#FFFFFF") - ggImg.DrawStringAnchored(label, (x+w)/2, y+(h/2), 0.5, 0.5) + img = ggImg.Image() + + if label != "" { + img, err = DrawText(img, label, DrawTextOptions{ + Anchor: &image.Point{ + X: int(x) + int(w/2), + Y: int(y) + int((h/2)*1.2), + }, + FontSize: int64(math.Max(h/2, h-5)), + }) + } - return ggImg.Image(), nil + return img, err } func HexColor(hex string) color.RGBA { diff --git a/img_test.go b/img_test.go index 11c2d23..83455ed 100644 --- a/img_test.go +++ b/img_test.go @@ -6,13 +6,12 @@ import ( "image/draw" "log" "math/rand" + "os" "testing" "github.com/golang/freetype/truetype" "github.com/stretchr/testify/assert" - "github.com/unix-streamdeck/api/v2/mocks/mock_api" "github.com/unix-streamdeck/gg" - "go.uber.org/mock/gomock" "golang.org/x/image/font/gofont/gobold" "golang.org/x/image/font/gofont/gobolditalic" "golang.org/x/image/font/gofont/goitalic" @@ -27,56 +26,22 @@ import ( "golang.org/x/image/font/gofont/gosmallcapsitalic" ) -func TestDrawText(t *testing.T) { - assertions := assert.New(t) - ctrl := gomock.NewController(t) - - context := mock_api.NewMockIContext(ctrl) - - context.EXPECT().SetRGB(1.0, 1.0, 1.0).Times(1) - - context.EXPECT().Width().Return(72).Times(2) - context.EXPECT().Height().Return(72).Times(1) - - context.EXPECT().SetFontFace(gomock.Any()).Times(2) - - context.EXPECT().MeasureMultilineString("Test", 1.0).Return(20.0, 24.0).Times(1) - - context.EXPECT().WordWrap("Test", 62.0).Return([]string{"Test"}).Times(1) - - context.EXPECT().DrawStringWrapped("Test", 36.0, 36.0, 0.5, 0.5, 62.0, 1.0, gg.AlignCenter) - - mockImg := setupImage(72, 72) - - context.EXPECT().Image().Return(mockImg).Times(1) - - img, err := drawText(context, "Test", DrawTextOptions{}) - - assertions.Equal(mockImg, img) - - assertions.Nil(err) - -} - func Test_calculateVerticalAlignment_Center(t *testing.T) { assertions := assert.New(t) - alignment, anchor := calculateVerticalAlignment(Center, 80) - assertions.Equal(0.5, alignment) + anchor := calculateVerticalAlignment(Center, 80, 1, 15, false) assertions.Equal(40.0, anchor) } func Test_calculateVerticalAlignment_Top(t *testing.T) { assertions := assert.New(t) - alignment, anchor := calculateVerticalAlignment(Top, 80) - assertions.Equal(0.0, alignment) - assertions.Equal(5.0, anchor) + anchor := calculateVerticalAlignment(Top, 80, 1, 15, false) + assertions.Equal(12.5, anchor) } func Test_calculateVerticalAlignment_Bottom(t *testing.T) { assertions := assert.New(t) - alignment, anchor := calculateVerticalAlignment(Bottom, 80) - assertions.Equal(1.0, alignment) - assertions.Equal(75.0, anchor) + anchor := calculateVerticalAlignment(Bottom, 80, 1, 15, false) + assertions.Equal(67.5, anchor) } func Test_calculateFontSize_SingleWord(t *testing.T) { @@ -85,7 +50,7 @@ func Test_calculateFontSize_SingleWord(t *testing.T) { ggImg := gg.NewContextForImage(img) ggImg.SetHexColor("#FFF") f, _ := truetype.Parse(goregular.TTF) - assertions.Equal(24.0, calculateFontSize(f, "Test", ggImg)) + assertions.Equal(24.0, calculateFontSize(f, "Test", 72, 72, Wrap)) } func Test_calculateFontSize_MultiLine(t *testing.T) { @@ -94,7 +59,7 @@ func Test_calculateFontSize_MultiLine(t *testing.T) { ggImg := gg.NewContextForImage(img) ggImg.SetHexColor("#FFF") f, _ := truetype.Parse(goregular.TTF) - assertions.Equal(24.0, calculateFontSize(f, "Lines Test", ggImg)) + assertions.Equal(24.0, calculateFontSize(f, "Lines Test", 72, 72, Wrap)) } func Test_calculateFontSize_LongMultiLine(t *testing.T) { @@ -103,7 +68,7 @@ func Test_calculateFontSize_LongMultiLine(t *testing.T) { ggImg := gg.NewContextForImage(img) ggImg.SetHexColor("#FFF") f, _ := truetype.Parse(goregular.TTF) - assertions.Equal(15.5, calculateFontSize(f, "Multiline Overflow", ggImg)) + assertions.Equal(15.5, calculateFontSize(f, "Multiline Overflow", 72, 72, Wrap)) } func Test_attemptFontSize_SingleLine(t *testing.T) { @@ -112,7 +77,7 @@ func Test_attemptFontSize_SingleLine(t *testing.T) { ggImg := gg.NewContextForImage(img) ggImg.SetHexColor("#FFF") f, _ := truetype.Parse(goregular.TTF) - assertions.True(attemptFontSize(f, "Test", ggImg, 24.0)) + assertions.True(attemptFontSize(f, "Test", 72, 72, 24.0, Wrap)) } func Test_attemptFontSize_MultiLine(t *testing.T) { @@ -121,7 +86,7 @@ func Test_attemptFontSize_MultiLine(t *testing.T) { ggImg := gg.NewContextForImage(img) ggImg.SetHexColor("#FFF") f, _ := truetype.Parse(goregular.TTF) - assertions.True(attemptFontSize(f, "Lines Test", ggImg, 24.0)) + assertions.True(attemptFontSize(f, "Lines Test", 72, 72, 24.0, Wrap)) } func Test_attemptFontSize_Overflow(t *testing.T) { @@ -130,7 +95,7 @@ func Test_attemptFontSize_Overflow(t *testing.T) { ggImg := gg.NewContextForImage(img) ggImg.SetHexColor("#FFF") f, _ := truetype.Parse(goregular.TTF) - assertions.False(attemptFontSize(f, "Muiltiline Overflow", ggImg, 24.0)) + assertions.False(attemptFontSize(f, "Muiltiline Overflow", 72, 72, 24.0, Wrap)) } func TestResizeImage(t *testing.T) { @@ -156,92 +121,138 @@ func TestResizeImageWH(t *testing.T) { assertions.Equal(resizedImage.Bounds().Max.Y, newHeight) } -func TestDrawText_WithColor(t *testing.T) { - assertions := assert.New(t) - ctrl := gomock.NewController(t) - - context := mock_api.NewMockIContext(ctrl) - - context.EXPECT().SetRGB(1.0, 1.0, 1.0).Times(1) - context.EXPECT().SetHexColor("#FF0000").Times(1) - - context.EXPECT().Width().Return(72).Times(2) - context.EXPECT().Height().Return(72).Times(1) - - context.EXPECT().SetFontFace(gomock.Any()).Times(2) - - context.EXPECT().MeasureMultilineString("Test", 1.0).Return(20.0, 24.0).Times(1) - - context.EXPECT().WordWrap("Test", 62.0).Return([]string{"Test"}).Times(1) - - context.EXPECT().DrawStringWrapped("Test", 36.0, 36.0, 0.5, 0.5, 62.0, 1.0, gg.AlignCenter) - - mockImg := setupImage(72, 72) - - context.EXPECT().Image().Return(mockImg).Times(1) - - img, err := drawText(context, "Test", DrawTextOptions{Colour: "#FF0000"}) - - assertions.Equal(mockImg, img) - assertions.Nil(err) +type TestParameters struct { + DrawTextOptions DrawTextOptions + ExpectedImage string + Name string + Text string } -func TestDrawText_WithFontSize(t *testing.T) { - assertions := assert.New(t) - ctrl := gomock.NewController(t) - - context := mock_api.NewMockIContext(ctrl) - - context.EXPECT().SetRGB(1.0, 1.0, 1.0).Times(1) - - context.EXPECT().Width().Return(72).Times(2) - context.EXPECT().Height().Return(72).Times(1) - - context.EXPECT().SetFontFace(gomock.Any()).Times(2) - - context.EXPECT().MeasureMultilineString("Test", 1.0).Return(25.0, 12.0).Times(1) - - context.EXPECT().WordWrap("Test", 62.0).Return([]string{"Test"}).Times(1) - - context.EXPECT().DrawStringWrapped("Test", 36.0, 36.0, 0.5, 0.5, 62.0, 1.0, gg.AlignCenter) - - mockImg := setupImage(72, 72) - - context.EXPECT().Image().Return(mockImg).Times(1) - - img, err := drawText(context, "Test", DrawTextOptions{FontSize: 16}) - - assertions.Equal(mockImg, img) - assertions.Nil(err) -} - -func TestDrawText_WithNewlines(t *testing.T) { - assertions := assert.New(t) - ctrl := gomock.NewController(t) - - context := mock_api.NewMockIContext(ctrl) - - context.EXPECT().SetRGB(1.0, 1.0, 1.0).Times(1) - - context.EXPECT().Width().Return(72).Times(2) - context.EXPECT().Height().Return(72).Times(1) - - context.EXPECT().SetFontFace(gomock.Any()).Times(2) - - context.EXPECT().MeasureMultilineString("Line1\nLine2", 1.0).Return(30.0, 48.0).Times(1) - - context.EXPECT().WordWrap("Line1\nLine2", 62.0).Return([]string{"Line1", "Line2"}).Times(1) - - context.EXPECT().DrawStringWrapped("Line1\nLine2", 36.0, 36.0, 0.5, 0.5, 62.0, 1.0, gg.AlignCenter) - - mockImg := setupImage(72, 72) - - context.EXPECT().Image().Return(mockImg).Times(1) - - img, err := drawText(context, "Line1\nLine2", DrawTextOptions{}) - - assertions.Equal(mockImg, img) - assertions.Nil(err) +func TestDrawText(t *testing.T) { + params := []TestParameters{ + { + Name: "No Params", + ExpectedImage: "normal.png", + Text: "00:00:00", + DrawTextOptions: DrawTextOptions{}, + }, + { + Name: "Colour", + ExpectedImage: "colour.png", + Text: "00:00:00", + DrawTextOptions: DrawTextOptions{ + Colour: "#CC3333", + }, + }, + { + Name: "Face", + ExpectedImage: "face.png", + Text: "Test", + DrawTextOptions: DrawTextOptions{ + FontFace: "mono", + }, + }, + { + Name: "Fade", + ExpectedImage: "fade.png", + Text: "Test", + DrawTextOptions: DrawTextOptions{ + Overflow: Fade, + }, + }, + { + Name: "Left", + ExpectedImage: "left.png", + Text: "Test", + DrawTextOptions: DrawTextOptions{ + HorizontalAlignment: Left, + }, + }, + { + Name: "Right", + ExpectedImage: "right.png", + Text: "Test", + DrawTextOptions: DrawTextOptions{ + HorizontalAlignment: Right, + }, + }, + { + Name: "Size", + ExpectedImage: "size.png", + Text: "00:00:00", + DrawTextOptions: DrawTextOptions{ + FontSize: 12, + }, + }, + { + Name: "Bottom", + ExpectedImage: "bottom.png", + Text: "00:00:00", + DrawTextOptions: DrawTextOptions{ + VerticalAlignment: Bottom, + }, + }, + { + Name: "Top", + ExpectedImage: "top.png", + Text: "00:00:00", + DrawTextOptions: DrawTextOptions{ + VerticalAlignment: Top, + }, + }, + { + Name: "Multiline Bottom", + ExpectedImage: "ml-bottom.png", + Text: "00:0\n0:00", + DrawTextOptions: DrawTextOptions{ + VerticalAlignment: Bottom, + }, + }, + { + Name: "Multiline Top", + ExpectedImage: "ml-top.png", + Text: "00:0\n0:00", + DrawTextOptions: DrawTextOptions{ + VerticalAlignment: Top, + }, + }, + } + + for _, param := range params { + t.Run(param.Name, func(t *testing.T) { + assertions := assert.New(t) + img := setupImage(72, 72) + img2, err := DrawText(img, param.Text, param.DrawTextOptions) + assertions.Nil(err) + f, err := os.Open("test_resources/" + param.ExpectedImage) + defer f.Close() + expected, _, err := image.Decode(f) + assertions.Equal(expected, img2) + }) + } +} + +func TestDrawText_Anchor(t *testing.T) { + for _, valign := range []VerticalAlignment{Top, Center, Bottom} { + for _, halign := range []HorizontalAlignment{Left, Middle, Right} { + t.Run(string(valign)+"-"+string(halign), func(t *testing.T) { + assertions := assert.New(t) + img := image.NewRGBA(image.Rect(0, 0, 72, 72)) + draw.Draw(img, img.Bounds(), image.Black, image.ZP, draw.Src) + actual, _ := DrawText(img, "Hi", DrawTextOptions{ + FontSize: 15, + Anchor: &image.Point{X: 20, Y: 20}, + Overflow: Fade, + VerticalAlignment: valign, + HorizontalAlignment: halign, + }) + f, _ := os.Open("test_resources/anchor/" + string(halign) + "-" + string(valign) + ".png") + defer f.Close() + expected, _, _ := image.Decode(f) + assertions.Equal(expected, actual) + }) + } + } } func Test_loadFontFace_Bold(t *testing.T) { @@ -411,6 +422,7 @@ func TestSubImage(t *testing.T) { assertions.Equal(50, result.Bounds().Max.X) assertions.Equal(50, result.Bounds().Max.Y) } + func setupImage(width int, height int) *image.RGBA { img := image.NewRGBA(image.Rect(0, 0, width, height)) draw.Draw(img, img.Bounds(), image.Black, image.ZP, draw.Src) diff --git a/test_resources/anchor/LEFT-BOTTOM.png b/test_resources/anchor/LEFT-BOTTOM.png new file mode 100644 index 0000000..4e783fc Binary files /dev/null and b/test_resources/anchor/LEFT-BOTTOM.png differ diff --git a/test_resources/anchor/LEFT-CENTER.png b/test_resources/anchor/LEFT-CENTER.png new file mode 100644 index 0000000..48fc84d Binary files /dev/null and b/test_resources/anchor/LEFT-CENTER.png differ diff --git a/test_resources/anchor/LEFT-TOP.png b/test_resources/anchor/LEFT-TOP.png new file mode 100644 index 0000000..e084999 Binary files /dev/null and b/test_resources/anchor/LEFT-TOP.png differ diff --git a/test_resources/anchor/MIDDLE-BOTTOM.png b/test_resources/anchor/MIDDLE-BOTTOM.png new file mode 100644 index 0000000..d6497ca Binary files /dev/null and b/test_resources/anchor/MIDDLE-BOTTOM.png differ diff --git a/test_resources/anchor/MIDDLE-CENTER.png b/test_resources/anchor/MIDDLE-CENTER.png new file mode 100644 index 0000000..ad534c0 Binary files /dev/null and b/test_resources/anchor/MIDDLE-CENTER.png differ diff --git a/test_resources/anchor/MIDDLE-TOP.png b/test_resources/anchor/MIDDLE-TOP.png new file mode 100644 index 0000000..069ac6e Binary files /dev/null and b/test_resources/anchor/MIDDLE-TOP.png differ diff --git a/test_resources/anchor/RIGHT-BOTTOM.png b/test_resources/anchor/RIGHT-BOTTOM.png new file mode 100644 index 0000000..d22eb4d Binary files /dev/null and b/test_resources/anchor/RIGHT-BOTTOM.png differ diff --git a/test_resources/anchor/RIGHT-CENTER.png b/test_resources/anchor/RIGHT-CENTER.png new file mode 100644 index 0000000..2fa86e3 Binary files /dev/null and b/test_resources/anchor/RIGHT-CENTER.png differ diff --git a/test_resources/anchor/RIGHT-TOP.png b/test_resources/anchor/RIGHT-TOP.png new file mode 100644 index 0000000..4779dde Binary files /dev/null and b/test_resources/anchor/RIGHT-TOP.png differ diff --git a/test_resources/bottom.png b/test_resources/bottom.png new file mode 100644 index 0000000..eddcb34 Binary files /dev/null and b/test_resources/bottom.png differ diff --git a/test_resources/colour.png b/test_resources/colour.png new file mode 100644 index 0000000..a4c8b09 Binary files /dev/null and b/test_resources/colour.png differ diff --git a/test_resources/face.png b/test_resources/face.png new file mode 100644 index 0000000..9752fe8 Binary files /dev/null and b/test_resources/face.png differ diff --git a/test_resources/fade.png b/test_resources/fade.png new file mode 100644 index 0000000..f7d2c5e Binary files /dev/null and b/test_resources/fade.png differ diff --git a/test_resources/left.png b/test_resources/left.png new file mode 100644 index 0000000..04841a5 Binary files /dev/null and b/test_resources/left.png differ diff --git a/test_resources/ml-bottom.png b/test_resources/ml-bottom.png new file mode 100644 index 0000000..c45e5d3 Binary files /dev/null and b/test_resources/ml-bottom.png differ diff --git a/test_resources/ml-top.png b/test_resources/ml-top.png new file mode 100644 index 0000000..9e71d0c Binary files /dev/null and b/test_resources/ml-top.png differ diff --git a/test_resources/normal.png b/test_resources/normal.png new file mode 100644 index 0000000..44d4926 Binary files /dev/null and b/test_resources/normal.png differ diff --git a/test_resources/right.png b/test_resources/right.png new file mode 100644 index 0000000..2dd40bb Binary files /dev/null and b/test_resources/right.png differ diff --git a/test_resources/size.png b/test_resources/size.png new file mode 100644 index 0000000..9afe10f Binary files /dev/null and b/test_resources/size.png differ diff --git a/test_resources/top.png b/test_resources/top.png new file mode 100644 index 0000000..57deb62 Binary files /dev/null and b/test_resources/top.png differ diff --git a/vendor/modules.txt b/vendor/modules.txt index 056784d..ab3966d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -24,7 +24,7 @@ github.com/unix-streamdeck/gg # go.uber.org/mock v0.6.0 ## explicit; go 1.23.0 go.uber.org/mock/gomock -# golang.org/x/image v0.37.0 +# golang.org/x/image v0.38.0 ## explicit; go 1.25.0 golang.org/x/image/draw golang.org/x/image/font