Files
marcandClaude Opus 5 5ea66ea884 fix(thumbnails): Fassung in der Adresse, Sofortanzeige, kein Flackern
Gleicher Dateiname erzeugt keine neue Datei, sondern eine neue Fassung -
gleiche ID, gleiche Vorschaubild-Adresse. Die darf einen Tag im Browser
liegen, also zeigte die Kachel nach dem erneuten Hochladen bis zu 24
Stunden das alte Bild. Das erklaert, warum eine Drehung erst nach
mehreren Versuchen "ankam". Die Adresse traegt jetzt ?v=<Fassung>.

Vorschaubilder entstehen im Hintergrund; die Kachel fragt jetzt bis zu
fuenfmal nach, statt bis zum naechsten Neuladen ihr Ersatzsymbol zu
zeigen. Nur bei Bildern juenger als zwei Minuten - sonst fragte jede
dauerhaft fehlgeschlagene Datei bei jedem Oeffnen erneut. Gemessen: 608
ms bis zur Anzeige, ohne Neuladen.

Das Anheben der Kachel beim Ueberfahren ist raus: Steht der Zeiger auf
der Unterkante, rutscht sie unter ihm weg, mouseleave, sie faellt
zurueck, mouseenter - im Kreis, sichtbar als Flackern.

Dazu liest die EXIF-Auswertung jetzt 256 statt 64 KB und gibt bei einem
abgeschnittenen Block nicht mehr auf: Die Drehung steht vorn, das
eingebettete Vorschaubild dahinter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 14:29:09 +02:00

165 lines
4.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
}
}
// Telefone legen ihr eigenes Vorschaubild in den EXIF-Block; der wird dadurch
// zehntausende Byte groß. Reicht der gelesene Anfang nicht bis zum Ende des
// Blocks, muss die Drehung trotzdem gefunden werden – sie steht vorn.
func TestOrientationBeiAbgeschnittenemBlock(t *testing.T) {
voll := jpegMitDrehung(t, 6, false)
// Absichtlich hinter dem Verzeichnis abschneiden, aber vor dem Dateiende.
for _, laenge := range []int{40, 60, 80} {
if laenge > len(voll) {
continue
}
if got := Orientation(voll[:laenge]); got != 6 {
t.Errorf("bei %d Byte Kopf: Drehung %d statt 6", laenge, got)
}
}
}