116 lines
3.9 KiB
Go
116 lines
3.9 KiB
Go
|
|
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())
|
|||
|
|
}
|
|||
|
|
}
|