1import { memo } from "react";
2import { QrCode } from "@t3tools/shared/qrCode";
3
4type QRCodeSvgProps = {
5 value: string;
6 size?: number;
7 level?: "L" | "M" | "Q" | "H";
8 marginSize?: number;
9 title?: string;
10 className?: string;
11 foregroundColor?: string;
12 backgroundColor?: string;
13};
14
15const ERROR_CORRECTION_LEVELS = {
16 L: QrCode.Ecc.LOW,
17 M: QrCode.Ecc.MEDIUM,
18 Q: QrCode.Ecc.QUARTILE,
19 H: QrCode.Ecc.HIGH,
20} as const;
21
22function buildQrPathData(qrCode: ReturnType<typeof QrCode.encodeText>, marginSize: number): string {
23 const commands: Array<string> = [];
24
25 for (let y = 0; y < qrCode.size; y += 1) {
26 let runStart = -1;
27
28 for (let x = 0; x <= qrCode.size; x += 1) {
29 const isDark = x < qrCode.size && qrCode.getModule(x, y);
30
31 if (isDark) {
32 if (runStart === -1) {
33 runStart = x;
34 }
35 continue;
36 }
37
38 if (runStart === -1) {
39 continue;
40 }
41
42 commands.push(
43 `M${runStart + marginSize} ${y + marginSize}h${x - runStart}v1H${runStart + marginSize}z`,
44 );
45 runStart = -1;
46 }
47 }
48
49 return commands.join("");
50}
51
52export const QRCodeSvg = memo(function QRCodeSvg({
53 value,
54 size = 128,
55 level = "L",
56 marginSize = 0,
57 title,
58 className,
59 foregroundColor = "#000",
60 backgroundColor = "#fff",
61}: QRCodeSvgProps) {
62 const qrCode = QrCode.encodeText(value, ERROR_CORRECTION_LEVELS[level]);
63 const viewBoxSize = qrCode.size + marginSize * 2;
64
65 return (
66 <svg
67 xmlns="http://www.w3.org/2000/svg"
68 viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
69 width={size}
70 height={size}
71 shapeRendering="crispEdges"
72 role={title ? "img" : undefined}
73 aria-label={title}
74 className={className}
75 >
76 {title ? <title>{title}</title> : null}
77 <rect width={viewBoxSize} height={viewBoxSize} fill={backgroundColor} />
78 <path d={buildQrPathData(qrCode, marginSize)} fill={foregroundColor} />
79 </svg>
80 );
81});