package imageutil import ( "bytes" "encoding/binary" "image" "image/color" "image/jpeg" "testing" ) // jpegMitDrehung baut eine echte JPEG-Datei mit EXIF-Block. Von Hand, damit // der Test keine Abhängigkeit braucht und genau das prüft, was Kameras // schreiben: APP1 direkt hinter dem Dateianfang. func jpegMitDrehung(t *testing.T, o uint16, big bool) []byte { t.Helper() var body bytes.Buffer if err := jpeg.Encode(&body, image.NewGray(image.Rect(0, 0, 8, 8)), nil); err != nil { t.Fatal(err) } raw := body.Bytes() // beginnt mit FFD8 var bo binary.AppendByteOrder = binary.LittleEndian order := []byte{'I', 'I'} if big { bo = binary.BigEndian order = []byte{'M', 'M'} } tif := make([]byte, 0, 26) tif = append(tif, order...) tif = bo.AppendUint16(tif, 42) tif = bo.AppendUint32(tif, 8) // Verzeichnis beginnt direkt dahinter tif = bo.AppendUint16(tif, 1) // ein Eintrag tif = bo.AppendUint16(tif, 0x0112) tif = bo.AppendUint16(tif, 3) // SHORT tif = bo.AppendUint32(tif, 1) // ein Wert tif = bo.AppendUint16(tif, o) tif = append(tif, 0, 0) // Auffüllung auf vier Byte tif = bo.AppendUint32(tif, 0) // kein weiteres Verzeichnis payload := append([]byte("Exif\x00\x00"), tif...) seg := []byte{0xFF, 0xE1} seg = binary.BigEndian.AppendUint16(seg, uint16(len(payload)+2)) seg = append(seg, payload...) out := append([]byte{}, raw[:2]...) // SOI out = append(out, seg...) return append(out, raw[2:]...) } func TestOrientationLiestAlleFaelle(t *testing.T) { for o := uint16(1); o <= 8; o++ { for _, big := range []bool{false, true} { got := Orientation(jpegMitDrehung(t, o, big)) if got != int(o) { name := "II" if big { name = "MM" } t.Errorf("%s Drehung %d gelesen als %d", name, o, got) } } } } // Was keine Drehung trägt oder kaputt ist, muss 1 ergeben – niemals einen // Fehler und niemals einen Zufallswert. func TestOrientationBleibtBeiEinsWennUnklar(t *testing.T) { var ohne bytes.Buffer if err := jpeg.Encode(&ohne, image.NewGray(image.Rect(0, 0, 4, 4)), nil); err != nil { t.Fatal(err) } mitDrehung := jpegMitDrehung(t, 6, false) faelle := map[string][]byte{ "leer": {}, "kein JPEG": []byte("das hier ist ein Text"), "nur SOI": {0xFF, 0xD8}, "ohne EXIF": ohne.Bytes(), "abgeschnitten": mitDrehung[:12], "Laenge zu gross": append([]byte{0xFF, 0xD8, 0xFF, 0xE1, 0xFF, 0xFF}, []byte("Exif\x00\x00")...), "Drehung 0": jpegMitDrehung(t, 0, false), "Drehung 99": jpegMitDrehung(t, 99, false), } for name, data := range faelle { if got := Orientation(data); got != 1 { t.Errorf("%s ergab %d statt 1", name, got) } } } // Ein Bild mit einer eindeutigen Ecke: Nur so lässt sich prüfen, dass nicht // bloß Breite und Höhe getauscht wurden, sondern richtig herum gedreht. func markiert(w, h int) image.Image { 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{0, 0, 0, 255}) } } img.Set(0, 0, color.RGBA{255, 0, 0, 255}) // oben links rot return img } func TestApplyDrehtRichtigHerum(t *testing.T) { const w, h = 4, 2 src := markiert(w, h) // Wo landet der rote Punkt von oben links? erwartet := map[int][2]int{ 1: {0, 0}, 2: {w - 1, 0}, // waagerecht gespiegelt 3: {w - 1, h - 1}, // 180 Grad 4: {0, h - 1}, // senkrecht gespiegelt 5: {0, 0}, // Hauptdiagonale 6: {h - 1, 0}, // 90 Grad im Uhrzeigersinn: links oben → rechts oben 7: {h - 1, w - 1}, 8: {0, w - 1}, // 90 Grad dagegen: links oben → links unten } for o, want := range erwartet { out := Apply(src, o) b := out.Bounds() wantW, wantH := w, h if o >= 5 { wantW, wantH = h, w } if b.Dx() != wantW || b.Dy() != wantH { t.Errorf("Drehung %d: %dx%d, erwartet %dx%d", o, b.Dx(), b.Dy(), wantW, wantH) continue } r, _, _, _ := out.At(want[0], want[1]).RGBA() if r < 0x8000 { t.Errorf("Drehung %d: an %v steht kein roter Punkt", o, want) } } } // Bei 1 darf kein neues Bild entstehen – das spart bei jedem Vorschaubild // eine vollständige Kopie. func TestApplyLaesstEinsUnberuehrt(t *testing.T) { src := markiert(3, 3) if Apply(src, 1) != src { t.Error("Drehung 1 hat kopiert") } if Apply(src, 0) != src || Apply(src, 9) != src { t.Error("unbekannte Drehung hat kopiert") } }