cubby-ui
uiqr-codesource: ok · refreshed
QR Code
A from-scratch, zero-dependency QR code generator rendered as pure scalable SVG.
npx shadcn add "https://www.cubby-ui.dev/r/qr-code.json"free
Visual style
terminal-mono
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Files15 · source cached 10/9/2026, 7:29:17 AM
registry/default/qr-code/qr-code.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import type {
ECLevel,
QRCodeHandle,
QRDotStyle,
QRFinderStyle,
QRFinderTuple,
QRLogo,
QRLogoObject,
QRRenderOptions,
} from "./lib/types";
import { encode } from "./lib/encoder";
import { buildRenderModel, serializeModel, resolveEcLevel } from "./lib/svg-builder";
import { matrixToDataURL } from "./lib/export";
export type QRCodeProps = Omit<
React.ComponentProps<"svg">,
"children" | "ref"
> & {
/** The data to encode (URL, text, etc.). */
value: string;
/** Error correction level. Defaults to `"M"`; auto-raised when a logo is set. */
ecLevel?: ECLevel;
/** Module dot style. Defaults to `"square"`. */
dotStyle?: QRDotStyle;
/** Quiet-zone width in modules. Defaults to `4`. */
margin?: number;
/** Foreground (dark module) color. Defaults to `"currentColor"`. */
foreground?: string;
/** Background color. Defaults to `"transparent"`. */
background?: string;
/** Unified or per-corner `[topLeft, topRight, bottomLeft]` finder styling. */
finder?: QRFinderStyle | QRFinderTuple;
/** A center logo: an image descriptor or any React element. */
logo?: QRLogo;
/** Fraction of the symbol width occupied by the logo (clamped to ≤ 0.3). */
logoSize?: number;
/** Quiet area in modules knocked out around the logo. Defaults to `1`. */
logoPadding?: number;
/** Minimum QR version (1–40). */
minVersion?: number;
/** Rendered size in pixels. The `viewBox` stays in module units. */
size?: number | string;
/**
* Accessible name, rendered as an SVG `<title>`. Defaults to `value`. Pass
* `aria-hidden` instead to mark a decorative code.
*/
title?: string;
/** Imperative handle exposing `toSVGString`, `toDataURL`, and `getMatrix`. */
ref?: React.Ref<QRCodeHandle>;
};
function isImageLogo(logo: QRLogo): logo is QRLogoObject {
return (
typeof logo === "object" &&
logo !== null &&
"src" in logo &&
typeof (logo as QRLogoObject).src === "string"
);
}
/**
* Renders a QR code as pure, scalable SVG with customizable dot styles, finder
* patterns, and an optional center logo.
*/
function QRCode({
value,
ecLevel,
dotStyle,
margin,
foreground,
background,
finder,
logo,
logoSize,
logoPadding,
minVersion,
size,
title,
className,
ref,
...props
}: QRCodeProps) {
const imageLogo = logo != null && isImageLogo(logo);
const hasLogo = logo != null && logo !== false;
const elementLogo = hasLogo && !imageLogo;
const resolvedEcLevel = resolveEcLevel(ecLevel, hasLogo);
// A consumer marking the code decorative (`aria-hidden`) shouldn't get an
// auto-filled accessible name from `value`.
const isDecorative =
props["aria-hidden"] === true || props["aria-hidden"] === "true";
const accessibleTitle = isDecorative ? title : (title ?? value);
// Encoding is the expensive step (8-mask penalty scoring), so it is memoized
// separately on primitive deps — style and logo changes never re-encode.
const matrix = React.useMemo(
() => encode(value, { ecLevel: resolvedEcLevel, minVersion }),
[value, resolvedEcLevel, minVersion],
);
const { model, renderOptions } = React.useMemo(() => {
const logoObject: QRLogoObject | undefined = imageLogo
? (logo as QRLogoObject)
: hasLogo
? { src: "" }
: undefined;
const options: QRRenderOptions = {
dotStyle,
margin,
foreground,
background,
finder,
logoSize,
logoPadding,
logo: logoObject,
title: accessibleTitle,
};
return { renderOptions: options, model: buildRenderModel(matrix, options) };
}, [
matrix,
dotStyle,
margin,
foreground,
background,
finder,
logo,
imageLogo,
hasLogo,
logoSize,
logoPadding,
accessibleTitle,
]);
React.useImperativeHandle(
ref,
(): QRCodeHandle => ({
toSVGString: () => serializeModel(model, size),
toDataURL: (options) => matrixToDataURL(matrix, renderOptions, options),
getMatrix: () => matrix,
}),
[model, matrix, renderOptions, size],
);
const dimension = size != null ? { width: size, height: size } : {};
return (
<svg
{...props}
{...dimension}
data-slot="qr-code"
data-dot-style={dotStyle ?? "square"}
role={props.role ?? "img"}
viewBox={`0 0 ${model.viewBox} ${model.viewBox}`}
shapeRendering={model.crisp ? "crispEdges" : undefined}
className={cn("block", className)}
>
{model.title ? <title>{model.title}</title> : null}
{model.background ? (
<rect width={model.viewBox} height={model.viewBox} fill={model.background} />
) : null}
{model.dataPath ? <path d={model.dataPath} fill={model.foreground} /> : null}
{model.finders.map((f) => (
<React.Fragment key={f.id}>
<path d={f.outerPath} fill={f.outerColor} fillRule="evenodd" />
<path d={f.innerPath} fill={f.innerColor} />
</React.Fragment>
))}
{model.knockout ? (
<rect
x={model.knockout.x}
y={model.knockout.y}
width={model.knockout.size}
height={model.knockout.size}
fill={model.knockout.color}
/>
) : null}
{imageLogo && model.image ? (
<image
href={model.image.href}
x={model.image.x}
y={model.image.y}
width={model.image.width}
height={model.image.height}
preserveAspectRatio="xMidYMid meet"
aria-label={model.image.alt}
/>
) : null}
{elementLogo && model.image ? (
<foreignObject
x={model.image.x}
y={model.image.y}
width={model.image.width}
height={model.image.height}
>
<div className="flex h-full w-full items-center justify-center">
{logo as React.ReactNode}
</div>
</foreignObject>
) : null}
</svg>
);
}
export { QRCode };
export { encode } from "./lib/encoder";
export { toSVGString, toDataURL } from "./lib/export";
export type {
ECLevel,
QRMode,
QRMatrix,
QRDotStyle,
QRFinderStyle,
QRFinderTuple,
QRLogo,
QRLogoObject,
QRCodeHandle,
QRExportOptions,
QRDataURLType,
QRDataURLOptions,
} from "./lib/types";
registry/default/qr-code/lib/bit-buffer.ts
/**
* A growable buffer of bits, used to assemble the QR data bit stream before it
* is packed into codewords.
*/
export class BitBuffer {
private readonly bits: number[] = [];
/** The number of bits currently stored. */
get length(): number {
return this.bits.length;
}
/**
* Appends the lower `len` bits of `value`, most-significant bit first.
*/
append(value: number, len: number): void {
if (len < 0 || len > 31 || value >>> len !== 0) {
throw new RangeError("Value does not fit in the given bit length");
}
for (let i = len - 1; i >= 0; i--) {
this.bits.push((value >>> i) & 1);
}
}
/** Reads the bit at `index`. */
get(index: number): number {
return this.bits[index];
}
/**
* Packs the bit stream into a byte array, most-significant bit first. The
* buffer length must be a multiple of 8.
*/
toBytes(): Uint8Array {
if (this.length % 8 !== 0) {
throw new Error("Bit buffer is not byte-aligned");
}
const out = new Uint8Array(this.length / 8);
for (let i = 0; i < this.length; i++) {
out[i >>> 3] |= this.bits[i] << (7 - (i & 7));
}
return out;
}
}
registry/default/qr-code/lib/encoder.ts
/**
* Top-level QR encoding pipeline.
*
* Implements ISO/IEC 18004 for Numeric, Alphanumeric, and Byte (UTF-8) modes
* across versions 1–40 and all four error correction levels, with automatic
* mode/version/mask selection.
*/
import type { ECLevel, EncodeOptions, QRMatrix, QRMode } from "./types";
import { BitBuffer } from "./bit-buffer";
import { detectMode, makeSegment, type Segment } from "./mode";
import { computeDivisor, computeRemainder } from "./reed-solomon";
import { buildMatrix } from "./matrix";
import {
MODE_INDICATOR,
getCharCountBits,
getNumDataCodewords,
getNumRawDataModules,
getNumEcBlocks,
getEccCodewordsPerBlock,
} from "./tables";
const EC_BOOST_ORDER: ECLevel[] = ["M", "Q", "H"];
/** Total bits a segment occupies at a given version (headers + payload). */
function getTotalBits(segment: Segment, version: number): number {
return 4 + getCharCountBits(segment.mode, version) + segment.dataBits;
}
/**
* Encodes `data` into a QR matrix.
*
* @throws if the data does not fit in any version up to `maxVersion`, or if a
* forced mode cannot represent the data.
*/
export function encode(data: string, options: EncodeOptions = {}): QRMatrix {
const requestedEcl: ECLevel = options.ecLevel ?? "M";
const minVersion = clampVersion(options.minVersion ?? 1);
const maxVersion = clampVersion(options.maxVersion ?? 40);
const boostEcl = options.boostEcl ?? true;
const mode: QRMode =
!options.mode || options.mode === "auto"
? detectMode(data)
: options.mode;
const segment = makeSegment(data, mode);
// Select the smallest version (≥ minVersion) that fits at the requested EC.
let version = -1;
for (let v = minVersion; v <= maxVersion; v++) {
if (getTotalBits(segment, v) <= getNumDataCodewords(v, requestedEcl) * 8) {
version = v;
break;
}
}
if (version === -1) {
throw new Error(
"Data is too long to encode in a QR code at the requested error correction level",
);
}
// Boost the EC level for free if the chosen version has spare capacity.
let ecLevel = requestedEcl;
if (boostEcl) {
const usedBits = getTotalBits(segment, version);
for (const candidate of EC_BOOST_ORDER) {
if (usedBits <= getNumDataCodewords(version, candidate) * 8) {
ecLevel = candidate;
}
}
}
// Assemble the data bit stream.
const buffer = new BitBuffer();
buffer.append(MODE_INDICATOR[mode], 4);
buffer.append(segment.numChars, getCharCountBits(mode, version));
segment.appendData(buffer);
const capacityBits = getNumDataCodewords(version, ecLevel) * 8;
buffer.append(0, Math.min(4, capacityBits - buffer.length)); // Terminator.
buffer.append(0, (8 - (buffer.length % 8)) % 8); // Byte-align.
// Pad bytes alternate 0xEC / 0x11 until the capacity is filled.
for (let pad = 0xec; buffer.length < capacityBits; pad ^= 0xec ^ 0x11) {
buffer.append(pad, 8);
}
const dataCodewords = buffer.toBytes();
const allCodewords = addEccAndInterleave(dataCodewords, version, ecLevel);
const { modules, size, mask } = buildMatrix(version, ecLevel, allCodewords);
return { size, modules, version, ecLevel, mode, maskPattern: mask };
}
/**
* Splits data codewords into blocks, appends Reed–Solomon error correction,
* and interleaves the result per ISO/IEC 18004.
*/
export function addEccAndInterleave(
data: Uint8Array,
version: number,
ecLevel: ECLevel,
): Uint8Array {
const numBlocks = getNumEcBlocks(version, ecLevel);
const blockEccLen = getEccCodewordsPerBlock(version, ecLevel);
const rawCodewords = Math.floor(getNumRawDataModules(version) / 8);
const numShortBlocks = numBlocks - (rawCodewords % numBlocks);
const shortBlockLen = Math.floor(rawCodewords / numBlocks);
const maxDataLen = shortBlockLen - blockEccLen + 1;
const dataParts: Uint8Array[] = [];
const eccParts: Uint8Array[] = [];
const divisor = computeDivisor(blockEccLen);
let offset = 0;
for (let i = 0; i < numBlocks; i++) {
const dataLen = maxDataLen - (i < numShortBlocks ? 1 : 0);
const part = data.slice(offset, offset + dataLen);
offset += dataLen;
dataParts.push(part);
eccParts.push(computeRemainder(part, divisor));
}
const result: number[] = [];
for (let i = 0; i < maxDataLen; i++) {
for (let j = 0; j < numBlocks; j++) {
if (i < dataParts[j].length) {
result.push(dataParts[j][i]);
}
}
}
for (let i = 0; i < blockEccLen; i++) {
for (let j = 0; j < numBlocks; j++) {
result.push(eccParts[j][i]);
}
}
return Uint8Array.from(result);
}
function clampVersion(version: number): number {
return Math.max(1, Math.min(40, Math.floor(version)));
}
registry/default/qr-code/lib/export.ts
/**
* Export helpers: a pure SSR-safe SVG string builder and a client-side data URL
* generator (PNG/JPEG via an offscreen canvas, or a vector SVG data URL).
*/
import type {
QRMatrix,
QRExportOptions,
QRRenderOptions,
QRDataURLOptions,
} from "./types";
import { encode } from "./encoder";
import { buildQRCodeSVG, resolveEcLevel } from "./svg-builder";
const DEFAULT_PIXEL_SIZE = 1024;
/**
* Encodes and renders a QR code to an SVG string. Pure and dependency-free, so
* it is safe to call on the server (route handlers, RSC, Node scripts).
*/
export function toSVGString(options: QRExportOptions): string {
const hasLogo = Boolean(options.logo);
const matrix = encode(options.value, {
ecLevel: resolveEcLevel(options.ecLevel, hasLogo),
minVersion: options.minVersion,
});
return buildQRCodeSVG(
matrix,
{ ...options, title: options.title ?? options.value },
options.size,
);
}
/** Builds a vector `data:image/svg+xml` URL from an SVG string. */
export function svgToDataURL(svg: string): string {
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
}
/**
* Renders an SVG string to a data URL. `"svg"` returns a vector data URL with
* no canvas; `"png"`/`"jpeg"` rasterize via an offscreen canvas and therefore
* require a browser environment.
*/
export async function rasterize(
svg: string,
options: { type?: "png" | "jpeg" | "svg"; quality?: number; pixelSize?: number } = {},
): Promise<string> {
const type = options.type ?? "png";
if (type === "svg") {
return svgToDataURL(svg);
}
if (typeof window === "undefined" && typeof OffscreenCanvas === "undefined") {
throw new Error(
"Rasterizing to PNG/JPEG requires a browser environment; use toSVGString() or type: \"svg\" on the server.",
);
}
const pixelSize = options.pixelSize ?? DEFAULT_PIXEL_SIZE;
const blob = new Blob([svg], { type: "image/svg+xml" });
const url = URL.createObjectURL(blob);
try {
const image = await loadImage(url);
const { canvas, context } = createCanvas(pixelSize);
if (type === "jpeg") {
context.fillStyle = "#ffffff";
context.fillRect(0, 0, pixelSize, pixelSize);
}
context.drawImage(image, 0, 0, pixelSize, pixelSize);
return await canvasToDataURL(canvas, type, options.quality);
} catch (error) {
if (error instanceof DOMException && error.name === "SecurityError") {
throw new Error(
"Cannot export this QR code to PNG/JPEG: the canvas was tainted by a " +
'cross-origin image. Use a same-origin or data-URI logo, or export with type: "svg".',
);
}
throw error;
} finally {
URL.revokeObjectURL(url);
}
}
/**
* Encodes, renders, and converts a QR code to a data URL in one call (client
* side for raster formats).
*/
export async function toDataURL(
options: QRDataURLOptions & { value: string },
): Promise<string> {
const type = options.type ?? "png";
const pixelSize = options.pixelSize ?? DEFAULT_PIXEL_SIZE;
const hasLogo = Boolean(options.logo);
const matrix = encode(options.value, {
ecLevel: resolveEcLevel(options.ecLevel, hasLogo),
minVersion: options.minVersion,
});
// Size the SVG to the target raster resolution for a crisp rasterization.
const size = type === "svg" ? options.size : pixelSize;
const renderOptions =
type === "svg"
? (options as QRRenderOptions)
: await inlineCrossOriginLogo(options as QRRenderOptions);
const svg = buildQRCodeSVG(matrix, renderOptions, size);
return rasterize(svg, { type, quality: options.quality, pixelSize });
}
/** Convenience: convert an already-encoded matrix to a data URL. */
export async function matrixToDataURL(
matrix: QRMatrix,
renderOptions: QRRenderOptions,
options: QRDataURLOptions = {},
): Promise<string> {
const type = options.type ?? "png";
const pixelSize = options.pixelSize ?? DEFAULT_PIXEL_SIZE;
const size = type === "svg" ? options.size : pixelSize;
const resolved =
type === "svg" ? renderOptions : await inlineCrossOriginLogo(renderOptions);
const svg = buildQRCodeSVG(matrix, resolved, size);
return rasterize(svg, { type, quality: options.quality, pixelSize });
}
const INLINE_URL_RE = /^(data:|blob:)/i;
const ABSOLUTE_URL_RE = /^[a-z]+:\/\//i;
/** Whether a logo `src` points to a different origin (and is not inlined). */
function isCrossOriginUrl(src: string): boolean {
if (INLINE_URL_RE.test(src)) {
return false;
}
if (typeof location === "undefined") {
return ABSOLUTE_URL_RE.test(src);
}
try {
return new URL(src, location.href).origin !== location.origin;
} catch {
return false;
}
}
/**
* Rasterizing an SVG that references a cross-origin image taints the canvas, so
* `toDataURL` would throw a `SecurityError`. When the logo is cross-origin we
* fetch it (which only succeeds if the host sends CORS headers) and inline it as
* a data URI. If that fails, we throw a clear, actionable error.
*/
async function inlineCrossOriginLogo(
options: QRRenderOptions,
): Promise<QRRenderOptions> {
const src = options.logo?.src;
if (!src || !isCrossOriginUrl(src)) {
return options;
}
try {
const response = await fetch(src, { mode: "cors" });
if (!response.ok) {
throw new Error(`status ${response.status}`);
}
const dataUrl = await blobToDataURL(await response.blob());
return { ...options, logo: { ...options.logo!, src: dataUrl } };
} catch {
throw new Error(
`Cannot rasterize a QR code with a cross-origin logo ("${src}") to PNG/JPEG — ` +
`the canvas would be tainted. Use a same-origin or data-URI logo, ensure the ` +
`image host sends CORS headers, or export with type: "svg".`,
);
}
}
async function loadImage(url: string): Promise<CanvasImageSource> {
if (typeof Image !== "undefined") {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error("Failed to load SVG for rasterization"));
img.src = url;
});
}
const response = await fetch(url);
return createImageBitmap(await response.blob());
}
function createCanvas(size: number): {
canvas: HTMLCanvasElement | OffscreenCanvas;
context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
} {
if (typeof document !== "undefined") {
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Could not acquire a 2D canvas context");
}
return { canvas, context };
}
const canvas = new OffscreenCanvas(size, size);
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Could not acquire a 2D canvas context");
}
return { canvas, context };
}
async function canvasToDataURL(
canvas: HTMLCanvasElement | OffscreenCanvas,
type: "png" | "jpeg",
quality?: number,
): Promise<string> {
const mime = type === "png" ? "image/png" : "image/jpeg";
if (canvas instanceof OffscreenCanvas) {
const blob = await canvas.convertToBlob({ type: mime, quality });
return blobToDataURL(blob);
}
return canvas.toDataURL(mime, quality);
}
function blobToDataURL(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = () => reject(new Error("Failed to read rasterized blob"));
reader.readAsDataURL(blob);
});
}
registry/default/qr-code/lib/galois.ts
/**
* Arithmetic in the Galois field GF(256) used by QR Reed–Solomon coding.
*
* The field is defined modulo the primitive polynomial
* `x^8 + x^4 + x^3 + x^2 + 1` (`0x11D`), per ISO/IEC 18004. Multiplication is
* implemented with the carry-less "Russian peasant" method, so no lookup
* tables are required and the module stays tiny and tree-shakeable.
*/
/** The QR primitive polynomial for GF(256), with the high bit dropped. */
const PRIMITIVE = 0x11d;
/**
* Multiplies two field elements in GF(256). Both operands and the result are
* bytes (0–255).
*/
export function gfMultiply(x: number, y: number): number {
let z = 0;
for (let i = 7; i >= 0; i--) {
// Multiply `z` by 2, reducing modulo the primitive polynomial.
z = (z << 1) ^ ((z >>> 7) * PRIMITIVE);
// Conditionally add `x` when bit `i` of `y` is set.
z ^= ((y >>> i) & 1) * x;
}
return z & 0xff;
}
registry/default/qr-code/lib/index.ts
/**
* Public entry point for the QR code encoder and renderer library.
*
* Every export is named (no default exports) so consumers and bundlers can
* tree-shake unused parts of the library.
*/
export { encode, addEccAndInterleave } from "./encoder";
export {
buildRenderModel,
serializeModel,
buildQRCodeSVG,
resolveEcLevel,
} from "./svg-builder";
export type {
QRRenderModel,
FinderRender,
KnockoutRect,
ImageRender,
} from "./svg-builder";
export {
toSVGString,
toDataURL,
svgToDataURL,
rasterize,
matrixToDataURL,
} from "./export";
export type {
ECLevel,
QRMode,
EncodeOptions,
QRMatrix,
QRDotStyle,
QRFinderStyle,
QRFinderTuple,
QRLogoObject,
QRLogo,
QRRenderOptions,
QRExportOptions,
QRDataURLType,
QRDataURLOptions,
QRCodeHandle,
} from "./types";
registry/default/qr-code/lib/mask.ts
/**
* Data masking and penalty scoring per ISO/IEC 18004.
*
* `maskBit` returns whether a module at (x, y) is inverted by a given mask
* pattern, and `computePenalty` scores a finished matrix using the four
* standard penalty rules so the lowest-scoring mask can be chosen.
*/
const PENALTY_N1 = 3;
const PENALTY_N2 = 3;
const PENALTY_N3 = 40;
const PENALTY_N4 = 10;
/** Returns whether mask pattern `mask` (0–7) inverts the module at (x, y). */
export function maskBit(mask: number, x: number, y: number): boolean {
switch (mask) {
case 0:
return (x + y) % 2 === 0;
case 1:
return y % 2 === 0;
case 2:
return x % 3 === 0;
case 3:
return (x + y) % 3 === 0;
case 4:
return (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0;
case 5:
return ((x * y) % 2) + ((x * y) % 3) === 0;
case 6:
return (((x * y) % 2) + ((x * y) % 3)) % 2 === 0;
case 7:
return (((x + y) % 2) + ((x * y) % 3)) % 2 === 0;
default:
throw new RangeError("Mask pattern out of range");
}
}
/** Scores a finished matrix; lower is better. */
export function computePenalty(modules: boolean[][], size: number): number {
let result = 0;
// Rule 1 (rows) and rule 3 (finder-like patterns) along each row.
for (let y = 0; y < size; y++) {
let runColor = false;
let runLength = 0;
const history = [0, 0, 0, 0, 0, 0, 0];
for (let x = 0; x < size; x++) {
if (modules[y][x] === runColor) {
runLength++;
if (runLength === 5) {
result += PENALTY_N1;
} else if (runLength > 5) {
result++;
}
} else {
addHistory(runLength, history, size);
if (!runColor) {
result += countFinderPatterns(history) * PENALTY_N3;
}
runColor = modules[y][x];
runLength = 1;
}
}
result += terminateAndCount(runColor, runLength, history, size) * PENALTY_N3;
}
// Rule 1 (columns) and rule 3 along each column.
for (let x = 0; x < size; x++) {
let runColor = false;
let runLength = 0;
const history = [0, 0, 0, 0, 0, 0, 0];
for (let y = 0; y < size; y++) {
if (modules[y][x] === runColor) {
runLength++;
if (runLength === 5) {
result += PENALTY_N1;
} else if (runLength > 5) {
result++;
}
} else {
addHistory(runLength, history, size);
if (!runColor) {
result += countFinderPatterns(history) * PENALTY_N3;
}
runColor = modules[y][x];
runLength = 1;
}
}
result += terminateAndCount(runColor, runLength, history, size) * PENALTY_N3;
}
// Rule 2: 2×2 blocks of the same color.
for (let y = 0; y < size - 1; y++) {
for (let x = 0; x < size - 1; x++) {
const color = modules[y][x];
if (
color === modules[y][x + 1] &&
color === modules[y + 1][x] &&
color === modules[y + 1][x + 1]
) {
result += PENALTY_N2;
}
}
}
// Rule 4: deviation of dark module proportion from 50%.
let dark = 0;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
if (modules[y][x]) {
dark++;
}
}
}
const total = size * size;
const k = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
result += k * PENALTY_N4;
return result;
}
/** Counts finder-like 1:1:3:1:1 patterns recorded in a run history. */
function countFinderPatterns(history: number[]): number {
const n = history[1];
const core =
n > 0 &&
history[2] === n &&
history[3] === n * 3 &&
history[4] === n &&
history[5] === n;
return (
(core && history[0] >= n * 4 && history[6] >= n ? 1 : 0) +
(core && history[6] >= n * 4 && history[0] >= n ? 1 : 0)
);
}
/** Finishes a line's run history (adding light borders) and counts patterns. */
function terminateAndCount(
runColor: boolean,
runLength: number,
history: number[],
size: number,
): number {
if (runColor) {
addHistory(runLength, history, size);
runLength = 0;
}
runLength += size;
addHistory(runLength, history, size);
return countFinderPatterns(history);
}
/** Pushes a run length into the 7-entry history, padding the first run. */
function addHistory(runLength: number, history: number[], size: number): void {
if (history[0] === 0) {
runLength += size;
}
history.pop();
history.unshift(runLength);
}
registry/default/qr-code/lib/matrix.ts
/**
* QR symbol matrix construction: function patterns, codeword placement,
* format/version information, and mask selection.
*/
import type { ECLevel } from "./types";
import {
getSize,
getEcFormatBits,
getAlignmentPatternPositions,
} from "./tables";
import { maskBit, computePenalty } from "./mask";
interface BuiltMatrix {
modules: boolean[][];
size: number;
mask: number;
}
/** Returns bit `i` of `value` as a boolean. */
function getBit(value: number, i: number): boolean {
return ((value >>> i) & 1) !== 0;
}
/**
* Builds the full module matrix for a version + EC level from its already
* interleaved data-and-EC codewords. When `forcedMask` is omitted, the mask
* with the lowest penalty score is selected automatically.
*/
export function buildMatrix(
version: number,
ecLevel: ECLevel,
codewords: Uint8Array,
forcedMask?: number,
): BuiltMatrix {
const size = getSize(version);
const modules: boolean[][] = Array.from({ length: size }, () =>
new Array<boolean>(size).fill(false),
);
const isFunction: boolean[][] = Array.from({ length: size }, () =>
new Array<boolean>(size).fill(false),
);
const setFunctionModule = (x: number, y: number, isDark: boolean): void => {
modules[y][x] = isDark;
isFunction[y][x] = true;
};
const drawFinderPattern = (cx: number, cy: number): void => {
for (let dy = -4; dy <= 4; dy++) {
for (let dx = -4; dx <= 4; dx++) {
const dist = Math.max(Math.abs(dx), Math.abs(dy));
const x = cx + dx;
const y = cy + dy;
if (x >= 0 && x < size && y >= 0 && y < size) {
setFunctionModule(x, y, dist !== 2 && dist !== 4);
}
}
}
};
const drawAlignmentPattern = (cx: number, cy: number): void => {
for (let dy = -2; dy <= 2; dy++) {
for (let dx = -2; dx <= 2; dx++) {
setFunctionModule(cx + dx, cy + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
}
}
};
const drawFormatBits = (mask: number): void => {
const data = (getEcFormatBits(ecLevel) << 3) | mask;
let rem = data;
for (let i = 0; i < 10; i++) {
rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
}
const bits = ((data << 10) | rem) ^ 0x5412;
// First copy, around the top-left finder.
for (let i = 0; i <= 5; i++) {
setFunctionModule(8, i, getBit(bits, i));
}
setFunctionModule(8, 7, getBit(bits, 6));
setFunctionModule(8, 8, getBit(bits, 7));
setFunctionModule(7, 8, getBit(bits, 8));
for (let i = 9; i < 15; i++) {
setFunctionModule(14 - i, 8, getBit(bits, i));
}
// Second copy, split across the other two finders.
for (let i = 0; i < 8; i++) {
setFunctionModule(size - 1 - i, 8, getBit(bits, i));
}
for (let i = 8; i < 15; i++) {
setFunctionModule(8, size - 15 + i, getBit(bits, i));
}
setFunctionModule(8, size - 8, true); // Always-dark module.
};
const drawVersion = (): void => {
if (version < 7) {
return;
}
let rem = version;
for (let i = 0; i < 12; i++) {
rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25);
}
const bits = (version << 12) | rem;
for (let i = 0; i < 18; i++) {
const bit = getBit(bits, i);
const a = size - 11 + (i % 3);
const b = Math.floor(i / 3);
setFunctionModule(a, b, bit);
setFunctionModule(b, a, bit);
}
};
// Timing patterns.
for (let i = 0; i < size; i++) {
setFunctionModule(6, i, i % 2 === 0);
setFunctionModule(i, 6, i % 2 === 0);
}
// Finder patterns (their light borders form the separators).
drawFinderPattern(3, 3);
drawFinderPattern(size - 4, 3);
drawFinderPattern(3, size - 4);
// Alignment patterns, skipping the three that overlap finder patterns.
const align = getAlignmentPatternPositions(version);
const numAlign = align.length;
for (let i = 0; i < numAlign; i++) {
for (let j = 0; j < numAlign; j++) {
const corner =
(i === 0 && j === 0) ||
(i === 0 && j === numAlign - 1) ||
(i === numAlign - 1 && j === 0);
if (!corner) {
drawAlignmentPattern(align[i], align[j]);
}
}
}
// Reserve the format and version areas.
drawFormatBits(0);
drawVersion();
// Place the data and EC codewords in zig-zag order.
let bitIndex = 0;
for (let right = size - 1; right >= 1; right -= 2) {
if (right === 6) {
right = 5;
}
for (let vert = 0; vert < size; vert++) {
for (let j = 0; j < 2; j++) {
const x = right - j;
const upward = ((right + 1) & 2) === 0;
const y = upward ? size - 1 - vert : vert;
if (!isFunction[y][x] && bitIndex < codewords.length * 8) {
modules[y][x] = getBit(codewords[bitIndex >>> 3], 7 - (bitIndex & 7));
bitIndex++;
}
}
}
}
const applyMask = (mask: number): void => {
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
if (!isFunction[y][x] && maskBit(mask, x, y)) {
modules[y][x] = !modules[y][x];
}
}
}
};
// Select the lowest-penalty mask unless one is forced.
let mask = forcedMask;
if (mask === undefined) {
let minPenalty = Infinity;
for (let m = 0; m < 8; m++) {
applyMask(m);
drawFormatBits(m);
const penalty = computePenalty(modules, size);
if (penalty < minPenalty) {
mask = m;
minPenalty = penalty;
}
applyMask(m); // Undo (XOR is self-inverse).
}
}
mask = mask ?? 0;
applyMask(mask);
drawFormatBits(mask);
return { modules, size, mask };
}
registry/default/qr-code/lib/mode.ts
/**
* Mode detection and per-mode data segment encoding.
*
* Supports Numeric, Alphanumeric, and Byte (UTF-8) modes. A {@link Segment}
* carries everything needed to emit its bit stream for a given version.
*/
import type { QRMode } from "./types";
import type { BitBuffer } from "./bit-buffer";
import { ALPHANUMERIC_CHARSET } from "./tables";
const NUMERIC_RE = /^[0-9]*$/;
const ALPHANUMERIC_RE = /^[0-9A-Z $%*+\-./:]*$/;
/** Char → value map for alphanumeric mode. */
const ALPHANUMERIC_MAP: Map<string, number> = new Map(
Array.from(ALPHANUMERIC_CHARSET, (char, value) => [char, value]),
);
/** A single encoded data segment. */
export interface Segment {
readonly mode: QRMode;
/** Character count used by the char-count indicator. */
readonly numChars: number;
/** Length, in bits, of the payload (excluding mode + count headers). */
readonly dataBits: number;
/** Appends the payload bits to a buffer. */
appendData(buffer: BitBuffer): void;
}
/** Selects the most compact mode that can represent `text`. */
export function detectMode(text: string): QRMode {
if (NUMERIC_RE.test(text)) {
return "numeric";
}
if (ALPHANUMERIC_RE.test(text)) {
return "alphanumeric";
}
return "byte";
}
/** Builds a segment for `text` in the given mode. */
export function makeSegment(text: string, mode: QRMode): Segment {
switch (mode) {
case "numeric":
return makeNumeric(text);
case "alphanumeric":
return makeAlphanumeric(text);
case "byte":
return makeBytes(text);
}
}
function makeNumeric(text: string): Segment {
if (!NUMERIC_RE.test(text)) {
throw new Error("Text contains non-numeric characters");
}
const n = text.length;
const dataBits = Math.floor(n / 3) * 10 + (n % 3 === 0 ? 0 : n % 3 === 1 ? 4 : 7);
return {
mode: "numeric",
numChars: n,
dataBits,
appendData(buffer) {
for (let i = 0; i < n; i += 3) {
const chunk = text.slice(i, i + 3);
buffer.append(Number.parseInt(chunk, 10), chunk.length * 3 + 1);
}
},
};
}
function makeAlphanumeric(text: string): Segment {
const n = text.length;
const dataBits = Math.floor(n / 2) * 11 + (n % 2) * 6;
return {
mode: "alphanumeric",
numChars: n,
dataBits,
appendData(buffer) {
for (let i = 0; i < n; i += 2) {
const first = ALPHANUMERIC_MAP.get(text[i]);
if (first === undefined) {
throw new Error("Text contains non-alphanumeric characters");
}
if (i + 1 < n) {
const second = ALPHANUMERIC_MAP.get(text[i + 1]);
if (second === undefined) {
throw new Error("Text contains non-alphanumeric characters");
}
buffer.append(first * 45 + second, 11);
} else {
buffer.append(first, 6);
}
}
},
};
}
function makeBytes(text: string): Segment {
const bytes = new TextEncoder().encode(text);
return {
mode: "byte",
numChars: bytes.length,
dataBits: bytes.length * 8,
appendData(buffer) {
for (const byte of bytes) {
buffer.append(byte, 8);
}
},
};
}
registry/default/qr-code/lib/path-builders.ts
/**
* SVG path (`d` attribute) builders for QR modules and finder patterns.
*
* All coordinates are in module units. The same builders are used by the React
* component and the standalone string serializer, so the rendered DOM and
* `toSVGString()` output are identical.
*/
import type { QRDotStyle } from "./types";
import { cornerRadii } from "./rounded";
/** Corner radius (in module units) used by the rounded dot style. */
const DOT_RADIUS = 0.5;
/** Inscribed radius used by the circle dot style. */
const DOT_CIRCLE_RADIUS = 0.5;
/** Formats a number compactly: up to 3 decimals, no trailing zeros. */
function f(n: number): string {
return Number(n.toFixed(3)).toString();
}
/** A full-cell square subpath at module (x, y), offset by `o`. */
function squareCell(x: number, y: number, o: number): string {
const px = x + o;
const py = y + o;
return `M${f(px)} ${f(py)}h1v1h-1Z`;
}
/** A circle inscribed in the cell at module (x, y), offset by `o`. */
function circleCell(x: number, y: number, o: number): string {
const cx = x + o + 0.5;
const cy = y + o + 0.5;
const r = DOT_CIRCLE_RADIUS;
return `M${f(cx - r)} ${f(cy)}a${f(r)} ${f(r)} 0 1 0 ${f(r * 2)} 0a${f(r)} ${f(r)} 0 1 0 ${f(-r * 2)} 0Z`;
}
/** A snake-rounded subpath at module (x, y), offset by `o`. */
function roundedCell(
modules: boolean[][],
x: number,
y: number,
o: number,
): string {
const r = DOT_RADIUS;
const { topLeft, topRight, bottomRight, bottomLeft } = cornerRadii(modules, x, y);
const px = x + o;
const py = y + o;
const x0 = px;
const y0 = py;
const x1 = px + 1;
const y1 = py + 1;
let d = `M${f(x0 + (topLeft ? r : 0))} ${f(y0)}`;
d += `H${f(x1 - (topRight ? r : 0))}`;
if (topRight) d += `A${f(r)} ${f(r)} 0 0 1 ${f(x1)} ${f(y0 + r)}`;
d += `V${f(y1 - (bottomRight ? r : 0))}`;
if (bottomRight) d += `A${f(r)} ${f(r)} 0 0 1 ${f(x1 - r)} ${f(y1)}`;
d += `H${f(x0 + (bottomLeft ? r : 0))}`;
if (bottomLeft) d += `A${f(r)} ${f(r)} 0 0 1 ${f(x0)} ${f(y1 - r)}`;
d += `V${f(y0 + (topLeft ? r : 0))}`;
if (topLeft) d += `A${f(r)} ${f(r)} 0 0 1 ${f(x0 + r)} ${f(y0)}`;
d += "Z";
return d;
}
/** Whether module (x, y) lies within one of the three finder pattern squares. */
export function isFinderModule(x: number, y: number, size: number): boolean {
const inTopLeft = x <= 6 && y <= 6;
const inTopRight = x >= size - 7 && y <= 6;
const inBottomLeft = x <= 6 && y >= size - 7;
return inTopLeft || inTopRight || inBottomLeft;
}
/**
* Builds the merged data-module path for all dark modules, excluding the finder
* patterns (which are rendered separately) and any cleared (knockout) modules.
*/
export function buildDataPath(
modules: boolean[][],
size: number,
offset: number,
style: QRDotStyle,
): string {
let d = "";
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
if (!modules[y][x] || isFinderModule(x, y, size)) {
continue;
}
if (style === "circle") {
d += circleCell(x, y, offset);
} else if (style === "rounded") {
d += roundedCell(modules, x, y, offset);
} else {
d += squareCell(x, y, offset);
}
}
}
return d;
}
/** A rectangle subpath, wound clockwise or counter-clockwise. */
function rectPath(
x: number,
y: number,
w: number,
h: number,
clockwise: boolean,
): string {
return clockwise
? `M${f(x)} ${f(y)}h${f(w)}v${f(h)}h${f(-w)}Z`
: `M${f(x)} ${f(y)}v${f(h)}h${f(w)}v${f(-h)}Z`;
}
/** A rounded-rectangle subpath, wound clockwise or counter-clockwise. */
function roundRectPath(
x: number,
y: number,
w: number,
h: number,
radius: number,
clockwise: boolean,
): string {
const r = Math.min(radius, w / 2, h / 2);
if (clockwise) {
return (
`M${f(x + r)} ${f(y)}H${f(x + w - r)}A${f(r)} ${f(r)} 0 0 1 ${f(x + w)} ${f(y + r)}` +
`V${f(y + h - r)}A${f(r)} ${f(r)} 0 0 1 ${f(x + w - r)} ${f(y + h)}` +
`H${f(x + r)}A${f(r)} ${f(r)} 0 0 1 ${f(x)} ${f(y + h - r)}` +
`V${f(y + r)}A${f(r)} ${f(r)} 0 0 1 ${f(x + r)} ${f(y)}Z`
);
}
return (
`M${f(x + r)} ${f(y)}A${f(r)} ${f(r)} 0 0 0 ${f(x)} ${f(y + r)}` +
`V${f(y + h - r)}A${f(r)} ${f(r)} 0 0 0 ${f(x + r)} ${f(y + h)}` +
`H${f(x + w - r)}A${f(r)} ${f(r)} 0 0 0 ${f(x + w)} ${f(y + h - r)}` +
`V${f(y + r)}A${f(r)} ${f(r)} 0 0 0 ${f(x + w - r)} ${f(y)}Z`
);
}
/** A ring (square with a concentric square hole) as an even-odd path. */
function ringPath(
x: number,
y: number,
outer: number,
thickness: number,
style: QRDotStyle,
): string {
const inner = outer - thickness * 2;
const ix = x + thickness;
const iy = y + thickness;
if (style === "circle") {
const oc = circleAt(x + outer / 2, y + outer / 2, outer / 2, true);
const ic = circleAt(x + outer / 2, y + outer / 2, inner / 2, false);
return oc + ic;
}
if (style === "rounded") {
return (
roundRectPath(x, y, outer, outer, outer / 3.5, true) +
roundRectPath(ix, iy, inner, inner, inner / 3, false)
);
}
return rectPath(x, y, outer, outer, true) + rectPath(ix, iy, inner, inner, false);
}
/** A circle path centered at (cx, cy), wound clockwise or counter-clockwise. */
function circleAt(cx: number, cy: number, r: number, clockwise: boolean): string {
const sweep = clockwise ? 1 : 0;
return (
`M${f(cx - r)} ${f(cy)}a${f(r)} ${f(r)} 0 1 ${sweep} ${f(r * 2)} 0` +
`a${f(r)} ${f(r)} 0 1 ${sweep} ${f(-r * 2)} 0Z`
);
}
/** A filled shape (eye) of the given style filling a `n×n` cell at (x, y). */
function fillShape(x: number, y: number, n: number, style: QRDotStyle): string {
if (style === "circle") {
return circleAt(x + n / 2, y + n / 2, n / 2, true);
}
if (style === "rounded") {
return roundRectPath(x, y, n, n, n / 3, true);
}
return rectPath(x, y, n, n, true);
}
/** Top-left module coordinates of each finder's 7×7 square. */
export function finderOrigins(size: number): Array<[number, number]> {
return [
[0, 0],
[size - 7, 0],
[0, size - 7],
];
}
/** Builds the outer-ring path for a finder at origin (fx, fy), offset by `o`. */
export function buildFinderOuter(
fx: number,
fy: number,
offset: number,
style: QRDotStyle,
): string {
return ringPath(fx + offset, fy + offset, 7, 1, style);
}
/** Builds the inner-eye path for a finder at origin (fx, fy), offset by `o`. */
export function buildFinderInner(
fx: number,
fy: number,
offset: number,
style: QRDotStyle,
): string {
return fillShape(fx + offset + 2, fy + offset + 2, 3, style);
}
registry/default/qr-code/lib/reed-solomon.ts
/**
* Reed–Solomon error correction over GF(256) for QR codes.
*
* `computeDivisor` builds the generator polynomial of a given degree, and
* `computeRemainder` divides a block of data codewords by it to produce the
* error correction codewords.
*/
import { gfMultiply } from "./galois";
/**
* Computes the Reed–Solomon generator polynomial divisor for the given number
* of error correction codewords (`degree`). Coefficients are returned highest
* power first, excluding the leading term (which is always 1).
*/
export function computeDivisor(degree: number): Uint8Array {
if (degree < 1 || degree > 255) {
throw new RangeError("Reed–Solomon degree out of range");
}
// Start with the monomial "1" (i.e. the polynomial 0*x^(n-1) + … + 1).
const result = new Uint8Array(degree);
result[degree - 1] = 1;
// Repeatedly multiply by (x - r^i), where r = 0x02 is a field generator.
let root = 1;
for (let i = 0; i < degree; i++) {
for (let j = 0; j < result.length; j++) {
result[j] = gfMultiply(result[j], root);
if (j + 1 < result.length) {
result[j] ^= result[j + 1];
}
}
root = gfMultiply(root, 0x02);
}
return result;
}
/**
* Computes the `divisor.length` error correction codewords for the given data
* block by polynomial division over GF(256).
*/
export function computeRemainder(
data: ArrayLike<number>,
divisor: Uint8Array,
): Uint8Array {
const result = new Uint8Array(divisor.length);
for (let i = 0; i < data.length; i++) {
const factor = data[i] ^ result[0];
result.copyWithin(0, 1);
result[result.length - 1] = 0;
for (let j = 0; j < divisor.length; j++) {
result[j] ^= gfMultiply(divisor[j], factor);
}
}
return result;
}
registry/default/qr-code/lib/rounded.ts
/**
* Neighbor-aware corner rounding for the "rounded" (snake-connected) dot style.
*
* A module corner is rounded only when both of the edges meeting at that corner
* face empty cells. Adjacent filled modules therefore keep their shared edges
* flat, so runs of modules merge into a continuous rounded "snake", while
* isolated modules become fully rounded dots.
*/
/** Which of a module's four corners should be rounded. */
export interface CornerRadii {
topLeft: boolean;
topRight: boolean;
bottomRight: boolean;
bottomLeft: boolean;
}
/** Reads a module, treating out-of-bounds cells as empty. */
function isFilled(modules: boolean[][], x: number, y: number): boolean {
return (
y >= 0 &&
y < modules.length &&
x >= 0 &&
x < modules[y].length &&
modules[y][x]
);
}
/** Computes which corners of the module at (x, y) are convex and rounded. */
export function cornerRadii(
modules: boolean[][],
x: number,
y: number,
): CornerRadii {
const top = isFilled(modules, x, y - 1);
const bottom = isFilled(modules, x, y + 1);
const left = isFilled(modules, x - 1, y);
const right = isFilled(modules, x + 1, y);
return {
topLeft: !top && !left,
topRight: !top && !right,
bottomRight: !bottom && !right,
bottomLeft: !bottom && !left,
};
}
registry/default/qr-code/lib/svg-builder.ts
/**
* The single source of truth for QR SVG geometry.
*
* `buildRenderModel` resolves all render options into a plain data structure of
* path strings and colors. The React component renders this model as JSX while
* `buildQRCodeSVG` serializes the same model to a string, guaranteeing the
* on-screen output and `toSVGString()` are identical.
*/
import type {
ECLevel,
QRMatrix,
QRRenderOptions,
QRDotStyle,
QRFinderStyle,
QRFinderTuple,
} from "./types";
import {
buildDataPath,
buildFinderOuter,
buildFinderInner,
finderOrigins,
} from "./path-builders";
const DEFAULT_MARGIN = 4;
const DEFAULT_FOREGROUND = "currentColor";
const DEFAULT_DOT_STYLE: QRDotStyle = "square";
const DEFAULT_LOGO_SIZE = 0.22;
const MAX_LOGO_SIZE = 0.3;
const DEFAULT_LOGO_PADDING = 1;
/** Corner ids in `finderOrigins` order: top-left, top-right, bottom-left. */
const FINDER_IDS = ["tl", "tr", "bl"] as const;
/** A resolved finder pattern: outer ring and inner eye, with paths and colors. */
export interface FinderRender {
/** Corner identity (top-left, top-right, bottom-left) — a stable React key. */
id: "tl" | "tr" | "bl";
outerPath: string;
outerColor: string;
innerPath: string;
innerColor: string;
}
/** A rectangle in viewBox units. */
export interface KnockoutRect {
x: number;
y: number;
size: number;
color: string;
}
/** An embedded image logo in viewBox units. */
export interface ImageRender {
href: string;
x: number;
y: number;
width: number;
height: number;
alt?: string;
}
/** Fully resolved geometry + colors ready to render or serialize. */
export interface QRRenderModel {
/** Number of modules per side. */
moduleCount: number;
/** ViewBox side length (`moduleCount + 2 * margin`). */
viewBox: number;
margin: number;
foreground: string;
background?: string;
dataPath: string;
finders: FinderRender[];
knockout?: KnockoutRect;
image?: ImageRender;
/** Accessible name, rendered as an SVG `<title>`. */
title?: string;
/** Whether every shape is axis-aligned (enables `crispEdges` rendering). */
crisp: boolean;
}
const EC_RANK: Record<ECLevel, number> = { L: 0, M: 1, Q: 2, H: 3 };
/**
* Resolves the effective error correction level. When a logo is present and the
* caller has not explicitly requested at least `"Q"`, the level is raised to
* `"H"` so the symbol stays scannable despite the cleared center.
*/
export function resolveEcLevel(
ecLevel: ECLevel | undefined,
hasLogo: boolean,
): ECLevel {
if (hasLogo && (ecLevel === undefined || EC_RANK[ecLevel] < EC_RANK.Q)) {
return "H";
}
return ecLevel ?? "M";
}
function normalizeFinders(
finder: QRFinderStyle | QRFinderTuple | undefined,
): QRFinderTuple {
if (Array.isArray(finder)) {
return finder;
}
const single = finder ?? {};
return [single, single, single];
}
function resolveFinder(
style: QRFinderStyle,
dotStyle: QRDotStyle,
foreground: string,
): { outerStyle: QRDotStyle; outerColor: string; innerStyle: QRDotStyle; innerColor: string } {
const outerStyle = style.outerStyle ?? dotStyle;
const outerColor = style.outerColor ?? foreground;
return {
outerStyle,
outerColor,
innerStyle: style.innerStyle ?? outerStyle,
innerColor: style.innerColor ?? outerColor,
};
}
/** Resolves render options against an encoded matrix into a {@link QRRenderModel}. */
export function buildRenderModel(
matrix: QRMatrix,
options: QRRenderOptions = {},
): QRRenderModel {
const margin = Math.max(0, options.margin ?? DEFAULT_MARGIN);
const foreground = options.foreground ?? DEFAULT_FOREGROUND;
const background =
options.background && options.background !== "transparent"
? options.background
: undefined;
const dotStyle = options.dotStyle ?? DEFAULT_DOT_STYLE;
const moduleCount = matrix.size;
const viewBox = moduleCount + margin * 2;
// Clone the matrix so logo knockout does not mutate the caller's data.
const modules = matrix.modules.map((row) => row.slice());
let knockout: KnockoutRect | undefined;
let image: ImageRender | undefined;
if (options.logo) {
const logoSize = Math.min(MAX_LOGO_SIZE, Math.max(0, options.logoSize ?? DEFAULT_LOGO_SIZE));
const padding = Math.max(0, options.logoPadding ?? DEFAULT_LOGO_PADDING);
const imageModules = moduleCount * logoSize;
const knockModules = imageModules + padding * 2;
const center = moduleCount / 2;
// Clear modules whose centers fall inside the knockout box.
const half = knockModules / 2;
const lo = center - half;
const hi = center + half;
for (let y = 0; y < moduleCount; y++) {
for (let x = 0; x < moduleCount; x++) {
const cx = x + 0.5;
const cy = y + 0.5;
if (cx >= lo && cx <= hi && cy >= lo && cy <= hi) {
modules[y][x] = false;
}
}
}
knockout = {
x: margin + center - half,
y: margin + center - half,
size: knockModules,
color: background ?? "#ffffff",
};
const w = options.logo.width ?? imageModules;
const h = options.logo.height ?? imageModules;
image = {
href: options.logo.src,
x: margin + center - w / 2,
y: margin + center - h / 2,
width: w,
height: h,
alt: options.logo.alt,
};
}
const dataPath = buildDataPath(modules, moduleCount, margin, dotStyle);
const finderStyles = normalizeFinders(options.finder);
const origins = finderOrigins(moduleCount);
let crisp = dotStyle === "square";
const finders: FinderRender[] = origins.map(([fx, fy], i) => {
const resolved = resolveFinder(finderStyles[i] ?? {}, dotStyle, foreground);
if (resolved.outerStyle !== "square" || resolved.innerStyle !== "square") {
crisp = false;
}
return {
id: FINDER_IDS[i],
outerPath: buildFinderOuter(fx, fy, margin, resolved.outerStyle),
outerColor: resolved.outerColor,
innerPath: buildFinderInner(fx, fy, margin, resolved.innerStyle),
innerColor: resolved.innerColor,
};
});
return {
moduleCount,
viewBox,
margin,
foreground,
background,
dataPath,
finders,
knockout,
image,
title: options.title,
crisp,
};
}
/** Escapes a string for safe use inside a double-quoted SVG attribute. */
function escapeAttr(value: string): string {
return value
.replace(/&/g, "&")
.replace(/"/g, """)
.replace(/</g, "<")
.replace(/>/g, ">");
}
/** Serializes a {@link QRRenderModel} to an SVG string. */
export function serializeModel(
model: QRRenderModel,
size?: number | string,
): string {
const vb = model.viewBox;
const dimension =
size === undefined
? ""
: ` width="${escapeAttr(String(size))}" height="${escapeAttr(String(size))}"`;
const shapeRendering = model.crisp ? ` shape-rendering="crispEdges"` : "";
let svg =
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${vb} ${vb}"${dimension}${shapeRendering} role="img">`;
if (model.title) {
svg += `<title>${escapeAttr(model.title)}</title>`;
}
if (model.background) {
svg += `<rect width="${vb}" height="${vb}" fill="${escapeAttr(model.background)}"/>`;
}
if (model.dataPath) {
svg += `<path d="${model.dataPath}" fill="${escapeAttr(model.foreground)}"/>`;
}
for (const finder of model.finders) {
svg += `<path d="${finder.outerPath}" fill="${escapeAttr(finder.outerColor)}" fill-rule="evenodd"/>`;
svg += `<path d="${finder.innerPath}" fill="${escapeAttr(finder.innerColor)}"/>`;
}
if (model.knockout) {
const k = model.knockout;
svg += `<rect x="${k.x}" y="${k.y}" width="${k.size}" height="${k.size}" fill="${escapeAttr(k.color)}"/>`;
}
// Element logos carry no `href` (they render as a live <foreignObject> and
// can't be serialized), so skip emitting an empty <image> on export.
if (model.image && model.image.href) {
const img = model.image;
const alt = img.alt ? ` aria-label="${escapeAttr(img.alt)}"` : "";
svg +=
`<image href="${escapeAttr(img.href)}" x="${img.x}" y="${img.y}" ` +
`width="${img.width}" height="${img.height}" preserveAspectRatio="xMidYMid meet"${alt}/>`;
}
svg += "</svg>";
return svg;
}
/** Builds a complete SVG string for an encoded matrix. */
export function buildQRCodeSVG(
matrix: QRMatrix,
options: QRRenderOptions = {},
size?: number | string,
): string {
return serializeModel(buildRenderModel(matrix, options), size);
}
registry/default/qr-code/lib/tables.ts
/**
* Constant tables and derived geometry from ISO/IEC 18004.
*
* The full per-version codeword/block layout is derived arithmetically from two
* compact tables (error correction codewords per block, and number of error
* correction blocks) rather than a large hard-coded table.
*/
import type { ECLevel, QRMode } from "./types";
/** Ordinal index into the EC tables below, in ascending recovery order. */
const EC_ORDINAL: Record<ECLevel, number> = { L: 0, M: 1, Q: 2, H: 3 };
/** 5-bit format-info value for each EC level (not the same as the ordinal). */
const EC_FORMAT_BITS: Record<ECLevel, number> = { L: 1, M: 0, Q: 3, H: 2 };
/** 4-bit mode indicator emitted at the start of each segment. */
export const MODE_INDICATOR: Record<QRMode, number> = {
numeric: 0x1,
alphanumeric: 0x2,
byte: 0x4,
};
/** The 45-character alphanumeric set, indexed by value. */
export const ALPHANUMERIC_CHARSET =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
// Indexed by [ecOrdinal][version]; index 0 of each row is an unused placeholder.
// prettier-ignore
const ECC_CODEWORDS_PER_BLOCK: number[][] = [
[-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], // L
[-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28], // M
[-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], // Q
[-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], // H
];
// Indexed by [ecOrdinal][version]; index 0 of each row is an unused placeholder.
// prettier-ignore
const NUM_ERROR_CORRECTION_BLOCKS: number[][] = [
[-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25], // L
[-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49], // M
[-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68], // Q
[-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81], // H
];
/** Returns the number of modules per side for a version (`17 + 4 * version`). */
export function getSize(version: number): number {
return version * 4 + 17;
}
/** Returns the EC ordinal for a level. */
export function getEcOrdinal(ecLevel: ECLevel): number {
return EC_ORDINAL[ecLevel];
}
/** Returns the 5-bit format-info value for a level. */
export function getEcFormatBits(ecLevel: ECLevel): number {
return EC_FORMAT_BITS[ecLevel];
}
/** Number of EC codewords per block for a version + level. */
export function getEccCodewordsPerBlock(
version: number,
ecLevel: ECLevel,
): number {
return ECC_CODEWORDS_PER_BLOCK[EC_ORDINAL[ecLevel]][version];
}
/** Number of error correction blocks for a version + level. */
export function getNumEcBlocks(version: number, ecLevel: ECLevel): number {
return NUM_ERROR_CORRECTION_BLOCKS[EC_ORDINAL[ecLevel]][version];
}
/**
* Total number of data-and-EC modules available for a version, excluding all
* function patterns and format/version information.
*/
export function getNumRawDataModules(version: number): number {
if (version < 1 || version > 40) {
throw new RangeError("Version out of range");
}
let result = (16 * version + 128) * version + 64;
if (version >= 2) {
const numAlign = Math.floor(version / 7) + 2;
result -= (25 * numAlign - 10) * numAlign - 55;
if (version >= 7) {
result -= 36;
}
}
return result;
}
/** Number of usable data codewords (excluding EC) for a version + level. */
export function getNumDataCodewords(version: number, ecLevel: ECLevel): number {
return (
Math.floor(getNumRawDataModules(version) / 8) -
getEccCodewordsPerBlock(version, ecLevel) * getNumEcBlocks(version, ecLevel)
);
}
/** Width, in bits, of the character-count indicator for a mode + version. */
export function getCharCountBits(mode: QRMode, version: number): number {
const band = version <= 9 ? 0 : version <= 26 ? 1 : 2;
switch (mode) {
case "numeric":
return [10, 12, 14][band];
case "alphanumeric":
return [9, 11, 13][band];
case "byte":
return [8, 16, 16][band];
}
}
/**
* Returns the center coordinates of the alignment patterns for a version. The
* three positions that collide with finder patterns are filtered out by the
* caller.
*/
export function getAlignmentPatternPositions(version: number): number[] {
if (version === 1) {
return [];
}
const numAlign = Math.floor(version / 7) + 2;
const step =
version === 32
? 26
: Math.ceil((version * 4 + 4) / (numAlign * 2 - 2)) * 2;
const result: number[] = [6];
for (
let pos = getSize(version) - 7;
result.length < numAlign;
pos -= step
) {
result.splice(1, 0, pos);
}
return result;
}
registry/default/qr-code/lib/types.ts
/**
* Shared types for the QR code encoder and SVG renderer.
*
* Pure type declarations only — this file has zero runtime cost and is safe to
* import from both server and client code.
*/
import type * as React from "react";
/** Error correction level, in ascending order of recovery capacity. */
export type ECLevel = "L" | "M" | "Q" | "H";
/** Supported encoding modes. Kanji is intentionally not implemented. */
export type QRMode = "numeric" | "alphanumeric" | "byte";
/** Options accepted by {@link encode}. */
export interface EncodeOptions {
/** Error correction level. Defaults to `"M"`. */
ecLevel?: ECLevel;
/** Smallest version (1–40) to consider. Defaults to `1`. */
minVersion?: number;
/** Largest version (1–40) to consider. Defaults to `40`. */
maxVersion?: number;
/**
* Force a specific mode. `"auto"` (the default) selects the most compact
* mode that can represent the input.
*/
mode?: QRMode | "auto";
/**
* When `true` (the default), the error correction level is silently upgraded
* to the highest level the chosen version can hold for free.
*/
boostEcl?: boolean;
}
/** A fully encoded QR symbol: a square grid of dark/light modules. */
export interface QRMatrix {
/** Number of modules per side (`17 + 4 * version`). */
size: number;
/** Row-major grid; `modules[y][x] === true` means a dark module. */
modules: boolean[][];
/** The version (1–40) that was selected. */
version: number;
/** The error correction level actually used (may be boosted). */
ecLevel: ECLevel;
/** The mode actually used to encode the data. */
mode: QRMode;
/** The mask pattern (0–7) chosen during encoding. */
maskPattern: number;
}
/** Visual style of a single module / dot. */
export type QRDotStyle = "square" | "circle" | "rounded";
/**
* Independent styling for a finder pattern ("eye"). A finder has an outer 7×7
* ring and an inner 3×3 eye that can be styled and colored separately.
*/
export interface QRFinderStyle {
/** Style of the outer ring. Falls back to the component's `dotStyle`. */
outerStyle?: QRDotStyle;
/** Color of the outer ring. Falls back to `foreground`. */
outerColor?: string;
/** Style of the inner eye. Falls back to `outerStyle`. */
innerStyle?: QRDotStyle;
/** Color of the inner eye. Falls back to `outerColor` / `foreground`. */
innerColor?: string;
}
/** A logo described by an image source rather than a React element. */
export interface QRLogoObject {
/** Image URL or data URI. */
src: string;
/** Logo width, in module units. */
width?: number;
/** Logo height, in module units. */
height?: number;
/** Alternative text for the embedded image. */
alt?: string;
}
/** The three finder patterns, ordered top-left, top-right, bottom-left. */
export type QRFinderTuple = [QRFinderStyle, QRFinderStyle, QRFinderStyle];
/** Render options shared by the component and the standalone SVG builders. */
export interface QRRenderOptions {
ecLevel?: ECLevel;
dotStyle?: QRDotStyle;
/** Quiet-zone width, in modules. Defaults to `4`. */
margin?: number;
/** Foreground (dark module) color. Defaults to `"currentColor"`. */
foreground?: string;
/** Background color. Defaults to `"transparent"`. */
background?: string;
/** Unified or per-corner finder styling. */
finder?: QRFinderStyle | QRFinderTuple;
/** Fraction of the symbol width occupied by the logo (clamped to ≤ 0.3). */
logoSize?: number;
/** Quiet area, in modules, knocked out around the logo. Defaults to `1`. */
logoPadding?: number;
/** Logo image; only `QRLogoObject` logos are serialized by the SVG builder. */
logo?: QRLogoObject;
/** Minimum QR version. */
minVersion?: number;
/** Accessible name rendered as an SVG `<title>` element. */
title?: string;
}
/** Options for the standalone {@link toSVGString} / {@link toDataURL} helpers. */
export interface QRExportOptions extends QRRenderOptions {
/** The data to encode. */
value: string;
/** Rendered size, in pixels (used as the SVG `width`/`height`). */
size?: number;
}
/** Output format for {@link toDataURL}. */
export type QRDataURLType = "png" | "jpeg" | "svg";
/** Options for {@link toDataURL}. */
export interface QRDataURLOptions extends Partial<QRExportOptions> {
/** Output format. Defaults to `"png"`. */
type?: QRDataURLType;
/** JPEG quality (0–1). Ignored for other formats. */
quality?: number;
/** Output raster size per side, in pixels. Defaults to `1024`. */
pixelSize?: number;
}
/** Imperative handle exposed by the `<QRCode>` ref. */
export interface QRCodeHandle {
/** Returns the rendered SVG as a string (SSR-safe). */
toSVGString: () => string;
/** Returns a data URL for the rendered code (PNG/JPEG via canvas, or SVG). */
toDataURL: (options?: QRDataURLOptions) => Promise<string>;
/** Returns the underlying encoded matrix. */
getMatrix: () => QRMatrix;
}
/** A logo accepted by the `<QRCode>` component. */
export type QRLogo = React.ReactNode | QRLogoObject;