import type { APIRoute } from 'astro'; import { layout, cutMarks, effectiveGap, type PlaceSpec, type SheetSpec, type MarkMode } from '../../../lib/printlayout'; import { renderCell, type CropRel, type SheetCellImage, buildSheetPdf } from '../../../lib/printrender'; import { loadSource, sheetFilename, type PrintSource } from '../../../lib/printsource'; import { paperById, parseSizeMm, labelMm } from '../../../lib/paper'; export const prerender = false; const MAX_CELLS = 40; // verschiedene Bilder je Bogen const MAX_COPIES = 200; // Exemplare je Bild const MAX_PAGES = 20; interface CellIn { id: string; src: PrintSource; crop?: CropRel | null; wMm?: number; hMm?: number; size?: string; // Freitext („12x15", „35x45mm") als Alternative zu wMm/hMm count?: number; allowRotate?: boolean; landscape?: boolean; // Bildformat quer statt hoch } const json = (b: unknown, s = 200) => new Response(JSON.stringify(b), { status: s, headers: { 'Content-Type': 'application/json' } }); export const POST: APIRoute = async ({ request, locals }) => { if (!locals.user) return new Response('Unauthorized', { status: 401 }); let body: any; try { body = await request.json(); } catch { return json({ error: 'Ungültige Anfrage.' }, 400); } try { const sheet = readSheet(body); const marksMode: MarkMode = ['none', 'corner', 'grid'].includes(body?.marks?.mode) ? body.marks.mode : 'corner'; const dpi = clamp(Number(body?.dpi) || 300, 72, 1200); const wantExt: 'jpg' | 'png' = body?.ext === 'png' ? 'png' : 'jpg'; const cellsIn: CellIn[] = Array.isArray(body?.cells) ? body.cells.slice(0, MAX_CELLS) : []; if (!cellsIn.length) return json({ error: 'Keine Bilder angegeben.' }, 400); const specs: PlaceSpec[] = []; const resolved: { cell: CellIn; wMm: number; hMm: number }[] = []; for (const [i, c] of cellsIn.entries()) { const size = cellSize(c); if (!size) return json({ error: `Bild ${i + 1}: Maß fehlt oder ist ungültig.` }, 400); const id = String(c.id || `c${i}`); specs.push({ id, wMm: size.w, hMm: size.h, count: clamp(Math.round(Number(c.count) || 1), 1, MAX_COPIES), allowRotate: c.allowRotate !== false, }); resolved.push({ cell: { ...c, id }, wMm: size.w, hMm: size.h }); } const plan = layout(specs, sheet); if (!plan.pages.length) { return json({ error: 'Nichts platzierbar — Bildmaß größer als die Nutzfläche des Bogens.', unplaced: plan.unplaced }, 422); } if (plan.pages.length > MAX_PAGES) return json({ error: `Zu viele Bogen (${plan.pages.length}). Bitte Anzahl reduzieren.` }, 422); // Für jedes Bild genau einmal rendern — auch bei 24 Exemplaren. // Gedrehte Platzierungen brauchen eine eigene Fassung. const images: Record = {}; const needRotated = new Set(); const needPlain = new Set(); for (const pg of plan.pages) for (const p of pg.placements) (p.rotated ? needRotated : needPlain).add(p.specId); for (const r of resolved) { const buf = await loadSource(r.cell.src, locals.user as any); if (needPlain.has(r.cell.id)) { const out = await renderCell(buf, r.cell.crop ?? null, r.wMm, r.hMm, dpi, { ext: wantExt, bleedMm: sheet.bleedMm, rotate: false }); images[r.cell.id] = { bytes: out.buffer, ext: out.ext }; } if (needRotated.has(r.cell.id)) { const out = await renderCell(buf, r.cell.crop ?? null, r.wMm, r.hMm, dpi, { ext: wantExt, bleedMm: sheet.bleedMm, rotate: true }); images[`${r.cell.id}::rot`] = { bytes: out.buffer, ext: out.ext }; } } // Platzierungen auf die passende Bildfassung umbiegen. const pages = plan.pages.map((pg) => ({ placements: pg.placements.map((p) => ({ ...p, specId: p.rotated ? `${p.specId}::rot` : p.specId })), })); const marks = plan.pages.map((pg) => cutMarks(pg, sheet, { mode: marksMode, lengthMm: numOr(body?.marks?.lengthMm, 4, 1, 20), offsetMm: body?.marks?.offsetMm != null ? numOr(body.marks.offsetMm, 3, 0, 20) : undefined, bleedMm: sheet.bleedMm, })); const kinds = resolved.map((r) => `${labelMm(r.wMm, r.hMm)}${specs.find((s) => s.id === r.cell.id)!.count > 1 ? ` ×${specs.find((s) => s.id === r.cell.id)!.count}` : ''}`); const footer = body?.footer === false ? null : `Klarbild · ${labelMm(sheet.wMm, sheet.hMm)} · ${kinds.join(' · ')} · ${dpi} dpi · Druck bei 100 % (nicht „an Seite anpassen")`; const bytes = await buildSheetPdf({ sheet, pages, marksPerPage: marks, images, title: String(body?.title || 'Klarbild Druckbogen'), footer, markWidthPt: numOr(body?.marks?.widthPt, 0.25, 0.1, 2), }); return new Response(Buffer.from(bytes), { headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': `attachment; filename="${sheetFilename(body?.name || 'druckbogen', 'pdf')}"`, 'X-Klarbild-Pages': String(pages.length), 'X-Klarbild-Per-Sheet': String(plan.perSheet), }, }); } catch (e: any) { return json({ error: e?.message || 'Druckbogen konnte nicht erzeugt werden.' }, 400); } }; function readSheet(body: any): SheetSpec { let w: number | null = null, h: number | null = null; const p = body?.paper; if (typeof p === 'string') { const pp = paperById(p); if (pp) { w = pp.w; h = pp.h; } } else if (p && typeof p === 'object') { if (p.id) { const pp = paperById(String(p.id)); if (pp) { w = pp.w; h = pp.h; } } if (p.wMm && p.hMm) { w = Number(p.wMm); h = Number(p.hMm); } if (!w && p.size) { const s = parseSizeMm(String(p.size)); if (s) { w = s.w; h = s.h; } } } if (!w || !h) throw new Error('Papierformat fehlt oder ist unbekannt.'); if (body?.landscape) [w, h] = [h, w]; if (w < 20 || h < 20 || w > 2000 || h > 2000) throw new Error('Papierformat außerhalb des zulässigen Bereichs.'); const bleedMm = numOr(body?.bleedMm, 0, 0, 10); const sheet: SheetSpec = { wMm: w, hMm: h, marginMm: numOr(body?.marginMm, 5, 0, 50), gapMm: numOr(body?.gapMm, 4, 0, 100), bleedMm, center: body?.center !== false, }; sheet.gapMm = effectiveGap(sheet); return sheet; } function cellSize(c: CellIn): { w: number; h: number } | null { let s: { w: number; h: number } | null = null; if (Number(c.wMm) > 0 && Number(c.hMm) > 0) s = { w: Number(c.wMm), h: Number(c.hMm) }; else if (c.size) s = parseSizeMm(String(c.size)); if (!s || s.w < 5 || s.h < 5 || s.w > 2000 || s.h > 2000) return null; return c.landscape ? { w: s.h, h: s.w } : s; } const clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n)); const numOr = (v: any, def: number, lo: number, hi: number) => { const n = Number(v); return Number.isFinite(n) ? clamp(n, lo, hi) : def; };