feat: print API - exact single image, sheet PDF, sheet presets
POST /api/print/single (image at an exact size, dpi headers incl. a real-dpi warning), GET /api/print/single as a mm-to-px calculator, POST /api/print/sheet (multi-image sheet as PDF, limits on cells/copies/pages) and print_presets for reusable sheet setups.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { renderCell, type CropRel } from '../../../lib/printrender';
|
||||
import { loadSource, sheetFilename, type PrintSource } from '../../../lib/printsource';
|
||||
import { parseSizeMm, mmToPx, labelMm } from '../../../lib/paper';
|
||||
import { dpiCheck } from '../../../lib/printlayout';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const json = (b: unknown, s = 200) =>
|
||||
new Response(JSON.stringify(b), { status: s, headers: { 'Content-Type': 'application/json' } });
|
||||
|
||||
/**
|
||||
* Ein einzelnes Bild exakt auf ein Maß bringen — ohne KI, ohne Bogen.
|
||||
* Antwort: die fertige Bilddatei (PNG/JPG) mit dpi-Metadaten.
|
||||
*/
|
||||
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 src: PrintSource = body?.src;
|
||||
const crop: CropRel | null = body?.crop ?? null;
|
||||
let size = (Number(body?.wMm) > 0 && Number(body?.hMm) > 0)
|
||||
? { w: Number(body.wMm), h: Number(body.hMm) }
|
||||
: parseSizeMm(String(body?.size || ''));
|
||||
if (!size) return json({ error: 'Maß fehlt oder ist ungültig.' }, 400);
|
||||
if (body?.landscape) size = { w: size.h, h: size.w };
|
||||
if (size.w < 5 || size.h < 5 || size.w > 2000 || size.h > 2000) return json({ error: 'Maß außerhalb des zulässigen Bereichs.' }, 400);
|
||||
|
||||
const dpi = Math.min(1200, Math.max(72, Number(body?.dpi) || 300));
|
||||
const ext: 'jpg' | 'png' = body?.ext === 'png' ? 'png' : 'jpg';
|
||||
const buf = await loadSource(src, locals.user as any);
|
||||
const out = await renderCell(buf, crop, size.w, size.h, dpi, { ext, bleedMm: Number(body?.bleedMm) || 0 });
|
||||
|
||||
// Hinweis, falls die Quelle für echte 300 dpi zu klein ist.
|
||||
const cropW = (crop?.w ?? 1) * out.srcPx[0];
|
||||
const check = dpiCheck(cropW, size.w, dpi);
|
||||
|
||||
return new Response(out.buffer, {
|
||||
headers: {
|
||||
'Content-Type': out.ext === 'png' ? 'image/png' : 'image/jpeg',
|
||||
'Content-Disposition': `attachment; filename="${sheetFilename(body?.name || labelMm(size.w, size.h).replace(/[^0-9x×]/g, ''), out.ext)}"`,
|
||||
'X-Klarbild-Px': `${out.width}x${out.height}`,
|
||||
'X-Klarbild-Real-Dpi': String(check.dpi),
|
||||
'X-Klarbild-Dpi-Ok': check.ok ? '1' : '0',
|
||||
},
|
||||
});
|
||||
} catch (e: any) {
|
||||
return json({ error: e?.message || 'Bild konnte nicht erzeugt werden.' }, 400);
|
||||
}
|
||||
};
|
||||
|
||||
/** Kleine Hilfe für Skripte/MCP: Maß → Pixel bei dpi. */
|
||||
export const GET: APIRoute = async ({ url, locals }) => {
|
||||
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||
const size = parseSizeMm(url.searchParams.get('size') || '');
|
||||
const dpi = Math.min(1200, Math.max(72, Number(url.searchParams.get('dpi')) || 300));
|
||||
if (!size) return json({ error: 'Parameter size fehlt (z. B. 12x15 oder 35x45mm).' }, 400);
|
||||
return json({ mm: size, dpi, px: { w: mmToPx(size.w, dpi), h: mmToPx(size.h, dpi) }, label: labelMm(size.w, size.h) });
|
||||
};
|
||||
Reference in New Issue
Block a user