package utils
import (
"bytes"
"image"
"image/color"
"image/png"
"math/rand"
"strings"
"testing"
)
func TestWriteKittyImage_SmallImage(t *testing.T) {
img := image.NewGray(image.Rect(0, 0, 2, 2))
img.SetGray(0, 0, color.Gray{Y: 128})
var buf strings.Builder
if err := WriteKittyImage(img, &buf); err != nil {
t.Fatalf("WriteKittyImage: %v", err)
}
out := buf.String()
if !strings.HasPrefix(out, "\x1b_Gf=100,a=T;") {
t.Error("expected single-chunk header with f=100,a=T")
}
if strings.Contains(out, "m=") {
t.Error("small image should not use chunked m= key")
}
if !strings.HasSuffix(out, "\x1b\\") {
t.Error("expected escape sequence terminator")
}
}
func TestWriteKittyImage_LargeImage_Chunked(t *testing.T) {
rng := rand.New(rand.NewSource(42))
img := image.NewGray(image.Rect(0, 0, 128, 128))
for y := range 128 {
for x := range 128 {
img.SetGray(x, y, color.Gray{Y: uint8(rng.Intn(256))})
}
}
var buf strings.Builder
if err := WriteKittyImage(img, &buf); err != nil {
t.Fatalf("WriteKittyImage: %v", err)
}
out := buf.String()
chunks := strings.Split(out, "\x1b\\")
chunks = chunks[:len(chunks)-1]
if len(chunks) < 2 {
t.Fatalf("expected multiple chunks, got %d", len(chunks))
}
if !strings.Contains(chunks[0], "f=100,a=T,m=1") {
t.Errorf("first chunk missing f=100,a=T,m=1: %s", chunks[0][:min(80, len(chunks[0]))])
}
last := chunks[len(chunks)-1]
if !strings.Contains(last, "\x1b_Gm=0;") {
t.Errorf("last chunk missing m=0: %s", last[:min(80, len(last))])
}
for i := 1; i < len(chunks)-1; i++ {
if !strings.Contains(chunks[i], "\x1b_Gm=1;") {
t.Errorf("middle chunk %d missing m=1", i)
}
}
}
func TestClearKittyImages(t *testing.T) {
var buf strings.Builder
ClearKittyImages(&buf)
expected := "\x1b_Ga=d\x1b\\"
if buf.String() != expected {
t.Errorf("got %q, want %q", buf.String(), expected)
}
}
func TestWriteSixelImage(t *testing.T) {
img := image.NewGray(image.Rect(0, 0, 4, 6))
for y := range 6 {
for x := range 4 {
img.SetGray(x, y, color.Gray{Y: uint8((x + y) * 40)})
}
}
var buf strings.Builder
if err := WriteSixelImage(img, &buf); err != nil {
t.Fatalf("WriteSixelImage: %v", err)
}
out := buf.String()
if !strings.HasPrefix(out, "\x1bP") {
t.Error("expected DCS prefix \\x1bP")
}
if !strings.HasSuffix(out, "\x1b\\") {
t.Error("expected ST suffix \\x1b\\\\")
}
if !strings.Contains(out, "q") {
t.Error("expected 'q' in DCS sequence")
}
}
func TestClearImages_Kitty(t *testing.T) {
var buf strings.Builder
ClearImages(&buf, ProtocolKitty)
if buf.String() != "\x1b_Ga=d\x1b\\" {
t.Errorf("got %q, want kitty clear sequence", buf.String())
}
}
func TestClearImages_Sixel(t *testing.T) {
var buf strings.Builder
ClearImages(&buf, ProtocolSixel)
if buf.String() != "" {
t.Errorf("expected no output for sixel clear, got %q", buf.String())
}
}
func TestWriteImage_Kitty(t *testing.T) {
img := image.NewGray(image.Rect(0, 0, 2, 2))
var buf strings.Builder
if err := WriteImage(img, &buf, ProtocolKitty); err != nil {
t.Fatalf("WriteImage kitty: %v", err)
}
if !strings.HasPrefix(buf.String(), "\x1b_G") {
t.Error("expected kitty escape prefix")
}
}
func TestWriteImage_Sixel(t *testing.T) {
img := image.NewGray(image.Rect(0, 0, 4, 6))
var buf strings.Builder
if err := WriteImage(img, &buf, ProtocolSixel); err != nil {
t.Fatalf("WriteImage sixel: %v", err)
}
if !strings.HasPrefix(buf.String(), "\x1bP") {
t.Error("expected sixel DCS prefix")
}
}
func TestWriteITermImage(t *testing.T) {
img := image.NewGray(image.Rect(0, 0, 4, 4))
img.SetGray(0, 0, color.Gray{Y: 128})
var buf strings.Builder
if err := WriteITermImage(img, &buf); err != nil {
t.Fatalf("WriteITermImage: %v", err)
}
out := buf.String()
if !strings.HasPrefix(out, "\x1b]1337;File=") {
t.Errorf("expected iTerm2 OSC prefix, got %q", out[:min(30, len(out))])
}
if !strings.Contains(out, "inline=1") {
t.Error("expected inline=1 parameter")
}
if !strings.HasSuffix(out, "\x07") {
t.Error("expected BEL terminator")
}
}
func TestWriteImage_ITerm(t *testing.T) {
img := image.NewGray(image.Rect(0, 0, 4, 4))
var buf strings.Builder
if err := WriteImage(img, &buf, ProtocolITerm); err != nil {
t.Fatalf("WriteImage iterm: %v", err)
}
if !strings.HasPrefix(buf.String(), "\x1b]1337;File=") {
t.Error("expected iTerm2 OSC prefix")
}
}
func TestClampImageSize(t *testing.T) {
tests := []struct {
input int
want int
}{
{100, 224}, {224, 224}, {448, 448}, {600, 600}, {896, 896}, {1000, 896}, }
for _, tt := range tests {
got := ClampImageSize(tt.input)
if got != tt.want {
t.Errorf("ClampImageSize(%d) = %d, want %d", tt.input, got, tt.want)
}
}
}
func TestCreateGrayscaleImage_Basic(t *testing.T) {
data := [][]uint8{
{0, 128, 255},
{64, 192, 32},
}
img := CreateGrayscaleImage(data)
if img == nil {
t.Fatal("expected non-nil image")
}
bounds := img.Bounds()
if bounds.Dx() != 3 {
t.Errorf("width = %d, want 3", bounds.Dx())
}
if bounds.Dy() != 2 {
t.Errorf("height = %d, want 2", bounds.Dy())
}
gray := img.(*image.Gray)
if gray.Pix[0] != 0 {
t.Errorf("pix[0] = %d, want 0", gray.Pix[0])
}
if gray.Pix[1] != 128 {
t.Errorf("pix[1] = %d, want 128", gray.Pix[1])
}
if gray.Pix[2] != 255 {
t.Errorf("pix[2] = %d, want 255", gray.Pix[2])
}
if gray.Pix[gray.Stride+0] != 64 {
t.Errorf("pix[row1+0] = %d, want 64", gray.Pix[gray.Stride])
}
if gray.Pix[gray.Stride+1] != 192 {
t.Errorf("pix[row1+1] = %d, want 192", gray.Pix[gray.Stride+1])
}
if gray.Pix[gray.Stride+2] != 32 {
t.Errorf("pix[row1+2] = %d, want 32", gray.Pix[gray.Stride+2])
}
}
func TestCreateGrayscaleImage_Empty(t *testing.T) {
tests := []struct {
name string
data [][]uint8
}{
{"nil", nil},
{"empty outer", [][]uint8{}},
{"empty inner", [][]uint8{{}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
img := CreateGrayscaleImage(tt.data)
if img != nil {
t.Errorf("expected nil for %s", tt.name)
}
})
}
}
func assertRGBAPixel(t *testing.T, rgba *image.RGBA, x, y int, wantR, wantG, wantB, wantA uint8) {
t.Helper()
off := y*rgba.Stride + x*4
got := rgba.Pix[off : off+4]
if got[0] != wantR || got[1] != wantG || got[2] != wantB || got[3] != wantA {
t.Errorf("pixel (%d,%d) = [%d,%d,%d,%d], want [%d,%d,%d,%d]",
x, y, got[0], got[1], got[2], got[3], wantR, wantG, wantB, wantA)
}
}
func TestCreateRGBImage_Basic(t *testing.T) {
data := [][]RGBPixel{
{{R: 255, G: 0, B: 0}, {R: 0, G: 255, B: 0}},
{{R: 0, G: 0, B: 255}, {R: 255, G: 255, B: 255}},
}
img := CreateRGBImage(data)
if img == nil {
t.Fatal("expected non-nil image")
}
bounds := img.Bounds()
if bounds.Dx() != 2 {
t.Errorf("width = %d, want 2", bounds.Dx())
}
if bounds.Dy() != 2 {
t.Errorf("height = %d, want 2", bounds.Dy())
}
rgba := img.(*image.RGBA)
assertRGBAPixel(t, rgba, 0, 0, 255, 0, 0, 255) assertRGBAPixel(t, rgba, 1, 0, 0, 255, 0, 255) assertRGBAPixel(t, rgba, 0, 1, 0, 0, 255, 255) }
func TestCreateRGBImage_Empty(t *testing.T) {
tests := []struct {
name string
data [][]RGBPixel
}{
{"nil", nil},
{"empty outer", [][]RGBPixel{}},
{"empty inner", [][]RGBPixel{{}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
img := CreateRGBImage(tt.data)
if img != nil {
t.Errorf("expected nil for %s", tt.name)
}
})
}
}
func TestResizeImage_Grayscale(t *testing.T) {
data := make([][]uint8, 4)
for i := range data {
data[i] = []uint8{128, 128, 128, 128}
}
src := CreateGrayscaleImage(data)
resized := ResizeImage(src, 2, 2)
gray := resized.(*image.Gray)
for y := range 2 {
for x := range 2 {
if gray.Pix[y*gray.Stride+x] != 128 {
t.Errorf("pixel (%d,%d) = %d, want 128", x, y, gray.Pix[y*gray.Stride+x])
}
}
}
}
func TestResizeImage_RGBA(t *testing.T) {
rgbData := make([][]RGBPixel, 4)
for i := range rgbData {
rgbData[i] = []RGBPixel{{R: 100, G: 150, B: 200}, {R: 100, G: 150, B: 200}, {R: 100, G: 150, B: 200}, {R: 100, G: 150, B: 200}}
}
src := CreateRGBImage(rgbData)
resized := ResizeImage(src, 2, 2)
rgba := resized.(*image.RGBA)
for y := range 2 {
for x := range 2 {
off := y*rgba.Stride + x*4
if rgba.Pix[off] != 100 || rgba.Pix[off+1] != 150 || rgba.Pix[off+2] != 200 {
t.Errorf("pixel (%d,%d) = (%d,%d,%d), want (100,150,200)",
x, y, rgba.Pix[off], rgba.Pix[off+1], rgba.Pix[off+2])
}
}
}
}
func TestResizeImage_GenericFallback(t *testing.T) {
palette := color.Palette{color.Black, color.White}
src := image.NewPaletted(image.Rect(0, 0, 4, 4), palette)
for y := range 4 {
for x := range 4 {
src.SetColorIndex(x, y, 0) }
}
resized := ResizeImage(src, 2, 2)
bounds := resized.Bounds()
if bounds.Dx() != 2 || bounds.Dy() != 2 {
t.Errorf("bounds = %v, want 2x2", bounds)
}
}
func TestResizeImage_Upscale(t *testing.T) {
data := [][]uint8{
{0, 255},
{128, 64},
}
src := CreateGrayscaleImage(data)
resized := ResizeImage(src, 4, 4)
gray := resized.(*image.Gray)
if gray.Pix[0] != 0 {
t.Errorf("(0,0) = %d, want 0", gray.Pix[0])
}
if gray.Pix[3] != 255 {
t.Errorf("(3,0) = %d, want 255", gray.Pix[3])
}
}
func TestWritePNG(t *testing.T) {
img := image.NewGray(image.Rect(0, 0, 4, 4))
var buf bytes.Buffer
if err := WritePNG(img, &buf); err != nil {
t.Fatalf("WritePNG: %v", err)
}
if buf.Len() == 0 {
t.Error("expected non-empty PNG output")
}
decoded, err := png.Decode(&buf)
if err != nil {
t.Fatalf("failed to decode PNG: %v", err)
}
if decoded.Bounds().Dx() != 4 || decoded.Bounds().Dy() != 4 {
t.Errorf("decoded bounds = %v, want 4x4", decoded.Bounds())
}
}
func TestClearImages_ITerm(t *testing.T) {
var buf strings.Builder
ClearImages(&buf, ProtocolITerm)
if buf.String() != "" {
t.Errorf("expected no output for iTerm2 clear, got %q", buf.String())
}
}