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 = { 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; 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" }, ];