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 (
|
import (
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/golang/freetype/truetype"
|
"github.com/golang/freetype/truetype"
|
||||||
"golang.org/x/image/draw"
|
"golang.org/x/image/draw"
|
||||||
@ -20,11 +21,16 @@ type Line struct {
|
|||||||
|
|
||||||
// contains reference to parsed font, populated once per instance
|
// contains reference to parsed font, populated once per instance
|
||||||
face font.Face
|
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.
|
// 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 }
|
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 {
|
type Segment struct {
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
Color color.Color `json:"color"`
|
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
|
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)}}
|
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 {
|
for _, seg := range l.Data {
|
||||||
drawer.Src.(*image.Uniform).C = seg.Color
|
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)
|
drawer.DrawString(seg.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if outOfBounds {
|
||||||
|
return syscall.EDOM
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"image/png"
|
"image/png"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gensokyo.uk/yonah/caption"
|
"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),
|
{"bad fast path font", &caption.Line{}, caption.NewContext([]byte{0xfd}, nil),
|
||||||
0, 0, "", truetype.FormatError("TTF data is too short")},
|
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{
|
{"custom line options", &caption.Line{
|
||||||
Data: []caption.Segment{{"Beispieltext", color.Black}},
|
Data: []caption.Segment{{"Beispieltext", color.Black}},
|
||||||
Options: &truetype.Options{Size: 24},
|
Options: &truetype.Options{Size: 24},
|
||||||
@ -74,12 +86,14 @@ func TestRender(t *testing.T) {
|
|||||||
ctx = sharedCtx
|
ctx = sharedCtx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tc.line.SetBoundsCheck(true)
|
||||||
|
|
||||||
redraw := ""
|
redraw := ""
|
||||||
redraw:
|
redraw:
|
||||||
got := image.NewRGBA64(image.Rectangle{Max: image.Point{X: 1 << 10, Y: 1 << 9}})
|
got := image.NewRGBA64(image.Rectangle{Max: image.Point{X: 1 << 10, Y: 1 << 9}})
|
||||||
err := tc.line.Render(ctx, got, tc.x, tc.y)
|
err := tc.line.Render(ctx, got, tc.x, tc.y)
|
||||||
for _, f := range beforeCheck {
|
for _, f := range beforeCheck {
|
||||||
f(t, tc.name, redraw == "", got)
|
f(t, tc.name, redraw != "", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !errors.Is(err, tc.wantErr) {
|
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) {
|
t.Run("bad global default", func(t *testing.T) {
|
||||||
wantErr := truetype.FormatError("TTF data is too short")
|
wantErr := truetype.FormatError("TTF data is too short")
|
||||||
caption.OverrideGlobalDefault(t, []byte{0xfd})
|
caption.OverrideGlobalDefault(t, []byte{0xfd})
|
||||||
err := (&caption.Line{Options: new(truetype.Options)}).
|
err := (&caption.Line{Options: new(truetype.Options)}).
|
||||||
Render(
|
Render(new(caption.Context), scratchpad, 0, 1<<8)
|
||||||
new(caption.Context),
|
|
||||||
image.NewRGBA64(image.Rectangle{Max: image.Point{X: 1 << 10, Y: 1 << 9}}),
|
|
||||||
0, 1<<8,
|
|
||||||
)
|
|
||||||
if !errors.Is(err, wantErr) {
|
if !errors.Is(err, wantErr) {
|
||||||
t.Errorf("Render: error = %q, want %q",
|
t.Errorf("Render: error = %q, want %q", err, wantErr)
|
||||||
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} }
|
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