diff --git a/docs/superpowers/plans/2026-10-04-aircard-logos-sprache.md b/docs/superpowers/plans/2026-10-04-aircard-logos-sprache.md index 80ee3bd..516e8a9 100644 --- a/docs/superpowers/plans/2026-10-04-aircard-logos-sprache.md +++ b/docs/superpowers/plans/2026-10-04-aircard-logos-sprache.md @@ -33,7 +33,7 @@ - **Gespeicherte Karte mit gelöschtem/ausgeblendetem Logo**: Öffnen zeichnet die Karte ohne Logo, zeigt „Das gewählte Logo ist nicht mehr verfügbar.“, kein Fehler-Loop, Download funktioniert; Veröffentlichen bricht mit dem Text aus Spec § 2 ab. Tests in Task 5. - **Sprachwechsel mit ungespeichertem Editor-Entwurf**: Bild, Ausschnitt und Einstellungen bleiben nach dem Umschalten erhalten, die Seite wird neu aufgebaut, nicht neu geladen. Test in Task 1 (Router baut neu auf, Store bleibt) und Live in Task 12. - **Herz doppelt geklickt / Gast bricht Login ab**: Solange eine Herz-Anfrage für ein Logo läuft, sind weitere Klicks auf dieses Herz wirkungslos; bricht ein Gast den Login ab, geht keine Anfrage raus und das Herz bleibt aus, ohne Fehlertext. Tests in Task 8. -- **SVG ohne Größe oder mit riesiger viewBox**: Ohne eigene Größe gilt 1000 × 1000; gerastert wird höchstens mit 2048 px an der längsten Seite, nie größer; Skripte im SVG laufen nie (Laden nur als Bild). Test in Task 6 (`svgRasterSize`). +- **SVG ohne Größe oder mit riesiger viewBox**: Das Seitenverhältnis kommt aus der viewBox (ohne brauchbare viewBox: quadratisch, entspricht den 1000 × 1000 der Spec); gerastert wird immer mit genau 2048 px an der längsten Seite, nie größer; Skripte im SVG laufen nie (Laden nur als Bild). Test in Task 6 (`svgRasterSize`). --- @@ -281,7 +281,7 @@ export function validateLogoFile(f: { type: string; size: number; name: string }): { ok: true } | { ok: false; message: string }; export function trimBox(data: Uint8ClampedArray, w: number, h: number, threshold?: number): { x: number; y: number; w: number; h: number } | null; export function fitWithin(w: number, h: number, maxW: number, maxH: number): { w: number; h: number }; // nie > 1, gerundet, mind. 1 - export function svgRasterSize(naturalW: number, naturalH: number): { w: number; h: number }; // 0 → 1000×1000; längste Seite ≤ 2048 + export function svgRasterSize(aspect: number): { w: number; h: number }; // längste Seite genau 2048; ungültiges Seitenverhältnis → 2048 × 2048 export function hasTransparency(data: Uint8ClampedArray): boolean; export function prepareLogo(file: File): Promise; // wirft Error mit übersetztem Text (Spec § 4/§ 11) // logos/upload-logo.ts @@ -295,11 +295,11 @@ - `validateLogoFile`: `image/gif` → „Bitte ein PNG-, JPG-, WebP- oder SVG-Bild wählen.“; leerer Typ + Name `logo.SVG` → ok; 5 242 881 Byte → „Die Datei ist größer als 5 MB.“ - `trimBox`: 4 × 3-Bild nur mit einem Pixel (2,1) Alpha 9 → `{x:2,y:1,w:1,h:1}`; Alpha ≤ 8 überall → null; volles Bild → ganze Fläche. - `fitWithin(1440, 500, 720, 250)` → `{w:720,h:250}`; `(300, 100, 720, 250)` → unverändert; `(2000, 20, 720, 250)` → `{w:720,h:7}`. - - `svgRasterSize(0, 0)` → `{w:1000,h:1000}`; `(100000, 50000)` → `{w:2048,h:1024}`; `(300, 100)` → 2048 × 683 (gerundet). + - `svgRasterSize(1)` → `{w:2048,h:2048}`; `(2)` → `{w:2048,h:1024}`; `(3)` → `{w:2048,h:683}`; `(0.5)` → `{w:1024,h:2048}`; `(NaN)`/`(0)` → `{w:2048,h:2048}`. - `hasTransparency`: alles 255 → false; ein Alpha 254 → true. - `uploadPreparedLogo`: `createFile` mit Dateiname `logo.png`, Typ `image/png`, Rechten `['read("any")','delete("user:u1")']`; `uploadLogo` bekommt getrimmten Namen/Anzeigenamen, `mono` nur `true`, wenn `hasTransparency`; Erfolg → `CommunityLogo` mit `id`, `fileId = id`, `ownerId u1`, `likeCount 0`, keine Löschung; eindeutige Ablehnung (403) → Datei gelöscht, Fehler weitergeworfen; offener Ausgang (503) → Datei bleibt, Fehler weitergeworfen; Fehler beim Löschen überdeckt nie den eigentlichen Fehler. - [ ] **Step 2:** `cd web && npx vitest run src/logos/prepare-logo.test.ts src/logos/upload-logo.test.ts` → FAIL. -- [ ] **Step 3:** Umsetzen. `prepareLogo`: Rasterbilder mit `createImageBitmap(file)` (Maße vor dem Zeichnen gegen `LOGO_SOURCE_MAX_PIXELS` prüfen, sonst „Das Bild ist zu groß (max. 40 Megapixel).“), SVG über `Image` aus Object-URL (nur als Bild, Größe aus `svgRasterSize`); zeichnen, `getImageData`, `trimBox` (null → „Das Logo ist leer.“), ausschneiden, `fitWithin(…, 720, 250)`, unter 8 × 8 → „Das Logo ist zu klein (mindestens 8 × 8 Pixel).“, `toBlob('image/png')`, `hasTransparency` auf den Endpixeln; nicht lesbar → „Das Logo konnte nicht gelesen werden.“ Neue Texte in `en.ts`. (`prepareLogo` selbst prüft Task 12 live; die reinen Helfer tragen die Tests.) +- [ ] **Step 3:** Umsetzen. `prepareLogo`: Rasterbilder mit `createImageBitmap(file)` (Maße vor dem Zeichnen gegen `LOGO_SOURCE_MAX_PIXELS` prüfen, sonst „Das Bild ist zu groß (max. 40 Megapixel).“), SVG: Text lesen, Seitenverhältnis mit `svgAspect` aus `card/marks.ts` (viewBox), `width`/`height` aus `svgRasterSize` ins Wurzelelement schreiben (wie `withIntrinsicSize` in `marks.ts`), dann über `Image` aus einer Object-URL laden (nur als Bild); zeichnen, `getImageData`, `trimBox` (null → „Das Logo ist leer.“), ausschneiden, `fitWithin(…, 720, 250)`, unter 8 × 8 → „Das Logo ist zu klein (mindestens 8 × 8 Pixel).“, `toBlob('image/png')`, `hasTransparency` auf den Endpixeln; nicht lesbar → „Das Logo konnte nicht gelesen werden.“ Neue Texte in `en.ts`. (`prepareLogo` selbst prüft Task 12 live; die reinen Helfer tragen die Tests.) - [ ] **Step 4:** `cd web && npx vitest run && npx tsc --noEmit` → grün. - [ ] **Step 5:** Commit `feat(logos): Logo zuschneiden, verkleinern und hochladen`.