shadimage
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, "&amp;")
    .replace(/"/g, "&quot;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

/** 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;

More from cubby-ui

Browse all components in this library →