Files
osdb-1-claude-opusplan/backend/internal/imaging/crop_test.go
juliaandClaude Sonnet 4.6 649c9687ac feat: fruit overview with thumbnails and card layout (story #07)
- Auto-crop thumbnails on image upload (Go stdlib, no external deps):
  find non-background bounding box, pad 5px, scale to ≤300px, encode JPEG
- Add thumbnail_data BYTEA column to fruit_images and publication_fruit_images
- New GET /api/v1/fruits/:id/images/:imageId/thumbnail endpoint (fallback to full data)
- New GET /api/v1/publications/:id/fruit-images/:imgId/thumbnail endpoint
- New POST /api/v1/admin/backfill-thumbnails to retroactively generate thumbnails
- ListFruits response now includes synonyms and thumbnail_url per fruit
- Replace FruitList table with responsive FruitCard grid (thumbnail + name + OSDB ID + type + synonyms)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 09:02:14 +02:00

154 lines
3.8 KiB
Go

package imaging_test
import (
"bytes"
"image"
"image/color"
"image/jpeg"
"image/png"
"testing"
"osdb/internal/imaging"
)
func makeWhitePNG(w, h int) []byte {
img := image.NewRGBA(image.Rect(0, 0, w, h))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
img.Set(x, y, color.White)
}
}
var buf bytes.Buffer
_ = png.Encode(&buf, img)
return buf.Bytes()
}
func makeContentPNG(w, h, contentX, contentY, contentW, contentH int) []byte {
img := image.NewRGBA(image.Rect(0, 0, w, h))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
img.Set(x, y, color.White)
}
}
for y := contentY; y < contentY+contentH; y++ {
for x := contentX; x < contentX+contentW; x++ {
img.Set(x, y, color.RGBA{R: 100, G: 50, B: 30, A: 255})
}
}
var buf bytes.Buffer
_ = png.Encode(&buf, img)
return buf.Bytes()
}
func imageDims(data []byte) (int, int) {
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return 0, 0
}
b := img.Bounds()
return b.Dx(), b.Dy()
}
func TestCropToContent_InvalidData(t *testing.T) {
result, err := imaging.CropToContent([]byte("not an image"))
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if !bytes.Equal(result, []byte("not an image")) {
t.Error("expected original bytes returned on decode failure")
}
}
func TestCropToContent_AllBackground(t *testing.T) {
src := makeWhitePNG(100, 100)
result, err := imaging.CropToContent(src)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result) == 0 {
t.Error("expected non-empty result")
}
}
func TestCropToContent_CropsToContent(t *testing.T) {
// 200x200 white image with a 20x20 content block at (80,80)
src := makeContentPNG(200, 200, 80, 80, 20, 20)
result, err := imaging.CropToContent(src)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
w, h := imageDims(result)
if w >= 200 || h >= 200 {
t.Errorf("expected smaller result, got %dx%d", w, h)
}
}
func TestCropToContent_PreservesContentWithPadding(t *testing.T) {
// 200x200 white image with a 30x30 red block at (85,85)
src := makeContentPNG(200, 200, 85, 85, 30, 30)
result, err := imaging.CropToContent(src)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Result should be smaller than original but include padding
w, h := imageDims(result)
if w == 0 || h == 0 {
t.Error("expected valid image dimensions")
}
if w >= 200 || h >= 200 {
t.Errorf("expected crop, got %dx%d", w, h)
}
}
func TestCropToContent_ScalesDownLargeImage(t *testing.T) {
// 600x600 image with content filling most of it
src := makeContentPNG(600, 600, 10, 10, 580, 580)
result, err := imaging.CropToContent(src)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
w, h := imageDims(result)
if w > 300 || h > 300 {
t.Errorf("expected scale-down to ≤300, got %dx%d", w, h)
}
}
func TestCropToContent_ReturnsJPEG(t *testing.T) {
src := makeContentPNG(100, 100, 20, 20, 60, 60)
result, err := imaging.CropToContent(src)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// JPEG magic bytes: FF D8 FF
if len(result) < 3 || result[0] != 0xFF || result[1] != 0xD8 {
// Check if we got back original (all-background case returns src)
if !bytes.Equal(result, src) {
t.Error("expected JPEG output")
}
}
}
func TestCropToContent_JPEGInput(t *testing.T) {
// Create a JPEG input
img := image.NewRGBA(image.Rect(0, 0, 100, 100))
for y := 0; y < 100; y++ {
for x := 0; x < 100; x++ {
if x > 20 && x < 80 && y > 20 && y < 80 {
img.Set(x, y, color.RGBA{R: 200, G: 100, B: 50, A: 255})
} else {
img.Set(x, y, color.White)
}
}
}
var buf bytes.Buffer
_ = jpeg.Encode(&buf, img, nil)
result, err := imaging.CropToContent(buf.Bytes())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result) == 0 {
t.Error("expected non-empty result")
}
}