Drucken: eindeutige Dateinamen für PDFs und Einzelbilder

Zwei Druckbogen desselben Tages hießen beide `2026-09-01_druckbogen.pdf`. Der
Browser hängte „(1)" an; wer sie in denselben Ordner oder aufs NAS legte, verlor
den ersten. Genau der Fehler, der im August für die Bibliotheksbilder behoben
wurde — im Druckmodul stand er noch.

Druckdateien folgen jetzt derselben Regel wie alles andere:

    2026-09-01_143207_druck-9-teile_a4_4eca.pdf

Datum, Uhrzeit auf die Sekunde, was auf dem Bogen liegt, das Papier, ein Kürzel.
Das Kürzel ist die Kennung des Verlaufseintrags — eine PDF-Datei lässt sich also
ihrem Eintrag unter „Zuletzt gedruckt" zuordnen. Dafür entsteht die Kennung vor
dem Dateinamen und wird an `recordRun` durchgereicht.

Einzelbilder erben das Motiv ihrer Quelle:
`2026-09-01_143207_lea-badge_130x180_25b3.jpg` statt `2026-09-01_1318.jpg`.
Dafür gibt `loadSourceNamed` neben den Bilddaten auch Namen und Kennung zurück.

Die Oberfläche erfindet keinen eigenen Namen mehr. Der Server schickt einen, der
Client liest ihn aus `Content-Disposition` — es gab zwei Wahrheiten, und die des
Clients war die schlechtere: Sie enthielt nur das Datum. Aus dem Kopf gelesene
Namen werden entschärft, bevor sie in `a.download` landen.

`sheetFilename` in printsource.ts ist entfallen; die Regeln stehen jetzt
vollständig in `naming.ts`, wo sie schon für die Bibliothek stehen.

12 neue Tests, 123 grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1
This commit is contained in:
tillandClaude Opus 5 committed 2026-09-01 15:31:23 +00:00
1 parent 841ff41076
commit a87587c273
9 files changed
+249 -19

No files matched your search

+12 -2
View File
@@ -1,8 +1,10 @@
import type { APIRoute } from 'astro';
import { randomUUID } from 'node:crypto';
import { layout, cutMarks, effectiveGap, type PlaceSpec, type SheetSpec, type MarkMode, type Page, type Line } from '../../../lib/printlayout';
import { fitsOnSheet, overflowPlacement, tilePlan, tileCrop, tileSheet, noteInset, type Oversize } from '../../../lib/printoversize';
import { renderCell, type CropRel, type SheetCellImage, buildSheetPdf, type PageExtras, type FooterPos } from '../../../lib/printrender';
import { loadSource, sheetFilename, type PrintSource } from '../../../lib/printsource';
import { loadSource, type PrintSource } from '../../../lib/printsource';
import { buildSheetFilename } from '../../../lib/naming';
import { paperById, parseSizeMm, labelMm } from '../../../lib/paper';
import { cfgForKey } from '../../../lib/delivery';
import { uploadBuffer } from '../../../lib/picdrop';
@@ -182,7 +184,14 @@ export const POST: APIRoute = async ({ request, locals }) => {
markWidthPt: numOr(body?.marks?.widthPt, 0.25, 0.1, 2),
});
const filename = sheetFilename(body?.name || 'druckbogen', 'pdf');
// Die Kennung des Verlaufseintrags entsteht **vor** dem Dateinamen, damit
// beide dasselbe Kürzel tragen: Wer eine PDF-Datei in der Hand hat, findet
// den zugehörigen Eintrag unter „Zuletzt gedruckt" wieder.
const runId = randomUUID();
const filename = buildSheetFilename({
name: body?.name, paperId: typeof body?.paper === 'string' ? body.paper : body?.paper?.id,
wMm: sheet.wMm, hMm: sheet.hMm, pieces: totalPieces, pages: pages.length, runId,
});
// Optional: den fertigen Bogen zusätzlich ins gewohnte Ziel schieben
// (Standard-Picdrop, NAS oder ein Zusatzziel) — wie bei den Bildern.
@@ -211,6 +220,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
// dürfen den Bogen nie kosten: Er ist fertig und geht gleich raus.
try {
await recordRun({
id: runId,
userId: locals.user.uid ?? null,
origin: String(body?.origin || 'web'),
summary: kurzfassung(kinds),
+8 -3
View File
@@ -1,6 +1,7 @@
import type { APIRoute } from 'astro';
import { renderCell, type CropRel } from '../../../lib/printrender';
import { loadSource, sheetFilename, type PrintSource } from '../../../lib/printsource';
import { loadSourceNamed, type PrintSource } from '../../../lib/printsource';
import { buildSingleFilename } from '../../../lib/naming';
import { parseSizeMm, mmToPx, labelMm } from '../../../lib/paper';
import { dpiCheck } from '../../../lib/printlayout';
@@ -30,7 +31,8 @@ export const POST: APIRoute = async ({ request, locals }) => {
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 quelle = await loadSourceNamed(src, locals.user as any);
const buf = quelle.buffer;
const fit = body?.fit === 'contain' ? 'contain' : 'cover';
const background = /^#[0-9a-f]{6}$/i.test(String(body?.bg || '')) ? String(body.bg) : '#ffffff';
const out = await renderCell(buf, crop, size.w, size.h, dpi,
@@ -43,7 +45,10 @@ export const POST: APIRoute = async ({ request, locals }) => {
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)}"`,
'Content-Disposition': `attachment; filename="${buildSingleFilename({
name: body?.name, sourceName: quelle.filename,
wMm: size.w, hMm: size.h, ext: out.ext, id: quelle.itemId,
})}"`,
'X-Klarbild-Px': `${out.width}x${out.height}`,
'X-Klarbild-Real-Dpi': String(check.dpi),
'X-Klarbild-Dpi-Ok': check.ok ? '1' : '0',