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()) } }