From 93d99ce693fed2f6e5051f496561b95e51770622 Mon Sep 17 00:00:00 2001 From: Yonah Date: Sun, 13 Jul 2025 03:04:05 +0900 Subject: [PATCH] line: optional bounds check This has significant overhead so is made optional. --- line.go | 20 +++++++++++++ caption_sample_test.go => line_sample_test.go | 0 caption_test.go => line_test.go | 28 ++++++++++++++++++- 3 files changed, 47 insertions(+), 1 deletion(-) rename caption_sample_test.go => line_sample_test.go (100%) rename caption_test.go => line_test.go (81%) diff --git a/line.go b/line.go index fb9230b..70d848a 100644 --- a/line.go +++ b/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 } diff --git a/caption_sample_test.go b/line_sample_test.go similarity index 100% rename from caption_sample_test.go rename to line_sample_test.go diff --git a/caption_test.go b/line_test.go similarity index 81% rename from caption_test.go rename to line_test.go index 8387185..71fd852 100644 --- a/caption_test.go +++ b/line_test.go @@ -7,6 +7,7 @@ import ( "image" "image/color" "image/png" + "syscall" "testing" "git.gensokyo.uk/yonah/caption" @@ -33,6 +34,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 +85,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) { @@ -127,6 +140,19 @@ func TestRender(t *testing.T) { err, wantErr) } }) + + t.Run("bounds check skip", func(t *testing.T) { + err := (&caption.Line{Data: []caption.Segment{{"Beispieltext", color.Black}}}). + Render( + new(caption.Context), + image.NewRGBA64(image.Rectangle{Max: image.Point{X: 1 << 10, Y: 1 << 9}}), + 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} }