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