hsa-app/internal/web/orientation_test.go

116 lines
3.9 KiB
Go
Raw Normal View History

package web
import (
"bytes"
"encoding/binary"
"image"
"image/color"
"image/jpeg"
"testing"
"github.com/disintegration/imaging"
)
// jpegBytes encodes a w×h test image as JPEG (no EXIF).
func jpegBytes(t *testing.T, w, h int) []byte {
t.Helper()
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.RGBA{uint8(x * 30), uint8(y * 30), 100, 255})
}
}
var buf bytes.Buffer
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 90}); err != nil {
t.Fatalf("encode jpeg: %v", err)
}
return buf.Bytes()
}
// withOrientation splices a minimal big-endian EXIF APP1 segment carrying the
// given Orientation value into a JPEG (right after the SOI marker).
func withOrientation(t *testing.T, jpg []byte, orientation uint16) []byte {
t.Helper()
tiff := new(bytes.Buffer)
tiff.WriteString("MM") // big-endian
binary.Write(tiff, binary.BigEndian, uint16(0x002A)) // magic
binary.Write(tiff, binary.BigEndian, uint32(8)) // IFD0 at offset 8
binary.Write(tiff, binary.BigEndian, uint16(1)) // one entry
binary.Write(tiff, binary.BigEndian, uint16(0x0112)) // tag: Orientation
binary.Write(tiff, binary.BigEndian, uint16(3)) // type: SHORT
binary.Write(tiff, binary.BigEndian, uint32(1)) // count
binary.Write(tiff, binary.BigEndian, orientation) // value (in high 2 bytes)
binary.Write(tiff, binary.BigEndian, uint16(0)) // value padding
binary.Write(tiff, binary.BigEndian, uint32(0)) // next IFD: none
payload := append([]byte("Exif\x00\x00"), tiff.Bytes()...)
app1 := new(bytes.Buffer)
app1.Write([]byte{0xFF, 0xE1})
binary.Write(app1, binary.BigEndian, uint16(len(payload)+2)) // segment length
app1.Write(payload)
out := make([]byte, 0, len(jpg)+app1.Len())
out = append(out, jpg[:2]...) // SOI
out = append(out, app1.Bytes()...)
out = append(out, jpg[2:]...)
return out
}
func TestExifOrientation(t *testing.T) {
plain := jpegBytes(t, 4, 2)
if got := exifOrientation(plain); got != 0 {
t.Errorf("plain JPEG: orientation = %d, want 0 (no tag)", got)
}
for _, o := range []uint16{1, 3, 6, 8} {
tagged := withOrientation(t, plain, o)
if got := exifOrientation(tagged); got != int(o) {
t.Errorf("tagged orientation %d: got %d", o, got)
}
}
if got := exifOrientation([]byte("not a jpeg")); got != 0 {
t.Errorf("non-JPEG: orientation = %d, want 0", got)
}
}
func TestNormalizeOrientationPassThrough(t *testing.T) {
plain := jpegBytes(t, 4, 2)
// Non-JPEG mime → untouched.
png := []byte("fake png bytes")
if got := normalizeOrientation(png, "image/png"); !bytes.Equal(got, png) {
t.Error("png should pass through unchanged")
}
// JPEG without EXIF → untouched (can't know, don't guess or recompress).
if got := normalizeOrientation(plain, "image/jpeg"); !bytes.Equal(got, plain) {
t.Error("EXIF-less JPEG should pass through byte-for-byte")
}
// JPEG already upright (orientation 1) → untouched.
up := withOrientation(t, plain, 1)
if got := normalizeOrientation(up, "image/jpeg"); !bytes.Equal(got, up) {
t.Error("orientation-1 JPEG should pass through byte-for-byte")
}
}
func TestNormalizeOrientationRotates(t *testing.T) {
// Stored 4×2 (wide) image tagged orientation 6 ("rotate 90° CW to view") must
// come out upright — i.e. tall (2×4) — with the EXIF tag stripped.
wide := jpegBytes(t, 4, 2)
tagged := withOrientation(t, wide, 6)
out := normalizeOrientation(tagged, "image/jpeg")
if bytes.Equal(out, tagged) {
t.Fatal("orientation-6 JPEG should have been re-encoded upright")
}
if got := exifOrientation(out); got != 0 {
t.Errorf("output still carries orientation tag %d; want stripped (0)", got)
}
img, err := imaging.Decode(bytes.NewReader(out))
if err != nil {
t.Fatalf("decode output: %v", err)
}
b := img.Bounds()
if b.Dx() != 2 || b.Dy() != 4 {
t.Errorf("output dims = %dx%d, want 2x4 (rotated upright)", b.Dx(), b.Dy())
}
}