Files
qrcode-generator/src/app/api/qr/download/route.ts
T
Florian Wilde c06f87e6d9 Logo in der Mitte des QR-Codes
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
2026-08-21 03:22:36 +02:00

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);
}
}