feat: custom formats (WxH/stickerN), JPG output option, failed-job status
- 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
This commit is contained in:
@@ -52,6 +52,8 @@ async function deliverableBuffer(
|
||||
buf: Buffer, filename: string, hasAlpha: boolean,
|
||||
): Promise<{ buf: Buffer; name: string }> {
|
||||
if (hasAlpha) return { buf, name: filename };
|
||||
// Schon JPEG (globales/Rezept-Format = jpg)? Dann unverändert lassen.
|
||||
if (buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) return { buf, name: filename };
|
||||
try {
|
||||
const sharp = (await import('sharp')).default;
|
||||
const meta = await sharp(buf, { failOn: 'none' }).metadata();
|
||||
|
||||
+11
-1
@@ -45,7 +45,17 @@ export function resolveDimensions(input: ResolveInput): Dimensions | null {
|
||||
let cm: [number, number] | undefined;
|
||||
if (format in CM) cm = CM[format];
|
||||
else if (format === 'sticker' || format === 'custom') cm = input.customCm ?? [5, 5];
|
||||
if (!cm) throw new Error(`Unbekanntes Format: ${format}`);
|
||||
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;
|
||||
|
||||
+18
-2
@@ -22,6 +22,7 @@ interface RecipeSnapshot {
|
||||
prompt_text?: string | null; // Beschreibung für compose/generate
|
||||
delivery?: 'library' | 'picdrop' | 'both';
|
||||
picdrop_gallery?: string | null;
|
||||
output_ext?: 'png' | 'jpg' | null; // Dateiformat-Override; sonst globale Einstellung
|
||||
}
|
||||
|
||||
/** Welches Modell-ID + Alpha-Fähigkeit? Aus models-Tabelle, sonst Default. */
|
||||
@@ -130,8 +131,23 @@ export async function processItem(itemId: string): Promise<ProcessResult> {
|
||||
hasAlpha = true;
|
||||
}
|
||||
|
||||
// Ausgabeformat: Rezept-Override, sonst globale Einstellung. JPG nur ohne Transparenz.
|
||||
const gset = await one<{ output_ext: string | null }>('SELECT output_ext FROM settings WHERE id=1');
|
||||
const wantExt = (r.output_ext || gset?.output_ext || 'png').toLowerCase();
|
||||
let ext = 'png';
|
||||
let contentType = 'image/png';
|
||||
if (wantExt === 'jpg' && !hasAlpha) {
|
||||
outBuf = await sharp(outBuf, { failOn: 'none' })
|
||||
.flatten({ background: '#ffffff' })
|
||||
.withMetadata({ density: dpi })
|
||||
.jpeg({ quality: 92, mozjpeg: true, chromaSubsampling: '4:4:4' })
|
||||
.toBuffer();
|
||||
ext = 'jpg';
|
||||
contentType = 'image/jpeg';
|
||||
}
|
||||
|
||||
const key = resultKey(item.id);
|
||||
await putObject(key, outBuf, 'image/png');
|
||||
await putObject(key, outBuf, contentType);
|
||||
|
||||
// Vorschaubild (optional, laut Einstellung)
|
||||
const cfg = await one<{ make_thumbnails: boolean; keep_sources: boolean; nas_enabled: boolean }>(
|
||||
@@ -145,7 +161,7 @@ export async function processItem(itemId: string): Promise<ProcessResult> {
|
||||
} catch (e) { console.error('[process] Thumbnail fehlgeschlagen', e); }
|
||||
}
|
||||
|
||||
const filename = buildResultFilename(item.filename, formatToken(r.output_format), 'png');
|
||||
const filename = buildResultFilename(item.filename, formatToken(r.output_format), ext);
|
||||
const outputPx = `${fin.width}x${fin.height}`;
|
||||
// Private Aufträge: kein Delivery, kein Backup, kein gespeicherter Prompt.
|
||||
const deliveryStatus = (!isPrivate && (r.delivery === 'picdrop' || r.delivery === 'both')) ? 'pending' : 'none';
|
||||
|
||||
Reference in New Issue
Block a user