Zur Wahl stehen zwei Quellen: das Logo der Installation aus dem Erscheinungsbild oder ein eigenes je Code. Die Groesse ist zwischen 10 und 25 Prozent der Kantenlaenge einstellbar, hinter dem Bild liegt eine Flaeche in der Hintergrundfarbe. Mit Logo gilt die Fehlerkorrektur H, unabhaengig von der Auswahl — was das Bild verdeckt, muss sie ausgleichen. Die Auswahl steht dann fest auf H und sagt auch warum; heimlich geschieht das nicht. Ab 22 Prozent kommt eine Warnung dazu. Gerendert wird weiterhin serverseitig, damit Vorschau und Download uebereinstimmen: das SVG bekommt ein eingebettetes Bild, das PNG wird mit sharp zusammengesetzt. sharp lag ohnehin als Abhaengigkeit von Next im Baum und steht jetzt ausdruecklich in der package.json. Ein eigenes Logo gehoert zur Gestaltung, nicht zum Inhalt — es muss trotzdem an den Code gehaengt werden. Sonst raeumt die Aufraeumroutine es nach einem Tag weg und der Code verliert sein Bild. Refs #8
59 lines
1.9 KiB
TypeScript
59 lines
1.9 KiB
TypeScript
import type { NextRequest } from "next/server";
|
|
import { z } from "zod";
|
|
|
|
import { getActor, requireSignedIn } from "@/lib/auth/actor";
|
|
import { errorResponse, firstZodMessage, jsonError } from "@/lib/http";
|
|
import { ladeLogo } from "@/lib/qr/logo";
|
|
import { CONTENT_TYPES, renderPng, renderSvg } from "@/lib/qr/render";
|
|
import {
|
|
downloadFilename,
|
|
qrRequestSchema,
|
|
QrRequestError,
|
|
resolveQrRequest,
|
|
} from "@/lib/qr/request";
|
|
|
|
const downloadSchema = qrRequestSchema.extend({
|
|
format: z.enum(["png", "svg"], { error: "Ungültiges Dateiformat." }),
|
|
});
|
|
|
|
/** Liefert den fertigen QR-Code als PNG- oder SVG-Datei zum Herunterladen. */
|
|
export async function POST(request: NextRequest): Promise<Response> {
|
|
try {
|
|
// Der Generator steht nur Angemeldeten offen; die Weiterleitung
|
|
// unter /r/{slug} bleibt davon unberührt und öffentlich.
|
|
requireSignedIn(await getActor());
|
|
|
|
const parsed = downloadSchema.safeParse(await request.json());
|
|
if (!parsed.success) {
|
|
return jsonError(firstZodMessage(parsed.error));
|
|
}
|
|
|
|
const { format, design } = parsed.data;
|
|
|
|
// Ein dynamischer Code ohne Slug wäre eine Weiterleitung ins Leere.
|
|
if (parsed.data.mode === "dynamic" && !parsed.data.slug) {
|
|
throw new QrRequestError(
|
|
"Bitte speichere den dynamischen Code zuerst — erst dann existiert die Kurz-URL.",
|
|
);
|
|
}
|
|
|
|
const { qrType, data, content } = await resolveQrRequest(parsed.data);
|
|
|
|
const logo = await ladeLogo(design);
|
|
const body =
|
|
format === "png"
|
|
? new Uint8Array(await renderPng(content, design, logo))
|
|
: await renderSvg(content, design, logo);
|
|
|
|
return new Response(body, {
|
|
headers: {
|
|
"Content-Type": CONTENT_TYPES[format],
|
|
"Content-Disposition": `attachment; filename="${downloadFilename(qrType, data, format, parsed.data.label)}"`,
|
|
"Cache-Control": "no-store",
|
|
},
|
|
});
|
|
} catch (error) {
|
|
return errorResponse(error);
|
|
}
|
|
}
|