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) }