3a061aa612
- format.ts parses free formats from the key: sticker<N> -> N×N cm, <W>x<H> -> W×H cm. Fixes 'Unbekanntes Format: sticker5' (the seeded 'Sticker 5 cm' recipe was unusable) and lets admins define ANY format without a migration. Guarded to <=300 cm. - Output file format PNG/JPG: global default (settings.output_ext) plus per-recipe/per-conversion override. JPG only for non-alpha; cutouts stay PNG. file.ts sniffs content-type from magic bytes. - Jobs can now be 'failed' (all items failed) -> red in the queue; add finished time to queue list + detail. migration 012. - Studio: custom-measure input + Dateiformat selector; Admin: global Standard-Dateiformat. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
78 lines
3.1 KiB
TypeScript
78 lines
3.1 KiB
TypeScript
// Zielformate & exakte Pixelberechnung (siehe 01 §5, 06 §3).
|
||
// px = round(cm / 2.54 * dpi)
|
||
|
||
export type Orientation = 'portrait' | 'landscape';
|
||
|
||
export interface Dimensions { w: number; h: number; cm?: [number, number]; label: string; }
|
||
|
||
// cm-Formate: [Breite, Höhe] in Hochformat-Konvention (kurze Seite × lange Seite)
|
||
const CM: Record<string, [number, number]> = {
|
||
'9x13': [9, 13], '10x15': [10, 15], '13x18': [13, 18], '15x20': [15, 20],
|
||
'20x30': [20, 30], '30x40': [30, 40], '30x45': [30, 45], '40x50': [40, 50],
|
||
'40x60': [40, 60], '50x70': [50, 70], '60x90': [60, 90],
|
||
A4: [21, 29.7], A3: [29.7, 42], A2: [42, 59.4],
|
||
'20x20': [20, 20], '30x30': [30, 30],
|
||
};
|
||
|
||
// Reine Bildschirmformate (feste Pixel, kein cm/dpi)
|
||
const SCREEN: Record<string, [number, number]> = {
|
||
theframe: [3840, 2160], // 16:9 quer
|
||
hochformat: [2160, 3840], // 9:16 hoch
|
||
};
|
||
|
||
export const cmToPx = (cm: number, dpi: number) => Math.round((cm / 2.54) * dpi);
|
||
|
||
export interface ResolveInput {
|
||
format: string; // Schlüssel oben, 'sticker', 'custom' oder 'keep'
|
||
orientation?: Orientation;
|
||
dpi?: number;
|
||
customCm?: [number, number]; // für 'sticker'/'custom'
|
||
}
|
||
|
||
/** Liefert exakte Zielpixel + Label. `keep` → null (Original behalten). */
|
||
export function resolveDimensions(input: ResolveInput): Dimensions | null {
|
||
const { format } = input;
|
||
const dpi = input.dpi ?? 300;
|
||
const orient = input.orientation ?? 'portrait';
|
||
|
||
if (format === 'keep') return null;
|
||
|
||
if (format in SCREEN) {
|
||
const [w, h] = SCREEN[format];
|
||
return { w, h, label: format === 'theframe' ? 'The Frame' : 'Hochformat' };
|
||
}
|
||
|
||
let cm: [number, number] | undefined;
|
||
if (format in CM) cm = CM[format];
|
||
else if (format === 'sticker' || format === 'custom') cm = input.customCm ?? [5, 5];
|
||
else {
|
||
// Frei definierte Formate direkt aus dem Schlüssel lesen — keine DB-Migration nötig:
|
||
// „sticker5" / „sticker-7,5" → quadratisch N×N cm; „25x35" / „25×35" → B×H cm.
|
||
const sq = /^sticker[-_ ]?(\d+(?:[.,]\d+)?)$/i.exec(format);
|
||
const wh = /^(\d+(?:[.,]\d+)?)\s*[x×*]\s*(\d+(?:[.,]\d+)?)$/i.exec(format);
|
||
const num = (s: string) => parseFloat(s.replace(',', '.'));
|
||
if (sq) { const n = num(sq[1]); cm = [n, n]; }
|
||
else if (wh) cm = [num(wh[1]), num(wh[2])];
|
||
}
|
||
if (!cm || !cm[0] || !cm[1] || cm[0] > 300 || cm[1] > 300)
|
||
throw new Error(`Unbekanntes Format: ${format}`);
|
||
|
||
// Hochformat-Konvention → gewünschte Orientierung anwenden
|
||
let [wCm, hCm] = cm;
|
||
if (orient === 'landscape') [wCm, hCm] = [hCm, wCm];
|
||
return { w: cmToPx(wCm, dpi), h: cmToPx(hCm, dpi), cm: [wCm, hCm],
|
||
label: `${cm[0]}×${cm[1]} cm` };
|
||
}
|
||
|
||
/** Vereinfachtes Seitenverhältnis als "b:h" für die Bild-API. */
|
||
export function aspectRatio(w: number, h: number): string {
|
||
const g = gcd(w, h);
|
||
return `${Math.round(w / g)}:${Math.round(h / g)}`;
|
||
}
|
||
function gcd(a: number, b: number): number { return b === 0 ? a : gcd(b, a % b); }
|
||
|
||
/** Reicht die native Auflösung für echte 300 dpi? (Hinweis in der UI.) */
|
||
export function upscaleWarning(srcW: number, srcH: number, target: Dimensions): boolean {
|
||
return srcW < target.w * 0.95 || srcH < target.h * 0.95;
|
||
}
|