Compare commits
2 Commits
21a0a68972
...
f0442a617c
| Author | SHA1 | Date | |
|---|---|---|---|
| f0442a617c | |||
| 4fb34cdab8 |
@@ -33,6 +33,12 @@ All notable changes to Klarbild are documented here. Newest first.
|
|||||||
button-driven, returns an A4 PDF), and the MCP tools `exact_size` / `print_sheet`.
|
button-driven, returns an A4 PDF), and the MCP tools `exact_size` / `print_sheet`.
|
||||||
- **API** — `POST /api/print/single`, `GET /api/print/single?size=…` (mm → px calculator),
|
- **API** — `POST /api/print/single`, `GET /api/print/single?size=…` (mm → px calculator),
|
||||||
`POST /api/print/sheet`, `GET|POST|DELETE /api/print/presets`. Documented in `/llms.txt`.
|
`POST /api/print/sheet`, `GET|POST|DELETE /api/print/presets`. Documented in `/llms.txt`.
|
||||||
|
- **Seeded sheet templates** — "Kita-Satz (A4)", "Schulsatz klein (A4)", "Passbildbogen 35×45 (A4)"
|
||||||
|
and "2 × 10×7,5 auf Fotopapier 10×15". Loading a multi-format template with a single image
|
||||||
|
clones that image into every format, so one click produces the whole set.
|
||||||
|
- **Margin profiles** — borderless (0 mm), standard (5 mm) and safe (10 mm) as one-click buttons.
|
||||||
|
- **Deliver the sheet** — the finished PDF can additionally be pushed to Picdrop, the NAS or any
|
||||||
|
extra delivery target (`deliver` in the sheet request).
|
||||||
- **Tests** — `tests/printlayout.test.ts` (`npm test`) covers size parsing, exact mm → px,
|
- **Tests** — `tests/printlayout.test.ts` (`npm test`) covers size parsing, exact mm → px,
|
||||||
non-overlap, margins, bleed spacing, mark geometry and multi-sheet paging.
|
non-overlap, margins, bleed spacing, mark geometry and multi-sheet paging.
|
||||||
|
|
||||||
|
|||||||
+51
-11
@@ -70,6 +70,9 @@ export default function PrintApp() {
|
|||||||
const [ext, setExt] = useState<'jpg' | 'png'>('jpg');
|
const [ext, setExt] = useState<'jpg' | 'png'>('jpg');
|
||||||
const [center, setCenter] = useState(true);
|
const [center, setCenter] = useState(true);
|
||||||
const [footer, setFooter] = useState(true);
|
const [footer, setFooter] = useState(true);
|
||||||
|
const [deliverTo, setDeliverTo] = useState<string>(''); // '' = nur herunterladen
|
||||||
|
const [gallery, setGallery] = useState('');
|
||||||
|
const [targets, setTargets] = useState<any[]>([]);
|
||||||
|
|
||||||
const [editing, setEditing] = useState<string | null>(null);
|
const [editing, setEditing] = useState<string | null>(null);
|
||||||
const [picker, setPicker] = useState(false);
|
const [picker, setPicker] = useState(false);
|
||||||
@@ -149,7 +152,10 @@ export default function PrintApp() {
|
|||||||
return () => window.removeEventListener('paste', onPaste);
|
return () => window.removeEventListener('paste', onPaste);
|
||||||
}, [addFiles]);
|
}, [addFiles]);
|
||||||
|
|
||||||
useEffect(() => { loadPresets(); }, []);
|
useEffect(() => {
|
||||||
|
loadPresets();
|
||||||
|
fetch('/api/delivery-targets').then((r) => r.json()).then((j) => setTargets(j.targets || [])).catch(() => {});
|
||||||
|
}, []);
|
||||||
const loadPresets = () => fetch('/api/print/presets').then((r) => r.json())
|
const loadPresets = () => fetch('/api/print/presets').then((r) => r.json())
|
||||||
.then((j) => setPresets(j.presets || [])).catch(() => {});
|
.then((j) => setPresets(j.presets || [])).catch(() => {});
|
||||||
|
|
||||||
@@ -183,6 +189,7 @@ export default function PrintApp() {
|
|||||||
paper: paperId === 'custom' ? { size: paperCustom } : { id: paperId },
|
paper: paperId === 'custom' ? { size: paperCustom } : { id: paperId },
|
||||||
landscape: paperLandscape,
|
landscape: paperLandscape,
|
||||||
marginMm, gapMm: gapEff, bleedMm, center, dpi, ext, footer,
|
marginMm, gapMm: gapEff, bleedMm, center, dpi, ext, footer,
|
||||||
|
...(deliverTo ? { deliver: { target: deliverTo === 'picdrop' ? '' : deliverTo, gallery: gallery.trim() || undefined } } : {}),
|
||||||
marks: { mode: marks, lengthMm: markLen, offsetMm: markOff },
|
marks: { mode: marks, lengthMm: markLen, offsetMm: markOff },
|
||||||
cells: cells.filter((c) => c.src && !c.error && sizeOfCell(c)).map((c) => {
|
cells: cells.filter((c) => c.src && !c.error && sizeOfCell(c)).map((c) => {
|
||||||
const s = sizeOfCell(c)!;
|
const s = sizeOfCell(c)!;
|
||||||
@@ -200,7 +207,9 @@ export default function PrintApp() {
|
|||||||
if (!res.ok) { notify((await res.json().catch(() => ({}))).error || 'Fehlgeschlagen.'); return; }
|
if (!res.ok) { notify((await res.json().catch(() => ({}))).error || 'Fehlgeschlagen.'); return; }
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
download(blob, `klarbild-druckbogen-${new Date().toISOString().slice(0, 10)}.pdf`);
|
download(blob, `klarbild-druckbogen-${new Date().toISOString().slice(0, 10)}.pdf`);
|
||||||
notify(`PDF erzeugt · ${res.headers.get('X-Klarbild-Pages') || '?'} Bogen. Beim Drucken „Tatsächliche Größe / 100 %" wählen.`);
|
const dmsgRaw = res.headers.get('X-Klarbild-Delivery-Msg');
|
||||||
|
const dmsg = dmsgRaw ? ` ${decodeURIComponent(dmsgRaw)}` : '';
|
||||||
|
notify(`PDF erzeugt · ${res.headers.get('X-Klarbild-Pages') || '?'} Bogen. Beim Drucken „Tatsächliche Größe / 100 %" wählen.${dmsg}`);
|
||||||
} catch { notify('Netzwerkfehler.'); } finally { setBusy(null); }
|
} catch { notify('Netzwerkfehler.'); } finally { setBusy(null); }
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -227,7 +236,7 @@ export default function PrintApp() {
|
|||||||
if (!name?.trim()) return;
|
if (!name?.trim()) return;
|
||||||
const config = {
|
const config = {
|
||||||
paperId, paperCustom, paperLandscape, marginMm, autoGap, gapMm, bleedMm,
|
paperId, paperCustom, paperLandscape, marginMm, autoGap, gapMm, bleedMm,
|
||||||
marks, markLen, markOff, dpi, ext, center, footer,
|
marks, markLen, markOff, dpi, ext, center, footer, deliverTo, gallery,
|
||||||
formats: cells.map((c) => ({ sizeId: c.sizeId, customSize: c.customSize, landscape: c.landscape, count: c.count, allowRotate: c.allowRotate })),
|
formats: cells.map((c) => ({ sizeId: c.sizeId, customSize: c.customSize, landscape: c.landscape, count: c.count, allowRotate: c.allowRotate })),
|
||||||
};
|
};
|
||||||
await fetch('/api/print/presets', {
|
await fetch('/api/print/presets', {
|
||||||
@@ -243,16 +252,26 @@ export default function PrintApp() {
|
|||||||
setMarginMm(c.marginMm ?? 5); setAutoGap(c.autoGap !== false); setGapMm(c.gapMm ?? 12);
|
setMarginMm(c.marginMm ?? 5); setAutoGap(c.autoGap !== false); setGapMm(c.gapMm ?? 12);
|
||||||
setBleedMm(c.bleedMm ?? 0); setMarks(c.marks ?? 'corner'); setMarkLen(c.markLen ?? 4); setMarkOff(c.markOff ?? 3);
|
setBleedMm(c.bleedMm ?? 0); setMarks(c.marks ?? 'corner'); setMarkLen(c.markLen ?? 4); setMarkOff(c.markOff ?? 3);
|
||||||
setDpi(c.dpi ?? 300); setExt(c.ext === 'png' ? 'png' : 'jpg'); setCenter(c.center !== false); setFooter(c.footer !== false);
|
setDpi(c.dpi ?? 300); setExt(c.ext === 'png' ? 'png' : 'jpg'); setCenter(c.center !== false); setFooter(c.footer !== false);
|
||||||
// Formate auf die vorhandenen Bilder anwenden (der Reihe nach).
|
setDeliverTo(c.deliverTo ?? ''); setGallery(c.gallery ?? '');
|
||||||
|
// Formate anwenden. Der typische Fall ist ein Bild und mehrere Größen
|
||||||
|
// („Kita-Satz": 1× 13×18, 2× 9×13, 8× Passbild) — dann wird das Bild geklont.
|
||||||
if (Array.isArray(c.formats) && c.formats.length) {
|
if (Array.isArray(c.formats) && c.formats.length) {
|
||||||
setCells((prev) => prev.map((cell, i) => {
|
setCells((prev) => {
|
||||||
const f = c.formats[Math.min(i, c.formats.length - 1)];
|
if (!prev.length) return prev;
|
||||||
const next = { ...cell, sizeId: f.sizeId, customSize: f.customSize || '', landscape: !!f.landscape, count: f.count || 1, allowRotate: f.allowRotate !== false };
|
const apply = (cell: Cell, f: any): Cell => {
|
||||||
const a = aspectOf(next); if (a) next.crop = coverCrop(next.natW, next.natH, a, cell.crop);
|
const next = { ...cell, id: cell.id, sizeId: f.sizeId, customSize: f.customSize || '',
|
||||||
return next;
|
landscape: !!f.landscape, count: f.count || 1, allowRotate: f.allowRotate !== false };
|
||||||
}));
|
const a = aspectOf(next); if (a) next.crop = coverCrop(next.natW, next.natH, a, cell.crop);
|
||||||
|
return next;
|
||||||
|
};
|
||||||
|
if (prev.length === 1 && c.formats.length > 1)
|
||||||
|
return c.formats.map((f: any, i: number) => apply({ ...prev[0], id: i === 0 ? prev[0].id : uid() }, f));
|
||||||
|
return prev.map((cell, i) => apply(cell, c.formats[Math.min(i, c.formats.length - 1)]));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
notify('Vorlage geladen.');
|
notify(Array.isArray(c.formats) && c.formats.length > 1
|
||||||
|
? 'Vorlage geladen — das Bild wurde für jedes Format übernommen.'
|
||||||
|
: 'Vorlage geladen.');
|
||||||
};
|
};
|
||||||
|
|
||||||
const deletePreset = async (id: string) => {
|
const deletePreset = async (id: string) => {
|
||||||
@@ -368,6 +387,11 @@ export default function PrintApp() {
|
|||||||
|
|
||||||
<div className="feld">
|
<div className="feld">
|
||||||
<label>Ränder & Abstände</label>
|
<label>Ränder & Abstände</label>
|
||||||
|
<div className="schalter zart">
|
||||||
|
<button className={marginMm === 0 ? 'an' : ''} onClick={() => setMarginMm(0)}>Randlos</button>
|
||||||
|
<button className={marginMm === 5 ? 'an' : ''} onClick={() => setMarginMm(5)}>Standard 5 mm</button>
|
||||||
|
<button className={marginMm === 10 ? 'an' : ''} onClick={() => setMarginMm(10)}>Sicher 10 mm</button>
|
||||||
|
</div>
|
||||||
<div className="reihe zwei">
|
<div className="reihe zwei">
|
||||||
<label className="mini">Rand zum Papier (mm)
|
<label className="mini">Rand zum Papier (mm)
|
||||||
<input className="input" type="number" min={0} max={40} step={0.5} value={marginMm}
|
<input className="input" type="number" min={0} max={40} step={0.5} value={marginMm}
|
||||||
@@ -412,6 +436,21 @@ export default function PrintApp() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="feld">
|
||||||
|
<label>Wohin?</label>
|
||||||
|
<select className="select" value={deliverTo} onChange={(e) => setDeliverTo(e.target.value)}>
|
||||||
|
<option value="">Nur herunterladen</option>
|
||||||
|
<option value="picdrop">Zusätzlich an Picdrop (Standard)</option>
|
||||||
|
<option value="nas">Zusätzlich aufs NAS</option>
|
||||||
|
{targets.map((t) => <option key={t.id} value={t.id}>Zusätzlich an „{t.name}"</option>)}
|
||||||
|
</select>
|
||||||
|
{deliverTo && (
|
||||||
|
<input className="input" style={{ marginTop: 7 }} value={gallery} onChange={(e) => setGallery(e.target.value)}
|
||||||
|
placeholder="Galerie/Ordner (leer = Standard)" />
|
||||||
|
)}
|
||||||
|
<div className="fein">Der fertige Bogen wird als PDF geladen — auf Wunsch zusätzlich ins gewohnte Ziel gelegt.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button className="knopf" disabled={!plan?.pages.length || busy === 'pdf'} onClick={makePdf}>
|
<button className="knopf" disabled={!plan?.pages.length || busy === 'pdf'} onClick={makePdf}>
|
||||||
{busy === 'pdf' ? <><span className="spin" />PDF wird gebaut …</> : 'Druck-PDF erzeugen'}
|
{busy === 'pdf' ? <><span className="spin" />PDF wird gebaut …</> : 'Druck-PDF erzeugen'}
|
||||||
</button>
|
</button>
|
||||||
@@ -722,6 +761,7 @@ function PrintStyles() {
|
|||||||
.schalter{display:flex;gap:6px;margin-top:8px;}
|
.schalter{display:flex;gap:6px;margin-top:8px;}
|
||||||
.schalter button{flex:1;background:#fff;border:1px solid var(--line);border-radius:3px;padding:8px;cursor:pointer;font-family:inherit;font-size:13px;color:var(--soft);}
|
.schalter button{flex:1;background:#fff;border:1px solid var(--line);border-radius:3px;padding:8px;cursor:pointer;font-family:inherit;font-size:13px;color:var(--soft);}
|
||||||
.schalter button.an{border-color:var(--accent);background:var(--accent-bg);color:var(--ink);font-weight:600;}
|
.schalter button.an{border-color:var(--accent);background:var(--accent-bg);color:var(--ink);font-weight:600;}
|
||||||
|
.schalter.zart button{font-size:12px;padding:6px 4px;}
|
||||||
.schalter.winzig{margin:0;flex:1;}
|
.schalter.winzig{margin:0;flex:1;}
|
||||||
.schalter.winzig button{padding:5px 4px;font-size:11.5px;}
|
.schalter.winzig button{padding:5px 4px;font-size:11.5px;}
|
||||||
.pbtn{background:#fff;border:1px solid var(--line);border-radius:3px;padding:8px 10px;cursor:pointer;font-family:inherit;font-size:12px;color:var(--ink);white-space:nowrap;}
|
.pbtn{background:#fff;border:1px solid var(--line);border-radius:3px;padding:8px 10px;cursor:pointer;font-family:inherit;font-size:12px;color:var(--ink);white-space:nowrap;}
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ async function galleryFor(item: { folder_id: string | null; job_id: string }): P
|
|||||||
return s?.picdrop_default_gallery || DEFAULT_GALLERY;
|
return s?.picdrop_default_gallery || DEFAULT_GALLERY;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cfgForKey(key: string | null | undefined): Promise<PicdropCfg | null> {
|
export async function cfgForKey(key: string | null | undefined): Promise<PicdropCfg | null> {
|
||||||
if (!key) return null;
|
if (!key) return null;
|
||||||
if (key === 'nas') { const { loadNasConfig } = await import('./nas'); return (await loadNasConfig()) as unknown as PicdropCfg; }
|
if (key === 'nas') { const { loadNasConfig } = await import('./nas'); return (await loadNasConfig()) as unknown as PicdropCfg; }
|
||||||
if (key === 'picdrop') return loadConfig();
|
if (key === 'picdrop') return loadConfig();
|
||||||
|
|||||||
@@ -54,6 +54,37 @@ export async function seed(): Promise<void> {
|
|||||||
{ contour_mm: 3 });
|
{ contour_mm: 3 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Druck-Vorlagen (Bogen ohne KI) — idempotent über den Namen, auch für bestehende Installationen.
|
||||||
|
{
|
||||||
|
const admin = await one<{ id: string }>(`SELECT id FROM users WHERE username='till'`);
|
||||||
|
const by = admin?.id ?? null;
|
||||||
|
const P = async (name: string, config: Record<string, unknown>) => {
|
||||||
|
const exists = await one(`SELECT id FROM print_presets WHERE name=$1`, [name]);
|
||||||
|
if (exists) return;
|
||||||
|
await query(`INSERT INTO print_presets (name, created_by, config) VALUES ($1,$2,$3)`,
|
||||||
|
[name, by, JSON.stringify(config)]);
|
||||||
|
};
|
||||||
|
const sheet = {
|
||||||
|
paperId: 'A4', paperCustom: '', paperLandscape: false, marginMm: 5,
|
||||||
|
autoGap: true, gapMm: 14, bleedMm: 0,
|
||||||
|
marks: 'corner', markLen: 4, markOff: 3, dpi: 300, ext: 'jpg', center: true, footer: true,
|
||||||
|
};
|
||||||
|
const F = (sizeId: string, count: number, landscape = false) =>
|
||||||
|
({ sizeId, customSize: '', landscape, count, allowRotate: true });
|
||||||
|
|
||||||
|
// Der klassische Kita-/Schulfoto-Satz aus einem Bild.
|
||||||
|
await P('Kita-Satz (A4)', { ...sheet, formats: [F('S13x18', 1), F('S9x13', 2), F('P35x45', 8)] });
|
||||||
|
// Nur die kleinen Abzüge, wenn der große schon gedruckt ist.
|
||||||
|
await P('Schulsatz klein (A4)', { ...sheet, formats: [F('S9x13', 4), F('K30x40', 8)] });
|
||||||
|
// Voller Passbildbogen — durchgehende Linien, weil alles gleich groß ist.
|
||||||
|
await P('Passbildbogen 35×45 (A4)', { ...sheet, marks: 'grid', autoGap: false, gapMm: 4, formats: [F('P35x45', 20)] });
|
||||||
|
// Zwei Bilder in Originalgröße auf ein Blatt 10×15-Fotopapier.
|
||||||
|
await P('2 × 10×7,5 auf Fotopapier 10×15', {
|
||||||
|
...sheet, paperId: 'F10x15', marginMm: 0, marks: 'grid', autoGap: false, gapMm: 0,
|
||||||
|
formats: [{ sizeId: 'custom', customSize: '10x7,5', landscape: false, count: 2, allowRotate: true }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// The-Frame-Rezept auf die eigene Galerie verdrahten (auch für bestehende Installationen).
|
// The-Frame-Rezept auf die eigene Galerie verdrahten (auch für bestehende Installationen).
|
||||||
await query(
|
await query(
|
||||||
`UPDATE recipes SET picdrop_gallery='TheFrame-Backgrounds', delivery='both'
|
`UPDATE recipes SET picdrop_gallery='TheFrame-Backgrounds', delivery='both'
|
||||||
|
|||||||
@@ -85,8 +85,16 @@ import Base from '../layouts/Base.astro';
|
|||||||
schiefem Schnitt kein weißer Rand entsteht. Der Abstand zwischen zwei Bildern wächst automatisch mit.</li>
|
schiefem Schnitt kein weißer Rand entsteht. Der Abstand zwischen zwei Bildern wächst automatisch mit.</li>
|
||||||
<li><b>Einzeln herunterladen</b>: ein Bild exakt auf Maß als PNG/JPG mit dpi-Metadaten — ohne Bogen.</li>
|
<li><b>Einzeln herunterladen</b>: ein Bild exakt auf Maß als PNG/JPG mit dpi-Metadaten — ohne Bogen.</li>
|
||||||
<li><b>Auflösungswarnung</b>: reicht die Quelle nicht für die gewählte dpi, steht es rot unter dem Bild.</li>
|
<li><b>Auflösungswarnung</b>: reicht die Quelle nicht für die gewählte dpi, steht es rot unter dem Bild.</li>
|
||||||
<li><b>Vorlagen</b>: die ganze Bogen-Einstellung (Papier, Marken, Formate, Stückzahlen) speichern —
|
<li><b>Vorlagen</b>: die ganze Bogen-Einstellung (Papier, Marken, Formate, Stückzahlen) speichern.
|
||||||
z. B. „Kita-Satz Felix" oder „8 × Passbild".</li>
|
Mitgeliefert sind <b>Kita-Satz (A4)</b> (1 × 13 × 18, 2 × 9 × 13, 8 × Passbild),
|
||||||
|
<b>Schulsatz klein (A4)</b>, <b>Passbildbogen 35 × 45 (A4)</b> und
|
||||||
|
<b>2 × 10 × 7,5 auf Fotopapier 10 × 15</b>. Lädst du eine mehrformatige Vorlage bei nur
|
||||||
|
<i>einem</i> Bild, wird das Bild automatisch für jedes Format übernommen — ein Klick,
|
||||||
|
fertiger Satz.</li>
|
||||||
|
<li><b>Randprofile</b>: <b>Randlos</b> (0 mm), <b>Standard</b> (5 mm) oder <b>Sicher</b> (10 mm)
|
||||||
|
per Knopf, der genaue Wert bleibt frei eintragbar.</li>
|
||||||
|
<li><b>Wohin?</b>: der fertige Bogen kann zusätzlich an Picdrop, aufs NAS oder an ein
|
||||||
|
Zusatzziel gelegt werden — wie die Bilder aus dem Studio.</li>
|
||||||
<li><b>Automatische Anordnung</b>: gleich große Bilder ergeben ein sauberes Raster; bei gemischten
|
<li><b>Automatische Anordnung</b>: gleich große Bilder ergeben ein sauberes Raster; bei gemischten
|
||||||
Größen werden die kleinen automatisch in die Restflächen neben den großen gesetzt.</li>
|
Größen werden die kleinen automatisch in die Restflächen neben den großen gesetzt.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { layout, cutMarks, effectiveGap, type PlaceSpec, type SheetSpec, type Ma
|
|||||||
import { renderCell, type CropRel, type SheetCellImage, buildSheetPdf } from '../../../lib/printrender';
|
import { renderCell, type CropRel, type SheetCellImage, buildSheetPdf } from '../../../lib/printrender';
|
||||||
import { loadSource, sheetFilename, type PrintSource } from '../../../lib/printsource';
|
import { loadSource, sheetFilename, type PrintSource } from '../../../lib/printsource';
|
||||||
import { paperById, parseSizeMm, labelMm } from '../../../lib/paper';
|
import { paperById, parseSizeMm, labelMm } from '../../../lib/paper';
|
||||||
|
import { cfgForKey } from '../../../lib/delivery';
|
||||||
|
import { uploadBuffer } from '../../../lib/picdrop';
|
||||||
|
import { one } from '../../../lib/db';
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
@@ -103,12 +106,34 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|||||||
markWidthPt: numOr(body?.marks?.widthPt, 0.25, 0.1, 2),
|
markWidthPt: numOr(body?.marks?.widthPt, 0.25, 0.1, 2),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const filename = sheetFilename(body?.name || 'druckbogen', 'pdf');
|
||||||
|
|
||||||
|
// Optional: den fertigen Bogen zusätzlich ins gewohnte Ziel schieben
|
||||||
|
// (Standard-Picdrop, NAS oder ein Zusatzziel) — wie bei den Bildern.
|
||||||
|
let delivered = '0', deliveryMsg = '';
|
||||||
|
if (body?.deliver) {
|
||||||
|
const key = body.deliver.target === '' || body.deliver.target == null ? 'picdrop' : String(body.deliver.target);
|
||||||
|
try {
|
||||||
|
const cfg = await cfgForKey(key);
|
||||||
|
if (!cfg) throw new Error('Ziel nicht konfiguriert.');
|
||||||
|
const s = await one<{ picdrop_default_gallery: string | null }>('SELECT picdrop_default_gallery FROM settings WHERE id=1');
|
||||||
|
const gallery = String(body.deliver.gallery || '').trim() || s?.picdrop_default_gallery || 'POSTER LEA';
|
||||||
|
await uploadBuffer(cfg, gallery, filename, Buffer.from(bytes));
|
||||||
|
delivered = '1';
|
||||||
|
deliveryMsg = `Bogen nach „${gallery}" ausgeliefert.`;
|
||||||
|
} catch (e: any) {
|
||||||
|
deliveryMsg = `Auslieferung fehlgeschlagen: ${e?.message || e}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new Response(Buffer.from(bytes), {
|
return new Response(Buffer.from(bytes), {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/pdf',
|
'Content-Type': 'application/pdf',
|
||||||
'Content-Disposition': `attachment; filename="${sheetFilename(body?.name || 'druckbogen', 'pdf')}"`,
|
'Content-Disposition': `attachment; filename="${filename}"`,
|
||||||
'X-Klarbild-Pages': String(pages.length),
|
'X-Klarbild-Pages': String(pages.length),
|
||||||
'X-Klarbild-Per-Sheet': String(plan.perSheet),
|
'X-Klarbild-Per-Sheet': String(plan.perSheet),
|
||||||
|
'X-Klarbild-Delivered': delivered,
|
||||||
|
...(deliveryMsg ? { 'X-Klarbild-Delivery-Msg': encodeURIComponent(deliveryMsg) } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -83,6 +83,12 @@ Rein lokale Geometrie: Zuschnitt (sharp) + PDF (pdf-lib). Kein Modell, keine Kos
|
|||||||
cells:[{ id, src, crop?, size|wMm+hMm, landscape?, count?, allowRotate? }] }.
|
cells:[{ id, src, crop?, size|wMm+hMm, landscape?, count?, allowRotate? }] }.
|
||||||
Antwort: application/pdf. Header: X-Klarbild-Pages, X-Klarbild-Per-Sheet.
|
Antwort: application/pdf. Header: X-Klarbild-Pages, X-Klarbild-Per-Sheet.
|
||||||
- GET/POST/DELETE /api/print/presets — Bogen-Vorlagen (Papier, Marken, Formate, Stückzahlen).
|
- GET/POST/DELETE /api/print/presets — Bogen-Vorlagen (Papier, Marken, Formate, Stückzahlen).
|
||||||
|
Mitgeseedet: „Kita-Satz (A4)", „Schulsatz klein (A4)", „Passbildbogen 35×45 (A4)",
|
||||||
|
„2 × 10×7,5 auf Fotopapier 10×15". Eine Vorlage mit mehreren Formaten und nur einem Bild
|
||||||
|
klont das Bild in alle Formate.
|
||||||
|
- Bogen ausliefern: \`deliver: { target: ''|'nas'|<uuid>, gallery? }\` in /api/print/sheet legt das PDF
|
||||||
|
zusätzlich ins Ziel. Antwort bleibt das PDF; Header X-Klarbild-Delivered (0|1) und
|
||||||
|
X-Klarbild-Delivery-Msg (URI-kodiert).
|
||||||
|
|
||||||
## MCP-Server (für Assistenten)
|
## MCP-Server (für Assistenten)
|
||||||
Werkzeug \`klarbild\` (mcp/klarbild-mcp.mjs), nutzt denselben API-Token:
|
Werkzeug \`klarbild\` (mcp/klarbild-mcp.mjs), nutzt denselben API-Token:
|
||||||
|
|||||||
Reference in New Issue
Block a user