Files
klarbild/src/pages/api/print/single.ts
T
till e8b3fde652 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.
2026-08-18 06:20:46 +00:00

62 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) });
};