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:
9 files changed
+249
-19
No files matched your search
@@ -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),
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user