line: optional bounds check
This has significant overhead so is made optional.
This commit is contained in:
parent
26651ce252
commit
6a2822f2fb
20
line.go
20
line.go
@ -3,6 +3,7 @@ package caption
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"syscall"
|
||||
|
||||
"github.com/golang/freetype/truetype"
|
||||
"golang.org/x/image/draw"
|
||||
@ -20,11 +21,16 @@ type Line struct {
|
||||
|
||||
// contains reference to parsed font, populated once per instance
|
||||
face font.Face
|
||||
// whether to fail if pixels are drawn out-of-bounds
|
||||
bounds bool
|
||||
}
|
||||
|
||||
// SetFont clears line font cache and sets the new font.
|
||||
func (l *Line) SetFont(v []byte, opts *truetype.Options) { l.face = nil; l.Font = v; l.Options = opts }
|
||||
|
||||
// SetBoundsCheck changes whether the frame's bounding box is enforced.
|
||||
func (l *Line) SetBoundsCheck(v bool) { l.bounds = v }
|
||||
|
||||
type Segment struct {
|
||||
Value string `json:"value"`
|
||||
Color color.Color `json:"color"`
|
||||
@ -59,12 +65,26 @@ func (l *Line) Render(ctx *Context, frame draw.Image, x, y int) error {
|
||||
face = ctx.defaultFont
|
||||
}
|
||||
|
||||
outOfBounds := false
|
||||
bounds := frame.Bounds()
|
||||
boundsFixed := fixed.R(bounds.Min.X, bounds.Min.Y, bounds.Max.X, bounds.Max.Y)
|
||||
|
||||
drawer := &font.Drawer{Dst: frame, Src: new(image.Uniform), Face: face, Dot: fixed.Point26_6{X: fixed.I(x), Y: fixed.I(y)}}
|
||||
for _, seg := range l.Data {
|
||||
drawer.Src.(*image.Uniform).C = seg.Color
|
||||
|
||||
// optional bounds check to reduce overhead
|
||||
if l.bounds && !outOfBounds {
|
||||
b, _ := drawer.BoundString(seg.Value)
|
||||
outOfBounds = !b.In(boundsFixed)
|
||||
}
|
||||
|
||||
drawer.DrawString(seg.Value)
|
||||
}
|
||||
|
||||
if outOfBounds {
|
||||
return syscall.EDOM
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,8 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"git.gensokyo.uk/yonah/caption"
|
||||
@ -33,6 +35,16 @@ func TestRender(t *testing.T) {
|
||||
{"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},
|
||||
@ -74,12 +86,14 @@ func TestRender(t *testing.T) {
|
||||
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)
|
||||
f(t, tc.name, redraw != "", got)
|
||||
}
|
||||
|
||||
if !errors.Is(err, tc.wantErr) {
|
||||
@ -113,20 +127,56 @@ func TestRender(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
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),
|
||||
image.NewRGBA64(image.Rectangle{Max: image.Point{X: 1 << 10, Y: 1 << 9}}),
|
||||
0, 1<<8,
|
||||
)
|
||||
Render(new(caption.Context), scratchpad, 0, 1<<8)
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Errorf("Render: error = %q, want %q",
|
||||
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) }
|
Loading…
x
Reference in New Issue
Block a user