feat: all AI formats printable, crop-vs-border rule, mobile layout, Passbilder rename

Print now offers every format the AI pipeline knows (9x13 to 60x90, DIN A6-A2,
squares, poster/frame sizes to 70x100, and the screen ratios as physical sizes).
When a picture does not match the target ratio the user picks per image between
cropping to fill and keeping the whole picture on a border colour - never a
stretch. The module is called Passbilder now; /druck redirects.

Fixes two real defects: sharp runs extend after resize, so padded cells came out
oversized (a 35x45 mm cell became 35x171 mm with a border), and the grid packer
rotated a single portrait photo just because more would fit sideways.

Mobile: cards become rows, touch targets ~40px, crop editor as a bottom sheet.
Verified at 390/820/1440px - all three produce the same PDF.
This commit is contained in:
2026-08-18 07:04:08 +00:00
parent f6b81a0f5e
commit 7d42782c41
17 changed files with 416 additions and 90 deletions
+23 -8
View File
@@ -29,27 +29,35 @@ export interface PhotoSize { id: string; label: string; w: number; h: number; gr
export const PHOTO_SIZES: PhotoSize[] = [
// Passbild / Ausweis
{ id: 'P35x45', label: 'Biometrisches Passbild 35 × 45 mm', w: 35, h: 45, group: 'Passbild' },
{ id: 'P50x50', label: 'Visum USA 50 × 50 mm (2 × 2 in)', w: 50.8, h: 50.8, group: 'Passbild' },
{ id: 'P35x45', label: 'Passbild 35 × 45 mm (biometrisch)', w: 35, h: 45, group: 'Passbild' },
{ id: 'P50x50', label: 'Visum USA 51 × 51 mm (2 × 2 in)', w: 50.8, h: 50.8, group: 'Passbild' },
// Kleinformate (Kita, Schule, Portemonnaie)
{ id: 'K20x30', label: '2 × 3 cm', w: 20, h: 30, group: 'Klein' },
{ id: 'K30x40', label: '3 × 4 cm', w: 30, h: 40, group: 'Klein' },
{ id: 'K40x50', label: '4 × 5 cm', w: 40, h: 50, group: 'Klein' },
{ id: 'K45x60', label: '4,5 × 6 cm', w: 45, h: 60, group: 'Klein' },
{ id: 'K60x80', label: '6 × 8 cm', w: 60, h: 80, group: 'Klein' },
{ id: 'K60x90', label: '6 × 9 cm', w: 60, h: 90, group: 'Klein' },
// Klassische Fotoformate
// Klassische Fotoformate — deckungsgleich mit den KI-Formaten aus src/lib/format.ts
{ id: 'S9x13', label: '9 × 13 cm', w: 90, h: 130, group: 'Foto' },
{ id: 'S10x15', label: '10 × 15 cm', w: 100, h: 150, group: 'Foto' },
{ id: 'S11x15', label: '11 × 15 cm', w: 110, h: 150, group: 'Foto' },
{ id: 'S12x15', label: '12 × 15 cm', w: 120, h: 150, group: 'Foto' },
{ id: 'S13x18', label: '13 × 18 cm', w: 130, h: 180, group: 'Foto' },
{ id: 'S15x20', label: '15 × 20 cm', w: 150, h: 200, group: 'Foto' },
{ id: 'S18x24', label: '18 × 24 cm', w: 180, h: 240, group: 'Foto' },
{ id: 'S20x25', label: '20 × 25 cm', w: 200, h: 250, group: 'Foto' },
{ id: 'S20x30', label: '20 × 30 cm', w: 200, h: 300, group: 'Foto' },
{ id: 'S30x40', label: '30 × 40 cm', w: 300, h: 400, group: 'Foto' },
{ id: 'S30x45', label: '30 × 45 cm', w: 300, h: 450, group: 'Foto' },
{ id: 'S40x50', label: '40 × 50 cm', w: 400, h: 500, group: 'Foto' },
{ id: 'S40x60', label: '40 × 60 cm', w: 400, h: 600, group: 'Foto' },
{ id: 'S50x70', label: '50 × 70 cm', w: 500, h: 700, group: 'Foto' },
{ id: 'S24x30', label: '24 × 30 cm', w: 240, h: 300, group: 'Foto' },
// Poster- und Rahmenformate (Bilderrahmen im Handel)
{ id: 'R30x40', label: '30 × 40 cm', w: 300, h: 400, group: 'Poster & Rahmen' },
{ id: 'R30x45', label: '30 × 45 cm', w: 300, h: 450, group: 'Poster & Rahmen' },
{ id: 'R40x50', label: '40 × 50 cm', w: 400, h: 500, group: 'Poster & Rahmen' },
{ id: 'R40x60', label: '40 × 60 cm', w: 400, h: 600, group: 'Poster & Rahmen' },
{ id: 'R50x70', label: '50 × 70 cm', w: 500, h: 700, group: 'Poster & Rahmen' },
{ id: 'R60x80', label: '60 × 80 cm', w: 600, h: 800, group: 'Poster & Rahmen' },
{ id: 'R60x90', label: '60 × 90 cm', w: 600, h: 900, group: 'Poster & Rahmen' },
{ id: 'R70x100', label: '70 × 100 cm', w: 700, h: 1000, group: 'Poster & Rahmen' },
// Quadrate
{ id: 'Q10x10', label: '10 × 10 cm', w: 100, h: 100, group: 'Quadrat' },
{ id: 'Q13x13', label: '13 × 13 cm', w: 130, h: 130, group: 'Quadrat' },
@@ -60,6 +68,13 @@ export const PHOTO_SIZES: PhotoSize[] = [
{ id: 'DA5', label: 'DIN A5 (14,8 × 21 cm)', w: 148, h: 210, group: 'DIN' },
{ id: 'DA4', label: 'DIN A4 (21 × 29,7 cm)', w: 210, h: 297, group: 'DIN' },
{ id: 'DA3', label: 'DIN A3 (29,7 × 42 cm)', w: 297, h: 420, group: 'DIN' },
{ id: 'DA2', label: 'DIN A2 (42 × 59,4 cm)', w: 420, h: 594, group: 'DIN' },
// Bildschirm-Seitenverhältnisse als Druckmaß (The Frame & Co.)
{ id: 'W16x9', label: '16:9 „The Frame" (30 cm breit)', w: 300, h: 168.75, group: 'Seitenverhältnis' },
{ id: 'W9x16', label: '9:16 Hochformat (30 cm hoch)', w: 168.75, h: 300, group: 'Seitenverhältnis' },
{ id: 'W3x2', label: '3:2 (auf 30 cm)', w: 300, h: 200, group: 'Seitenverhältnis' },
{ id: 'W4x3', label: '4:3 (auf 30 cm)', w: 300, h: 225, group: 'Seitenverhältnis' },
{ id: 'W5x4', label: '5:4 (auf 30 cm)', w: 300, h: 240, group: 'Seitenverhältnis' },
];
export const paperById = (id: string) => PAPERS.find((p) => p.id === id) || null;