203 lines
5.2 KiB
Go
203 lines
5.2 KiB
Go
package generator
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"golang.org/x/image/font"
|
|
)
|
|
|
|
func testFace(t *testing.T, size float64) font.Face {
|
|
t.Helper()
|
|
face, err := LoadFace(filepath.Join("..", "..", "assets", "fonts",
|
|
"JetBrainsMonoNLNerdFontMono-Regular.ttf"), size)
|
|
if err != nil {
|
|
t.Fatalf("LoadFace: %v", err)
|
|
}
|
|
t.Cleanup(func() { face.Close() })
|
|
return face
|
|
}
|
|
|
|
func TestNewTextOverlay(t *testing.T) {
|
|
const frameW, frameH = 1920, 1080
|
|
|
|
o, err := NewTextOverlay("AB", frameW, frameH, 0, 0, "", testFace(t, 48))
|
|
if err != nil {
|
|
t.Fatalf("NewTextOverlay: %v", err)
|
|
}
|
|
if o.w%6 != 0 {
|
|
t.Fatalf("w=%d not a multiple of 6", o.w)
|
|
}
|
|
if o.x%6 != 0 {
|
|
t.Fatalf("x=%d not a multiple of 6", o.x)
|
|
}
|
|
if o.x != 0 || o.y != 0 {
|
|
t.Fatalf("position = (%d,%d), want (0,0)", o.x, o.y)
|
|
}
|
|
if o.blocks == nil || len(o.blocks) != o.h*(o.w/6)*16 {
|
|
t.Fatalf("pre-packed tile has wrong size")
|
|
}
|
|
if o.cov[2*o.w+2] != 0 {
|
|
t.Fatalf("padding pixel (2,2) covered: %d", o.cov[2*o.w+2])
|
|
}
|
|
nonzero, full := 0, 0
|
|
for _, c := range o.cov {
|
|
if c > 0 {
|
|
nonzero++
|
|
}
|
|
if c == 255 {
|
|
full++
|
|
}
|
|
}
|
|
if nonzero < 100 {
|
|
t.Fatalf("too few covered pixels: %d", nonzero)
|
|
}
|
|
if full == 0 {
|
|
t.Fatalf("no fully covered (255) pixels")
|
|
}
|
|
|
|
// Text without descenders ("AB") must leave the bottom quarter empty;
|
|
// text with a descender ("Ag") must have coverage there.
|
|
for _, tc := range []struct {
|
|
text string
|
|
wantAny bool
|
|
}{
|
|
{"AB", false},
|
|
{"Ag", true},
|
|
} {
|
|
o, err := NewTextOverlay(tc.text, frameW, frameH, 0, 0, "", testFace(t, 48))
|
|
if err != nil {
|
|
t.Fatalf("NewTextOverlay(%q): %v", tc.text, err)
|
|
}
|
|
any := false
|
|
for row := o.h * 3 / 4; row < o.h; row++ {
|
|
for col := 0; col < o.w; col++ {
|
|
if o.cov[row*o.w+col] > 0 {
|
|
any = true
|
|
}
|
|
}
|
|
}
|
|
if any != tc.wantAny {
|
|
t.Fatalf("%q: coverage in bottom quarter = %v, want %v", tc.text, any, tc.wantAny)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewTextOverlayPositioning(t *testing.T) {
|
|
const frameW, frameH = 1920, 1080
|
|
face := testFace(t, 48)
|
|
|
|
t.Run("explicit", func(t *testing.T) {
|
|
o, err := NewTextOverlay("AB", frameW, frameH, 120, 48, "", face)
|
|
if err != nil {
|
|
t.Fatalf("NewTextOverlay: %v", err)
|
|
}
|
|
if o.x != 120 || o.y != 48 {
|
|
t.Fatalf("position = (%d,%d), want (120,48)", o.x, o.y)
|
|
}
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
pos string
|
|
want func(*TextOverlay) bool
|
|
}{
|
|
{name: "top center", pos: "tc", want: func(o *TextOverlay) bool {
|
|
return o.x%6 == 0 && absInt((o.x+o.w/2)-frameW/2) <= 3 && o.y == 0
|
|
}},
|
|
{name: "center", pos: "cc", want: func(o *TextOverlay) bool {
|
|
return o.x%6 == 0 && absInt((o.x+o.w/2)-frameW/2) <= 3 && o.y == (frameH-o.h)/2
|
|
}},
|
|
{name: "bottom right", pos: "br", want: func(o *TextOverlay) bool {
|
|
return o.x+o.w == frameW && o.y+o.h == frameH
|
|
}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
o, err := NewTextOverlay("AB", frameW, frameH, 0, 0, tc.pos, face)
|
|
if err != nil {
|
|
t.Fatalf("NewTextOverlay: %v", err)
|
|
}
|
|
if !tc.want(o) {
|
|
t.Fatalf("unexpected %s position: box=(%d,%d %dx%d)", tc.pos, o.x, o.y, o.w, o.h)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewTextOverlayRejectsInvalidPosition(t *testing.T) {
|
|
const frameW, frameH = 1920, 1080
|
|
face := testFace(t, 48)
|
|
|
|
tests := []struct {
|
|
name string
|
|
x, y int
|
|
wantErrSub string
|
|
}{
|
|
{name: "unaligned x", x: 7, y: 0, wantErrSub: "divisible by 6"},
|
|
{name: "negative x", x: -6, y: 0, wantErrSub: "must not be negative"},
|
|
{name: "right overflow", x: 1902, y: 0, wantErrSub: "does not fit"},
|
|
{name: "bottom overflow", x: 0, y: 1070, wantErrSub: "does not fit"},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := NewTextOverlay("AB", frameW, frameH, tc.x, tc.y, "", face)
|
|
if err == nil || !strings.Contains(err.Error(), tc.wantErrSub) {
|
|
t.Fatalf("error = %v, want substring %q", err, tc.wantErrSub)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func absInt(v int) int {
|
|
if v < 0 {
|
|
return -v
|
|
}
|
|
return v
|
|
}
|
|
|
|
func TestTextOverlayApplyV210(t *testing.T) {
|
|
const frameW, frameH = 1920, 1080
|
|
o, err := NewTextOverlay("MXL", frameW, frameH, 0, 0, "", testFace(t, 48))
|
|
if err != nil {
|
|
t.Fatalf("NewTextOverlay: %v", err)
|
|
}
|
|
|
|
frame := make([]byte, frameW*frameH*8/3)
|
|
for i := range frame {
|
|
frame[i] = 0x5A // marker: untouched regions must survive
|
|
}
|
|
if err := o.ApplyV210(frame); err != nil {
|
|
t.Fatalf("ApplyV210: %v", err)
|
|
}
|
|
|
|
// Every box pixel: Y from coverage, neutral chroma at even columns.
|
|
for row := 0; row < o.h; row++ {
|
|
for col := 0; col < o.w; col++ {
|
|
wantY := uint32(64 + (int(o.cov[row*o.w+col])*876+127)/255)
|
|
yc, cb, cr := sampleV210(frame, frameW, o.x+col, o.y+row)
|
|
if yc != wantY {
|
|
t.Fatalf("box pixel (%d,%d): Y=%d, want %d", o.x+col, o.y+row, yc, wantY)
|
|
}
|
|
if (o.x+col)%2 == 0 && (cb != 512 || cr != 512) {
|
|
t.Fatalf("box pixel (%d,%d): Cb=%d Cr=%d, want 512/512", o.x+col, o.y+row, cb, cr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Blocks just outside the box (right edge and below) must be untouched.
|
|
for _, p := range [][2]int{
|
|
{o.x + o.w, o.y}, // right of box, block-aligned
|
|
{o.x, o.y + o.h}, // below box
|
|
{o.x + o.w, o.y + o.h}, // corner
|
|
{100, 1000}, // far away
|
|
} {
|
|
off := ((p[1]*frameW + p[0]) / 6) * 16
|
|
for i := 0; i < 16; i++ {
|
|
if frame[off+i] != 0x5A {
|
|
t.Fatalf("block at (%d,%d) modified outside the box", p[0], p[1])
|
|
}
|
|
}
|
|
}
|
|
}
|