caption/line_test.go
Yonah 6a2822f2fb
line: optional bounds check
This has significant overhead so is made optional.
2025-07-13 03:21:20 +09:00

183 lines
5.3 KiB
Go

package caption_test
import (
"crypto/sha1"
"encoding/hex"
"errors"
"image"
"image/color"
"image/png"
"runtime"
"syscall"
"testing"
"git.gensokyo.uk/yonah/caption"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font/gofont/gobolditalic"
"golang.org/x/image/font/gofont/gomono"
)
var (
beforeCheck []func(t *testing.T, name string, redraw bool, got image.Image)
)
func TestRender(t *testing.T) {
testCases := []struct {
name string
line *caption.Line
ctx *caption.Context
x, y int
want string
wantErr error
}{
{"bad line font", &caption.Line{Font: []byte{0xfd}}, nil,
0, 0, "", truetype.FormatError("TTF data is too short")},
{"bad fast path font", &caption.Line{}, caption.NewContext([]byte{0xfd}, nil),
0, 0, "", truetype.FormatError("TTF data is too short")},
{"out of bounds top left", &caption.Line{
Data: []caption.Segment{{"Beispieltext", color.Black}},
}, nil, 0, 0, "", syscall.EDOM},
{"out of bounds right", &caption.Line{
Data: []caption.Segment{{"Beispieltext", color.Black}},
}, nil, 1<<10 - 0x3e, 1 << 8, "", syscall.EDOM},
{"right edge", &caption.Line{
Data: []caption.Segment{{"Beispieltext", color.Black}},
}, nil, 1<<10 - 0x3f, 1 << 8, "8037459938bcaa6c427412be2ec1e8e77f8ff0e4", nil},
{"custom line options", &caption.Line{
Data: []caption.Segment{{"Beispieltext", color.Black}},
Options: &truetype.Options{Size: 24},
}, nil,
0, 1 << 8, "0f4077326dc233331c00ee30b06ba76e2ad3a09a", nil},
{"custom line font", &caption.Line{
Data: []caption.Segment{{"Beispieltext", color.Black}},
Font: gomono.TTF,
}, nil,
0, 1 << 8, "af983600732f6c78a6123e1dc1033cdbab9e06b5", nil},
{"custom fast path font", &caption.Line{
Data: []caption.Segment{{"Beispieltext", color.Black}},
}, caption.NewContext(gomono.TTF, nil),
0, 1 << 8, "af983600732f6c78a6123e1dc1033cdbab9e06b5", nil},
{"alternate custom line font", &caption.Line{
Data: []caption.Segment{{"Beispieltext", color.Black}},
Font: gobolditalic.TTF,
}, nil,
0, 1 << 8, "2371b3458a12ca5b4657e4a46b84ca8c6c2c9142", nil},
{"alternate custom fast path font", &caption.Line{
Data: []caption.Segment{{"Beispieltext", color.Black}},
}, caption.NewContext(gobolditalic.TTF, nil),
0, 1 << 8, "2371b3458a12ca5b4657e4a46b84ca8c6c2c9142", nil},
{"fast path colors", &caption.Line{Data: []caption.Segment{
{"Blue", c(0x55, 0xcd, 0xfc)},
{"Pink", c(0xf7, 0xa8, 0xb8)},
{"White", color.White},
{"Pink", c(0xf7, 0xa8, 0xb8)},
{"Blue", c(0x55, 0xcd, 0xfc)},
}}, nil, 7 << 6, 1 << 8, "2af4b09361aeb3a9befc79cc0b28dd3874657fa0", nil},
}
sharedCtx := new(caption.Context)
var digest []byte
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx := tc.ctx
if ctx == nil {
ctx = sharedCtx
}
tc.line.SetBoundsCheck(true)
redraw := ""
redraw:
got := image.NewRGBA64(image.Rectangle{Max: image.Point{X: 1 << 10, Y: 1 << 9}})
err := tc.line.Render(ctx, got, tc.x, tc.y)
for _, f := range beforeCheck {
f(t, tc.name, redraw != "", got)
}
if !errors.Is(err, tc.wantErr) {
t.Fatalf("Render: error = %q, want %q", err, tc.wantErr)
}
if err != nil {
return
}
t.Run("hash"+redraw, func(t *testing.T) {
var wantDigest []byte
if wantDigest, err = hex.DecodeString(tc.want); err != nil {
t.Errorf("cannot parse expected hash: %v", err)
}
h := sha1.New()
if err = (&png.Encoder{CompressionLevel: png.NoCompression}).Encode(h, got); err != nil {
t.Fatalf("cannot encode png for hashing: %v", err)
}
digest := h.Sum(digest[:0])
if string(digest) != string(wantDigest) {
t.Errorf("Render: %x, want %x", digest, wantDigest)
}
})
if redraw == "" {
redraw = " redraw"
tc.line.SetFont(tc.line.Font, tc.line.Options)
goto redraw
}
})
}
scratchpad := image.NewRGBA64(image.Rectangle{Max: image.Point{X: 1 << 10, Y: 1 << 9}})
t.Run("bad global default", func(t *testing.T) {
wantErr := truetype.FormatError("TTF data is too short")
caption.OverrideGlobalDefault(t, []byte{0xfd})
err := (&caption.Line{Options: new(truetype.Options)}).
Render(new(caption.Context), scratchpad, 0, 1<<8)
if !errors.Is(err, wantErr) {
t.Errorf("Render: error = %q, want %q", err, wantErr)
}
})
t.Run("bounds check skip", func(t *testing.T) {
err := (&caption.Line{Data: []caption.Segment{{"Beispieltext", color.Black}}}).
Render(new(caption.Context), scratchpad, 0, 0)
if err != nil {
t.Errorf("Render: error = %q", err)
}
})
}
func c(r, g, b uint8) color.Color { return color.NRGBA{R: r, G: g, B: b, A: 0xff} }
type stubImage struct {
image.Uniform
}
func (s *stubImage) Set(x, y int, c color.Color) {
runtime.KeepAlive(x)
runtime.KeepAlive(y)
runtime.KeepAlive(c)
}
func (s *stubImage) Bounds() image.Rectangle {
return image.Rectangle{Max: image.Point{X: 1 << 10, Y: 1 << 9}}
}
func benchmarkRender(b *testing.B, bounds bool) {
stub := new(stubImage)
stub.C = color.Black
ctx := new(caption.Context)
line := &caption.Line{Data: []caption.Segment{{"Beispieltext", color.Black}}}
line.SetBoundsCheck(bounds)
for i := 0; i < b.N; i++ {
err := line.Render(ctx, stub, 0, 1<<8)
if err != nil {
b.Fatalf("Render: error = %q", err)
}
}
}
func BenchmarkRender(b *testing.B) { benchmarkRender(b, false) }
func BenchmarkRenderBounds(b *testing.B) { benchmarkRender(b, true) }