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
120 lines
4.1 KiB
TypeScript
120 lines
4.1 KiB
TypeScript
import { z } from "zod";
|
|
|
|
export const EC_LEVELS = ["L", "M", "Q", "H"] as const;
|
|
export type EcLevel = (typeof EC_LEVELS)[number];
|
|
|
|
export const EC_LEVEL_LABELS: Record<EcLevel, string> = {
|
|
L: "L — niedrig (7 % wiederherstellbar)",
|
|
M: "M — mittel (15 %, Standard)",
|
|
Q: "Q — hoch (25 %)",
|
|
H: "H — sehr hoch (30 %)",
|
|
};
|
|
|
|
export const MIN_SIZE = 256;
|
|
export const MAX_SIZE = 2048;
|
|
|
|
/**
|
|
* Logo in der Mitte des Codes.
|
|
*
|
|
* `branding` nimmt das Logo der Installation aus dem Erscheinungsbild,
|
|
* `upload` ein eigenes, das zu diesem Code gehört. Ohne Angabe kein Logo.
|
|
*/
|
|
export const LOGO_SOURCES = ["branding", "upload"] as const;
|
|
export type LogoSource = (typeof LOGO_SOURCES)[number];
|
|
|
|
/**
|
|
* Kantenlänge des Logos in Prozent der Kantenlänge des Codes.
|
|
*
|
|
* Bei 20 % sind knapp 4 % der Fläche verdeckt — das steckt die
|
|
* Fehlerkorrektur H (30 %) mühelos weg. Die Obergrenze liegt bei 25 %, weil
|
|
* darüber weniger die Fehlerkorrektur das Problem ist als die Scanner-Apps:
|
|
* Manche suchen in der Mitte nach Ausrichtungsmustern.
|
|
*/
|
|
export const MIN_LOGO_SCALE = 10;
|
|
export const MAX_LOGO_SCALE = 25;
|
|
/** Ab hier ein Hinweis, dass es knapp wird. */
|
|
export const LOGO_SCALE_WARN = 22;
|
|
export const DEFAULT_LOGO_SCALE = 20;
|
|
|
|
const HEX_COLOR = /^#[0-9a-fA-F]{6}$/;
|
|
|
|
const hexColor = z
|
|
.string()
|
|
.trim()
|
|
.regex(HEX_COLOR, {
|
|
error: "Bitte eine Farbe im Format #RRGGBB angeben, z. B. #1E293B.",
|
|
})
|
|
.transform((value) => value.toUpperCase());
|
|
|
|
export const designSchema = z.object({
|
|
fgColor: hexColor,
|
|
bgColor: hexColor,
|
|
size: z.coerce
|
|
.number({ error: "Die Größe muss eine Zahl sein." })
|
|
.int({ error: "Die Größe muss eine ganze Zahl sein." })
|
|
.min(MIN_SIZE, { error: `Die Größe muss mindestens ${MIN_SIZE} px betragen.` })
|
|
.max(MAX_SIZE, { error: `Die Größe darf höchstens ${MAX_SIZE} px betragen.` }),
|
|
ecLevel: z.enum(EC_LEVELS, {
|
|
error: "Ungültiges Fehlerkorrektur-Level. Erlaubt sind L, M, Q und H.",
|
|
}),
|
|
margin: z.coerce
|
|
.number({ error: "Der Rand muss eine Zahl sein." })
|
|
.int({ error: "Der Rand muss eine ganze Zahl sein." })
|
|
.min(0, { error: "Der Rand darf nicht negativ sein." })
|
|
.max(16, { error: "Der Rand darf höchstens 16 Module breit sein." }),
|
|
logoSource: z
|
|
.enum(LOGO_SOURCES, { error: "Unbekannte Herkunft des Logos." })
|
|
.nullable()
|
|
.default(null),
|
|
logoUploadId: z.string().trim().min(1).nullable().default(null),
|
|
logoScale: z.coerce
|
|
.number({ error: "Die Logogröße muss eine Zahl sein." })
|
|
.int({ error: "Die Logogröße muss eine ganze Zahl sein." })
|
|
.min(MIN_LOGO_SCALE, {
|
|
error: `Das Logo muss mindestens ${MIN_LOGO_SCALE} % breit sein.`,
|
|
})
|
|
.max(MAX_LOGO_SCALE, {
|
|
error: `Das Logo darf höchstens ${MAX_LOGO_SCALE} % breit sein — darüber wird der Code unzuverlässig.`,
|
|
})
|
|
.default(DEFAULT_LOGO_SCALE),
|
|
}).refine(
|
|
(design) => design.logoSource !== "upload" || design.logoUploadId !== null,
|
|
{ error: "Für ein eigenes Logo fehlt die hochgeladene Datei." },
|
|
);
|
|
|
|
export type QrDesign = z.infer<typeof designSchema>;
|
|
|
|
export const defaultDesign: QrDesign = {
|
|
fgColor: "#0F172A",
|
|
bgColor: "#FFFFFF",
|
|
size: 512,
|
|
ecLevel: "M",
|
|
margin: 4,
|
|
logoSource: null,
|
|
logoUploadId: null,
|
|
logoScale: DEFAULT_LOGO_SCALE,
|
|
};
|
|
|
|
/**
|
|
* Ein Logo verdeckt einen Teil des Codes. Was fehlt, muss die Fehlerkorrektur
|
|
* ausgleichen — deshalb gilt mit Logo immer **H**, unabhängig davon, was in
|
|
* der Auswahl steht. Die Oberfläche sagt das dazu; heimlich geschieht hier
|
|
* nichts.
|
|
*/
|
|
export function effectiveEcLevel(design: QrDesign): EcLevel {
|
|
return design.logoSource === null ? design.ecLevel : "H";
|
|
}
|
|
|
|
/** Farbvorschläge für die schnelle Auswahl in der Oberfläche. */
|
|
export const COLOR_PRESETS: ReadonlyArray<{
|
|
label: string;
|
|
fgColor: string;
|
|
bgColor: string;
|
|
}> = [
|
|
{ label: "Klassisch", fgColor: "#0F172A", bgColor: "#FFFFFF" },
|
|
{ label: "Indigo", fgColor: "#3730A3", bgColor: "#FFFFFF" },
|
|
{ label: "Waldgrün", fgColor: "#14532D", bgColor: "#F0FDF4" },
|
|
{ label: "Bordeaux", fgColor: "#7F1D1D", bgColor: "#FFF7ED" },
|
|
{ label: "Marine", fgColor: "#1E3A8A", bgColor: "#EFF6FF" },
|
|
];
|