import QRCode from "qrcode"; import type { QrPattern, QrRenderOptions } from "@/lib/qr-options"; const QUIET_ZONE = 4; interface MatrixInfo { size: number; modules: boolean[][]; } /** 使用 qrcode 库生成模块矩阵 */ export function buildMatrix(value: string, errorCorrection: QrRenderOptions["errorCorrection"]): MatrixInfo { const qr = QRCode.create(value, { errorCorrectionLevel: errorCorrection }); const size = qr.modules.size; const modules: boolean[][] = []; for (let row = 0; row < size; row += 1) { const line: boolean[] = []; for (let col = 0; col < size; col += 1) { line.push(Boolean(qr.modules.get(row, col))); } modules.push(line); } return { size, modules }; } function isFinder(row: number, col: number, size: number) { const inTopLeft = row < 7 && col < 7; const inTopRight = row < 7 && col >= size - 7; const inBottomLeft = row >= size - 7 && col < 7; return inTopLeft || inTopRight || inBottomLeft; } function modulePath(pattern: QrPattern, x: number, y: number, cell: number) { const pad = cell * 0.12; const cx = x + cell / 2; const cy = y + cell / 2; const r = (cell - pad * 2) / 2; switch (pattern) { case "dots": return ``; case "diamonds": { const s = r * 0.95; return ``; } case "stars": { const outer = r; const inner = r * 0.45; const points = Array.from({ length: 10 }, (_, i) => { const angle = -Math.PI / 2 + (i * Math.PI) / 5; const radius = i % 2 === 0 ? outer : inner; return `${cx + Math.cos(angle) * radius},${cy + Math.sin(angle) * radius}`; }).join(" "); return ``; } case "hearts": { const s = r * 1.15; return ``; } default: return ``; } } function finderMark(originX: number, originY: number, cell: number, fill: string, background: string) { const size = cell * 7; const inset = cell; const core = cell * 3; return ` `; } export function getQrLayout(options: QrRenderOptions) { const { size: modules } = buildMatrix(options.value, options.errorCorrection); const framePad = options.frameEnabled ? 3 : 0; const totalModules = modules + QUIET_ZONE * 2 + framePad * 2; const banner = options.frameEnabled ? 3.2 : 0; const view = totalModules + banner; return { modules, framePad, totalModules, banner, view }; } /** 将渲染选项输出为完整 SVG 字符串,预览与下载共用 */ export function renderQrSvg(options: QrRenderOptions) { const matrix = buildMatrix(options.value, options.errorCorrection); const cell = 10; const framePad = options.frameEnabled ? 3 : 0; const contentModules = matrix.size + QUIET_ZONE * 2; const board = (contentModules + framePad * 2) * cell; const banner = options.frameEnabled ? 32 : 0; const width = board; const height = board + banner; const offset = (QUIET_ZONE + framePad) * cell; const gradientId = `halo-fg-${Math.abs(hashString(options.value + options.gradientFrom + options.pattern)).toString(36)}`; const fill = options.gradientKind === "none" ? options.fgColor : `url(#${gradientId})`; const bg = options.transparentBackground ? "transparent" : options.bgColor; const gradientDef = options.gradientKind === "radial" ? ` ` : ` `; const finders = [ [0, 0], [0, matrix.size - 7], [matrix.size - 7, 0], ]; const modulesMarkup = matrix.modules .map((row, rowIndex) => row .map((on, colIndex) => { if (!on || isFinder(rowIndex, colIndex, matrix.size)) return ""; return modulePath(options.pattern, offset + colIndex * cell, offset + rowIndex * cell, cell); }) .join(""), ) .join(""); const finderMarkup = finders .map(([row, col]) => finderMark(offset + col * cell, offset + row * cell, cell, fill, options.transparentBackground ? "#ffffff" : options.bgColor)) .join(""); const logoSize = cell * 7; const logoX = offset + (matrix.size * cell - logoSize) / 2; const logoY = offset + (matrix.size * cell - logoSize) / 2; const logoMarkup = options.logoDataUrl ? ` ` : ""; const frameMarkup = options.frameEnabled ? ` ${escapeXml( options.frameText || "SCAN ME", )}` : ""; // 预览用 innerHTML 时不能带 XML 声明,否则浏览器会丢掉 svg 节点 return ` ${gradientDef} ${modulesMarkup} ${finderMarkup} ${logoMarkup} ${frameMarkup} `; } function hashString(value: string) { return value.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0); } function escapeXml(value: string) { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } export function svgToDataUrl(svg: string) { return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; }