feat: exact-size rendering and 1:1 sheet PDF (sharp + pdf-lib)

renderCell brings a relative crop onto an exact physical size; missing bleed
pixels are copied from the edge instead of stretching the motif. Without a
crop it falls back to a centred cover crop, never a distorted fill.
buildSheetPdf writes a PDF page that is exactly the sheet size in mm with
hairline crop marks and an optional footer. Source resolution guards uploads
to the sources/ prefix and reuses the library visibility rules.
This commit is contained in:
2026-08-18 06:20:46 +00:00
parent a651025422
commit a593dea6c5
4 changed files with 755 additions and 3 deletions
+44
View File
@@ -0,0 +1,44 @@
// Auflösen der Bildquellen für den Druckbogen: frisch hochgeladen oder aus der Bibliothek.
import { one } from './db';
import { getObject } from './storage';
export type PrintSource =
| { kind: 'upload'; path: string }
| { kind: 'item'; id: string };
export interface SessionUser { uid: string | null; role: string }
/** Lädt die Bilddaten und prüft dabei die Zugriffsrechte wie /api/items/:id/file. */
export async function loadSource(src: PrintSource, user: SessionUser): Promise<Buffer> {
if (!src || typeof src !== 'object') throw new Error('Quelle fehlt.');
if (src.kind === 'upload') {
// Nur der Upload-Bereich ist erreichbar — kein Weg zu results/ oder /etc.
const p = String(src.path || '');
if (!/^sources\/[0-9]{4}\/[A-Za-z0-9_-]+\.[A-Za-z0-9]{2,5}$/.test(p)) throw new Error('Ungültige Quelle.');
return getObject(p);
}
if (src.kind === 'item') {
const item = await one<any>(
`SELECT i.result_path, i.filename, j.created_by, j.private
FROM items i JOIN jobs j ON j.id = i.job_id WHERE i.id = $1`, [src.id]);
if (!item?.result_path) throw new Error('Bild nicht gefunden.');
const isAdmin = user.role === 'admin';
const own = item.created_by === user.uid;
if (!isAdmin && !own) {
const s = await one<{ library_visibility: string }>('SELECT library_visibility FROM settings WHERE id=1');
if (item.private || s?.library_visibility !== 'shared') throw new Error('Kein Zugriff auf dieses Bild.');
}
return getObject(item.result_path);
}
throw new Error('Unbekannte Quellenart.');
}
/** Sprechender Dateiname für den Download. */
export function sheetFilename(prefix: string, ext: string): string {
const date = new Date().toISOString().slice(0, 10);
const slug = (prefix || 'druckbogen').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
return `${date}_${slug || 'druckbogen'}.${ext}`;
}