caption: basic line rendering
This commit is contained in:
commit
734c4e7324
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.pkg
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# env file
|
||||||
|
.env
|
||||||
|
.idea
|
||||||
|
.vscode
|
10
caption.go
Normal file
10
caption.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// Package caption provides a caption rendering interface that prioritises ease of use.
|
||||||
|
package caption
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/image/font/gofont/goregular"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultFont = goregular.TTF
|
||||||
|
)
|
30
caption_sample_test.go
Normal file
30
caption_sample_test.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
//go:build export_samples
|
||||||
|
|
||||||
|
package caption_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
beforeCheck = append(beforeCheck, func(t *testing.T, name string, got image.Image) {
|
||||||
|
t.Run("export", func(t *testing.T) {
|
||||||
|
outPath := strings.TrimSpace(os.Getenv("TEST_RENDER_OUT_PATH"))
|
||||||
|
if len(outPath) == 0 {
|
||||||
|
outPath = os.TempDir()
|
||||||
|
}
|
||||||
|
outPath = path.Join(outPath, "overlay-"+name+".png")
|
||||||
|
if re, err := os.Create(outPath); err != nil {
|
||||||
|
t.Fatalf("cannot create: %v", err)
|
||||||
|
} else if err = (&png.Encoder{CompressionLevel: png.NoCompression}).Encode(re, got); err != nil {
|
||||||
|
t.Fatalf("cannot encode to file: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("test case exported to %s", outPath)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
75
caption_test.go
Normal file
75
caption_test.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package caption_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/png"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/yonah/caption"
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
beforeCheck []func(t *testing.T, name string, got image.Image)
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRender(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
line *caption.Line
|
||||||
|
x, y int
|
||||||
|
want string
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{"bad font", &caption.Line{Font: []byte{0xfd}},
|
||||||
|
0, 0, "", truetype.FormatError("TTF data is too short")},
|
||||||
|
{"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)},
|
||||||
|
}}, 7 << 6, 1 << 8, "2af4b09361aeb3a9befc79cc0b28dd3874657fa0", nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
var digest []byte
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got := image.NewRGBA64(image.Rectangle{Max: image.Point{X: 1 << 10, Y: 1 << 9}})
|
||||||
|
err := tc.line.Render(got, tc.x, tc.y)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, tc.wantErr) {
|
||||||
|
t.Errorf("Render: error = %q, want %q",
|
||||||
|
err, tc.wantErr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range beforeCheck {
|
||||||
|
f(t, tc.name, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("hash", 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func c(r, g, b uint8) color.Color { return color.NRGBA{R: r, G: g, B: b, A: 0xff} }
|
8
go.mod
Normal file
8
go.mod
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
module git.gensokyo.uk/yonah/caption
|
||||||
|
|
||||||
|
go 1.24.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||||
|
golang.org/x/image v0.29.0
|
||||||
|
)
|
4
go.sum
Normal file
4
go.sum
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
|
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
|
||||||
|
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
|
52
line.go
Normal file
52
line.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package caption
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Line struct {
|
||||||
|
// collection of all text segments
|
||||||
|
Data []Segment `json:"data"`
|
||||||
|
// applies to all segments; avoid direct assignment and use SetFont instead
|
||||||
|
Font []byte `json:"font"`
|
||||||
|
// applies to all segments; avoid direct assignment and use SetFont instead
|
||||||
|
Options *truetype.Options `json:"options"`
|
||||||
|
|
||||||
|
// contains reference to parsed font, populated once per instance
|
||||||
|
face font.Face
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 }
|
||||||
|
|
||||||
|
type Segment struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
Color color.Color `json:"color"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Line) Render(frame draw.Image, x, y int) error {
|
||||||
|
if l.face == nil {
|
||||||
|
if len(l.Font) == 0 {
|
||||||
|
l.SetFont(defaultFont, l.Options)
|
||||||
|
}
|
||||||
|
if f, err := truetype.Parse(l.Font); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
l.face = truetype.NewFace(f, l.Options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawer := &font.Drawer{Dst: frame, Face: l.face, Dot: fixed.Point26_6{X: fixed.I(x), Y: fixed.I(y)}}
|
||||||
|
for _, seg := range l.Data {
|
||||||
|
drawer.Src = image.NewUniform(seg.Color)
|
||||||
|
drawer.DrawString(seg.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user