delete js-qrcode dependencies (no needed anymore)

This commit is contained in:
infrost
2026-07-08 03:20:34 +01:00
parent 001f152eec
commit bea0f5cf03
23 changed files with 680 additions and 267 deletions
+1 -2
View File
@@ -28,7 +28,7 @@
"verify:raptorq-wasm": "node scripts/verify-raptorq-wasm.mjs",
"cli": "bun run src/cli/qr-stream.ts",
"cli:file": "bun run src/cli/qr-stream.ts",
"build:cli": "esbuild src/cli/qr-stream.ts --bundle --platform=node --target=node18 --outfile=dist/qr-stream.js --format=esm --external:node:*",
"build:cli": "esbuild src/cli/qr-stream.ts --bundle --platform=node --target=node18 --outfile=dist/qr-stream.js --format=esm --external:node:* && node scripts/copy-cli-wasm-assets.mjs",
"prepublishOnly": "pnpm run build"
},
"keywords": [
@@ -65,7 +65,6 @@
"fflate": "^0.8.2",
"gifenc": "^1.0.3",
"preact": "10.29.4",
"qrcode-generator": "1",
"zxing-wasm": "^3.1.0"
}
}
-8
View File
@@ -17,9 +17,6 @@ importers:
preact:
specifier: 10.29.4
version: 10.29.4
qrcode-generator:
specifier: '1'
version: 1.5.2
zxing-wasm:
specifier: ^3.1.0
version: 3.1.0(@types/emscripten@1.41.5)
@@ -904,9 +901,6 @@ packages:
preact@10.29.4:
resolution: {integrity: sha512-GMpwh9+NJ8tSmqwIaVyFRQkiKfBEzQ+k7r7tle4W+kaJ+7wJiB9hFz9BixAomMtenPPSBfM4bZhXozGxhf0uFQ==}
qrcode-generator@1.5.2:
resolution: {integrity: sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==}
rollup@4.62.2:
resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -1830,8 +1824,6 @@ snapshots:
preact@10.29.4: {}
qrcode-generator@1.5.2: {}
rollup@4.62.2:
dependencies:
'@types/estree': 1.0.9
+19
View File
@@ -0,0 +1,19 @@
import { copyFileSync, mkdirSync } from 'node:fs';
import { basename, join } from 'node:path';
const root = process.cwd();
const dist = join(root, 'dist');
const assets = [
{
source: join(root, 'src', 'fast_qr_wasm', 'wasm', 'qrstream_fast_qr_wasm_bg.wasm'),
target: join(dist, 'qrstream_fast_qr_wasm_bg.wasm'),
},
];
mkdirSync(dist, { recursive: true });
for (const asset of assets) {
copyFileSync(asset.source, asset.target);
console.log(`copied ${basename(asset.target)}`);
}
+1 -2
View File
@@ -17,7 +17,7 @@
import { readFileSync, existsSync, openSync, closeSync } from 'fs';
import { ReadStream } from 'tty';
import { COMPATIBLE_QR_ENCODER, encodeQRCodeMatrix } from '../core/qr/qr_encoder';
import { encodeQRCodeMatrix } from '../core/qr/qr_encoder_node';
import { packetize } from '../core/sender/packetizer';
import { scheduleFrames } from '../core/sender/scheduler';
import { QR_VERSION, ECC_LEVEL } from '../core/protocol/constants';
@@ -232,7 +232,6 @@ async function main() {
pkt,
QR_VERSION,
ECC_LEVEL,
COMPATIBLE_QR_ENCODER,
);
frames.push(renderToTerminal(matrix));
}
+4 -5
View File
@@ -4,11 +4,10 @@
* Each Worker that needs a `QrRenderer` should call `ensureFastQrWasm()` once,
* then instantiate `new QrRenderer()` from this module. The WASM linear memory
* is exposed via `getFastQrWasmMemory()` so callers can build a zero-copy
* `Uint8ClampedArray` view after each `render()` call.
* view after each `render_rgba()` or `render_matrix()` call.
*
* If the WASM artifacts have not been built yet (stub JS throws on init) every
* call silently falls back — callers should check `isFastQrAvailable()` and use
* the JS QR path when it returns false.
* If the WASM artifacts have not been built yet (stub JS throws on init),
* callers surface a controlled unavailable error.
*
* @module
*/
@@ -51,7 +50,7 @@ export async function ensureFastQrWasm(): Promise<void> {
/**
* Returns true after `ensureFastQrWasm()` has resolved successfully.
* Use this to decide whether to use the WASM or JS fallback path.
* Use this for diagnostics and tests.
*/
export function isFastQrAvailable(): boolean {
return wasmOutput !== null;
+1 -1
View File
@@ -29,7 +29,7 @@ function calcPixelSize(moduleCount: number, scale: number): number {
* A 4-module quiet zone (white) is added on all four sides.
* Output is pure black (0,0,0,255) and white (255,255,255,255).
*
* @param matrix QR module matrix from `generateQRMatrix`
* @param matrix QR module matrix where `true` is a dark module
* @param scale Pixels per module (default 3)
* @returns RGBA `ImageData` (non-premultiplied, 4 bytes per pixel)
*/
+6 -64
View File
@@ -1,13 +1,11 @@
/**
* QR code generation wrapper.
* QR capacity helpers.
*
* Uses the `qrcode-generator` library to create QR codes in byte mode
* with exact version and ECC level. Throws if the data payload exceeds
* the capacity of the requested version/ECC combination.
* The actual QR symbol writers live in `qr_encoder_browser.ts` and
* `qr_encoder_node.ts`; this module only keeps the shared capacity math used
* by transfer profiles and packet sizing.
*/
import qrcode from 'qrcode-generator';
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
@@ -15,7 +13,7 @@ import qrcode from 'qrcode-generator';
export type EccLevel = 'L' | 'M' | 'Q' | 'H';
// ---------------------------------------------------------------------------
// RS block table (copied from qrcode-generator's internal RS_BLOCK_TABLE)
// RS block table
// Layout per version: [L-entry, M-entry, Q-entry, H-entry]
// Each entry is a flat array of [count, totalCodewords, dataCodewords, …]
// repeating for each block-group type.
@@ -125,7 +123,7 @@ export function getMaxByteCapacity(version: number, eccLevel: EccLevel): number
/**
* Maximum raw bytes accepted by `zxing-wasm/writer` when the input is a
* Uint8Array. ZXing emits a binary ECI segment for byte input, which consumes
* two extra QR codewords versus the legacy qrcode-generator Byte-mode path.
* two extra QR codewords versus the plain Byte-mode path used by fast_qr.
*/
export function getMaxZXingWriterByteCapacity(
version: number,
@@ -179,59 +177,3 @@ function getTotalDataCodewords(version: number, eccLevel: EccLevel): number {
return totalDataCodewords;
}
// ---------------------------------------------------------------------------
// QR generation
// ---------------------------------------------------------------------------
/**
* Generate a QR code symbol in Byte mode for the given raw data.
*
* @param data The raw bytes to encode
* @param version QR code version (1 40)
* @param eccLevel Error correction level
* @returns A 2-D boolean array where `true` = black module, `false` = white
* @throws `Error` if `data` exceeds the maximum payload for the requested
* version and ECC level.
*/
export function generateQRMatrix(
data: Uint8Array,
version: number,
eccLevel: EccLevel,
): boolean[][] {
if (version < 1 || version > 40) {
throw new Error(`Invalid QR version: ${version}. Must be 1-40.`);
}
// Capacity check
const maxBytes = getMaxByteCapacity(version, eccLevel);
if (data.length > maxBytes) {
const minVer = getMinVersion(data.length, eccLevel);
throw new Error(
`Data too large for V${version}-${eccLevel}. ` +
`Maximum ${maxBytes} data bytes in byte mode, got ${data.length}. ` +
`Minimum required version: V${minVer}.`,
);
}
// Convert Uint8Array → string (lossless for bytes 0-255)
// The library's default stringToBytes does s.charCodeAt(i) & 0xff,
// which preserves byte values through the string encoding.
const dataStr = String.fromCharCode(...data);
const qr = qrcode(version as any, eccLevel);
qr.addData(dataStr, 'Byte');
qr.make();
const moduleCount = qr.getModuleCount();
const matrix: boolean[][] = [];
for (let row = 0; row < moduleCount; row++) {
const rowArr: boolean[] = [];
for (let col = 0; col < moduleCount; col++) {
rowArr.push(qr.isDark(row, col));
}
matrix.push(rowArr);
}
return matrix;
}
+1 -61
View File
@@ -9,22 +9,17 @@
* - This file must stay browser-WASM-free.
* - Browser-only encoders such as ZXing WASM and fast_qr WASM are wired in
* `qr_encoder_browser.ts`.
* - Node/CLI-safe fallback is always `js-qrcode`.
* - Node/CLI WASM loading is wired in `qr_encoder_node.ts`.
*/
import { rasterizeQR } from './frame_raster';
import { generateQRMatrix, type EccLevel } from './qr_encode';
export const QR_ENCODERS = [
'fast-qr-wasm',
'zxing-wasm',
'js-qrcode',
] as const;
export type QREncoder = typeof QR_ENCODERS[number];
export const DEFAULT_QR_ENCODER: QREncoder = 'fast-qr-wasm';
export const COMPATIBLE_QR_ENCODER: QREncoder = 'js-qrcode';
export function normalizeQREncoder(value: unknown): QREncoder {
switch (value) {
@@ -38,11 +33,6 @@ export function normalizeQREncoder(value: unknown): QREncoder {
case 'zxingWasm':
return 'zxing-wasm';
case 'js-qrcode':
case 'js':
case 'jsQRCode':
return 'js-qrcode';
default:
return DEFAULT_QR_ENCODER;
}
@@ -54,55 +44,5 @@ export function formatQREncoder(encoder: QREncoder): string {
return 'fast_qr WASM';
case 'zxing-wasm':
return 'ZXing WASM';
case 'js-qrcode':
return 'JS QR';
}
}
export function encodeQRCodeMatrixWithJS(
data: Uint8Array,
version: number,
eccLevel: EccLevel,
): boolean[][] {
return generateQRMatrix(data, version, eccLevel);
}
export function renderQRCodeImageDataWithJS(
data: Uint8Array,
version: number,
eccLevel: EccLevel,
scale: number,
): ImageData {
return rasterizeQR(generateQRMatrix(data, version, eccLevel), scale);
}
export async function encodeQRCodeMatrix(
data: Uint8Array,
version: number,
eccLevel: EccLevel,
encoder: QREncoder = COMPATIBLE_QR_ENCODER,
): Promise<boolean[][]> {
if (encoder !== 'js-qrcode') {
throw new Error(
`${formatQREncoder(encoder)} is only available through qr_encoder_browser.`,
);
}
return encodeQRCodeMatrixWithJS(data, version, eccLevel);
}
export async function renderQRCodeImageData(
data: Uint8Array,
version: number,
eccLevel: EccLevel,
scale: number,
encoder: QREncoder = COMPATIBLE_QR_ENCODER,
): Promise<ImageData> {
if (encoder !== 'js-qrcode') {
throw new Error(
`${formatQREncoder(encoder)} is only available through qr_encoder_browser.`,
);
}
return renderQRCodeImageDataWithJS(data, version, eccLevel, scale);
}
+53 -27
View File
@@ -2,15 +2,11 @@
* Browser QR encoder facade.
*
* This file is allowed to import browser-only WASM encoders. Node/CLI code
* should import `qr_encoder.ts` so esbuild does not pull browser-only wasm
* should import `qr_encoder_node.ts` so esbuild does not pull browser-only wasm
* assets into the terminal bundle.
*/
import {
type QREncoder,
encodeQRCodeMatrixWithJS,
renderQRCodeImageDataWithJS,
} from './qr_encoder';
import { type QREncoder } from './qr_encoder';
import type { EccLevel } from './qr_encode';
import {
generateQRMatrixWithZXing,
@@ -21,6 +17,7 @@ import {
isFastQrAvailable,
getFastQrWasmMemory,
QrRenderer,
fastQrUnavailableMessage,
} from './fast_qr_wasm';
export * from './qr_encoder';
@@ -32,7 +29,7 @@ const ECC_TO_NUM: Record<EccLevel, number> = {
H: 3,
};
let fastQrRendererPromise: Promise<QrRenderer | null> | null = null;
let fastQrRendererPromise: Promise<QrRenderer> | null = null;
export async function encodeQRCodeMatrix(
data: Uint8Array,
@@ -42,12 +39,7 @@ export async function encodeQRCodeMatrix(
): Promise<boolean[][]> {
switch (encoder) {
case 'fast-qr-wasm':
// fast_qr path currently exposes a raster renderer, not a matrix API.
// For matrix callers, use the existing JS encoder as a safe fallback.
return encodeQRCodeMatrixWithJS(data, version, eccLevel);
case 'js-qrcode':
return encodeQRCodeMatrixWithJS(data, version, eccLevel);
return encodeQRCodeMatrixWithFastQr(data, version, eccLevel);
case 'zxing-wasm':
return generateQRMatrixWithZXing(data, version, eccLevel);
@@ -65,22 +57,27 @@ export async function renderQRCodeImageData(
case 'fast-qr-wasm':
return renderQRCodeImageDataWithFastQr(data, version, eccLevel, scale);
case 'js-qrcode':
return renderQRCodeImageDataWithJS(data, version, eccLevel, scale);
case 'zxing-wasm':
return renderQRCodeImageDataWithZXing(data, version, eccLevel, scale);
}
}
async function getFastQrRenderer(): Promise<QrRenderer | null> {
async function getFastQrRenderer(): Promise<QrRenderer> {
if (!fastQrRendererPromise) {
fastQrRendererPromise = ensureFastQrWasm()
.then(() => new QrRenderer())
.catch(() => null);
.catch((err: unknown) => {
fastQrRendererPromise = null;
const message = err instanceof Error ? err.message : String(err);
throw new Error(`${fastQrUnavailableMessage()} ${message}`);
});
}
return fastQrRendererPromise;
const renderer = await fastQrRendererPromise;
if (!renderer || !isFastQrAvailable()) {
throw new Error(fastQrUnavailableMessage());
}
return renderer;
}
async function renderQRCodeImageDataWithFastQr(
@@ -90,21 +87,50 @@ async function renderQRCodeImageDataWithFastQr(
scale: number,
): Promise<ImageData> {
const renderer = await getFastQrRenderer();
if (!renderer || !isFastQrAvailable()) {
return renderQRCodeImageDataWithJS(data, version, eccLevel, scale);
}
const eccNum = ECC_TO_NUM[eccLevel];
const sidePx = renderer.render(data, version, eccNum, scale);
const sidePx = renderer.render_rgba(data, version, eccNum, scale);
const byteLen = sidePx * sidePx * 4;
const memory = getFastQrWasmMemory();
const ptr = renderer.buf_ptr();
const ptr = renderer.rgba_ptr();
const view = new Uint8ClampedArray(memory.buffer, ptr, byteLen);
const copy = new Uint8ClampedArray(byteLen);
copy.set(view);
return new ImageData(copy, sidePx, sidePx);
}
}
async function encodeQRCodeMatrixWithFastQr(
data: Uint8Array,
version: number,
eccLevel: EccLevel,
): Promise<boolean[][]> {
const renderer = await getFastQrRenderer();
const eccNum = ECC_TO_NUM[eccLevel];
const sideModules = renderer.render_matrix(data, version, eccNum);
const byteLen = sideModules * sideModules;
const memory = getFastQrWasmMemory();
const ptr = renderer.matrix_ptr();
const modules = new Uint8Array(memory.buffer, ptr, byteLen);
return matrixFromModuleBytes(modules, sideModules);
}
function matrixFromModuleBytes(modules: Uint8Array, sideModules: number): boolean[][] {
const matrix: boolean[][] = [];
for (let row = 0; row < sideModules; row++) {
const outRow: boolean[] = [];
const rowOffset = row * sideModules;
for (let col = 0; col < sideModules; col++) {
outRow.push(modules[rowOffset + col] === 1);
}
matrix.push(outRow);
}
return matrix;
}
+148
View File
@@ -0,0 +1,148 @@
/**
* Node/CLI QR encoder facade.
*
* The web app uses `qr_encoder_browser.ts`, where Vite owns WASM URL loading.
* The CLI uses this file instead so fast_qr's wasm-bindgen module can be
* initialised from filesystem bytes.
*/
import {
QrRenderer,
initSync,
type InitOutput,
} from '../../fast_qr_wasm/wasm/qrstream_fast_qr_wasm.js';
import {
DEFAULT_QR_ENCODER,
formatQREncoder,
type QREncoder,
} from './qr_encoder';
import type { EccLevel } from './qr_encode';
import { readNodeWasmAsset } from '@/core/wasm/node_assets';
export * from './qr_encoder';
export const DEFAULT_CLI_QR_ENCODER: QREncoder = DEFAULT_QR_ENCODER;
const FAST_QR_WASM_ASSET = {
distFileName: 'qrstream_fast_qr_wasm_bg.wasm',
sourceRelativePath: 'src/fast_qr_wasm/wasm/qrstream_fast_qr_wasm_bg.wasm',
envVar: 'QRSTREAM_FAST_QR_WASM',
};
const ECC_TO_NUM: Record<EccLevel, number> = {
L: 0,
M: 1,
Q: 2,
H: 3,
};
let fastQrInitOutput: InitOutput | null = null;
let fastQrRenderer: QrRenderer | null = null;
let fastQrInitFailed = false;
export function isFastQrNodeAvailable(): boolean {
try {
getFastQrNodeRenderer();
return true;
} catch {
return false;
}
}
export async function encodeQRCodeMatrix(
data: Uint8Array,
version: number,
eccLevel: EccLevel,
encoder: QREncoder = DEFAULT_CLI_QR_ENCODER,
): Promise<boolean[][]> {
switch (encoder) {
case 'fast-qr-wasm':
return encodeQRCodeMatrixWithFastQr(data, version, eccLevel);
case 'zxing-wasm':
throw new Error(
`${formatQREncoder(encoder)} is not available in the CLI yet. ` +
'Use fast_qr WASM.',
);
}
}
export async function renderQRCodeImageData(
data: Uint8Array,
version: number,
eccLevel: EccLevel,
scale: number,
encoder: QREncoder = DEFAULT_CLI_QR_ENCODER,
): Promise<ImageData> {
if (encoder === 'zxing-wasm') {
throw new Error(
`${formatQREncoder(encoder)} is not available in the CLI yet. ` +
'Use fast_qr WASM.',
);
}
const renderer = getFastQrNodeRenderer();
const eccNum = ECC_TO_NUM[eccLevel];
const sidePx = renderer.render_rgba(data, version, eccNum, scale);
const byteLen = sidePx * sidePx * 4;
const view = new Uint8ClampedArray(
fastQrInitOutput!.memory.buffer,
renderer.rgba_ptr(),
byteLen,
);
const copy = new Uint8ClampedArray(byteLen);
copy.set(view);
return new ImageData(copy, sidePx, sidePx);
}
function encodeQRCodeMatrixWithFastQr(
data: Uint8Array,
version: number,
eccLevel: EccLevel,
): boolean[][] {
const renderer = getFastQrNodeRenderer();
const eccNum = ECC_TO_NUM[eccLevel];
const sideModules = renderer.render_matrix(data, version, eccNum);
const byteLen = sideModules * sideModules;
const modules = new Uint8Array(
fastQrInitOutput.memory.buffer,
renderer.matrix_ptr(),
byteLen,
);
return matrixFromModuleBytes(modules, sideModules);
}
function getFastQrNodeRenderer(): QrRenderer {
if (fastQrRenderer) return fastQrRenderer;
if (fastQrInitFailed) {
throw new Error('fast_qr WASM is unavailable in Node.');
}
try {
const wasmBytes = readNodeWasmAsset(FAST_QR_WASM_ASSET);
fastQrInitOutput = initSync({ module: wasmBytes });
fastQrRenderer = new QrRenderer();
return fastQrRenderer;
} catch (err) {
fastQrInitFailed = true;
throw err instanceof Error ? err : new Error(String(err));
}
}
function matrixFromModuleBytes(modules: Uint8Array, sideModules: number): boolean[][] {
const matrix: boolean[][] = [];
for (let row = 0; row < sideModules; row++) {
const outRow: boolean[] = [];
const rowOffset = row * sideModules;
for (let col = 0; col < sideModules; col++) {
outRow.push(modules[rowOffset + col] === 1);
}
matrix.push(outRow);
}
return matrix;
}
+63
View File
@@ -0,0 +1,63 @@
/**
* Node-only WASM asset resolution.
*
* Browser builds let Vite rewrite `*.wasm?url` imports. The CLI is bundled as
* a plain Node ESM file, so WASM sidecars need an explicit filesystem lookup.
* Keep this generic so future CLI ZXing/RaptorQ loaders can reuse the same
* package-relative asset convention.
*/
import { existsSync, readFileSync } from 'node:fs';
import { dirname, parse, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
export interface NodeWasmAsset {
/** File copied next to the bundled CLI entrypoint in `dist/`. */
distFileName: string;
/** Repo/package-relative source path kept for dev and npm source fallback. */
sourceRelativePath: string;
/** Optional absolute path override for local debugging. */
envVar?: string;
}
export function readNodeWasmAsset(asset: NodeWasmAsset): Uint8Array {
for (const candidate of nodeWasmAssetCandidates(asset)) {
if (existsSync(candidate)) {
return new Uint8Array(readFileSync(candidate));
}
}
throw new Error(
`WASM asset not found: ${asset.distFileName}. ` +
`Expected it next to the CLI bundle or at ${asset.sourceRelativePath}.`,
);
}
export function nodeWasmAssetCandidates(asset: NodeWasmAsset): string[] {
const here = dirname(fileURLToPath(import.meta.url));
const candidates: Array<string | undefined> = [
asset.envVar ? process.env[asset.envVar] : undefined,
resolve(here, asset.distFileName),
resolve(process.cwd(), 'dist', asset.distFileName),
resolve(process.cwd(), asset.sourceRelativePath),
findUp(here, asset.sourceRelativePath),
];
return dedupe(candidates.filter((value): value is string => Boolean(value)));
}
function findUp(startDir: string, relativePath: string): string | undefined {
let dir = resolve(startDir);
const root = parse(dir).root;
while (true) {
const candidate = resolve(dir, relativePath);
if (existsSync(candidate)) return candidate;
if (dir === root) return undefined;
dir = dirname(dir);
}
}
function dedupe(values: string[]): string[] {
return Array.from(new Set(values));
}
+118 -24
View File
@@ -6,10 +6,11 @@ not need a Rust toolchain. It can be pasted directly into a Colab cell. If a
QR Stream repo is present, artifacts are copied into src/fast_qr_wasm/wasm;
otherwise they are written to /content/qrstream_fast_qr_wasm_artifacts and zipped.
The crate exposes a single `QrRenderer` struct whose internal RGBA buffer is
allocated exactly once at construction time. Callers invoke `render()` and then
read the result directly through a JavaScript `Uint8ClampedArray` view into the
WASM linear memory zero heap allocation per frame on the Rust side.
The crate exposes a single `QrRenderer` struct whose internal RGBA and matrix
buffers are allocated exactly once at construction time. Callers invoke
`render_rgba()` for app/live/GIF rendering or `render_matrix()` for CLI/tests,
then read the result directly through JavaScript views into WASM linear memory.
The legacy `render()` method remains as an alias for `render_rgba()`.
"""
from __future__ import annotations
@@ -60,17 +61,26 @@ wasm-opt = false
# src/lib.rs
# ---------------------------------------------------------------------------
# Design:
# - `QrRenderer` allocates a single fixed RGBA buffer large enough to hold
# a version-40 QR at scale 8 (the largest sensible combination):
# - `QrRenderer` allocates two fixed buffers:
# 1. An RGBA buffer large enough to hold a version-40 QR at scale 8:
# (177 modules + 4*2 quiet-zone) * 8 px = 1480 px per side
# 1480 * 1480 * 4 bytes ≈ 8.76 MB
# - `render()` writes RGBA directly into that buffer and returns the side
# pixel count so JS knows how many bytes are valid.
# - `buf_ptr()` / `buf_len()` expose the raw pointer and total capacity.
# 2. A 177*177 byte matrix buffer for raw module data.
# - `render_rgba()` writes RGBA directly into the RGBA buffer and returns the
# side pixel count so JS knows how many bytes are valid.
# - `render_matrix()` writes 0/1 bytes directly into the matrix buffer and
# returns the side module count.
# - `render()` is a backward-compatible alias for `render_rgba()`; it also
# outputs RGBA into the same fixed RGBA buffer.
# - `buf_ptr()` / `buf_len()` expose the RGBA buffer for compatibility with
# the previous wrapper. `rgba_ptr()` / `rgba_len()` and
# `matrix_ptr()` / `matrix_len()` are the explicit APIs.
# - JS zero-copy path:
# const sidePx = renderer.render(data, version, ecc, scale);
# const view = new Uint8ClampedArray(wasm.memory.buffer, renderer.buf_ptr(), sidePx*sidePx*4);
# const sidePx = renderer.render_rgba(data, version, ecc, scale);
# const view = new Uint8ClampedArray(wasm.memory.buffer, renderer.rgba_ptr(), sidePx*sidePx*4);
# const imageData = new ImageData(view.slice(), sidePx, sidePx);
# const sideMods = renderer.render_matrix(data, version, ecc);
# const modules = new Uint8Array(wasm.memory.buffer, renderer.matrix_ptr(), sideMods*sideMods);
WRAPPER_LIB_RS = r"""
use fast_qr::{ECL, Version, QRBuilder};
@@ -85,6 +95,12 @@ const MAX_SIDE_PX: u32 = (177 + QZ * 2) * 8;
/// Total bytes in the fixed RGBA buffer.
const MAX_BUF: usize = (MAX_SIDE_PX as usize) * (MAX_SIDE_PX as usize) * 4;
/// Maximum raw module count in a QR matrix (V40 = 177x177).
const MAX_MATRIX_SIDE: usize = 177;
/// Total bytes in the fixed matrix buffer. Each module is 0=light, 1=dark.
const MAX_MATRIX_BUF: usize = MAX_MATRIX_SIDE * MAX_MATRIX_SIDE;
// ---------------------------------------------------------------------------
// ECC helpers
// ---------------------------------------------------------------------------
@@ -131,18 +147,25 @@ fn version_from_u8(v: u8) -> Result<Version, JsValue> {
#[wasm_bindgen]
pub struct QrRenderer {
/// Fixed-capacity RGBA buffer one lifetime allocation.
buf: Vec<u8>,
rgba_buf: Vec<u8>,
/// Fixed-capacity raw QR module buffer. Values are 0=light, 1=dark.
matrix_buf: Vec<u8>,
/// Side module count written by the latest `render_matrix()` call.
matrix_size: u32,
}
#[wasm_bindgen]
impl QrRenderer {
/// Allocate the renderer and its fixed buffer once.
/// No further heap allocation occurs inside `render()`.
/// Allocate the renderer and its fixed buffers once.
#[wasm_bindgen(constructor)]
pub fn new() -> QrRenderer {
// Pre-fill with white (255) so any untouched pixels are white.
QrRenderer {
buf: vec![0xffu8; MAX_BUF],
rgba_buf: vec![0xffu8; MAX_BUF],
matrix_buf: vec![0u8; MAX_MATRIX_BUF],
matrix_size: 0,
}
}
@@ -159,7 +182,13 @@ impl QrRenderer {
/// `[buf_ptr .. buf_ptr + sidePx*sidePx*4)`.
///
/// Throws a `JsValue` error string on failure.
pub fn render(&mut self, data: &[u8], version: u8, ecc: u8, scale: u8) -> Result<u32, JsValue> {
pub fn render_rgba(
&mut self,
data: &[u8],
version: u8,
ecc: u8,
scale: u8,
) -> Result<u32, JsValue> {
if scale == 0 || scale > 8 {
return Err(JsValue::from_str(&format!("Scale must be 1-8, got {scale}")));
}
@@ -179,7 +208,7 @@ impl QrRenderer {
let side_px = side_mods * scale_u;
let required = (side_px as usize) * (side_px as usize) * 4;
if required > self.buf.len() {
if required > self.rgba_buf.len() {
return Err(JsValue::from_str(&format!(
"Buffer too small: need {required} bytes for V{version} scale {scale}"
)));
@@ -202,26 +231,91 @@ impl QrRenderer {
let v = if is_dark { 0u8 } else { 0xffu8 };
let idx = ((py * side_px + px) * 4) as usize;
self.buf[idx] = v; // R
self.buf[idx + 1] = v; // G
self.buf[idx + 2] = v; // B
self.buf[idx + 3] = 0xff; // A
self.rgba_buf[idx] = v; // R
self.rgba_buf[idx + 1] = v; // G
self.rgba_buf[idx + 2] = v; // B
self.rgba_buf[idx + 3] = 0xff; // A
}
}
Ok(side_px)
}
/// Backward-compatible alias for the previous wrapper API.
/// This is exactly `render_rgba()`: it outputs RGBA pixels into the fixed
/// RGBA buffer and returns the side pixel count.
pub fn render(&mut self, data: &[u8], version: u8, ecc: u8, scale: u8) -> Result<u32, JsValue> {
self.render_rgba(data, version, ecc, scale)
}
/// Generate a QR code in-place and write its raw module matrix to the
/// fixed matrix buffer.
///
/// Matrix values are one byte per module: 0 = light, 1 = dark. The matrix
/// does not include quiet-zone modules. The valid byte region is
/// `[matrix_ptr .. matrix_ptr + sideMods*sideMods)`.
pub fn render_matrix(&mut self, data: &[u8], version: u8, ecc: u8) -> Result<u32, JsValue> {
let ecl = ecc_from_u8(ecc)?;
let ver = version_from_u8(version)?;
let qr = QRBuilder::new(data)
.version(ver)
.ecl(ecl)
.build()
.map_err(|e| JsValue::from_str(&format!("fast_qr build error: {e:?}")))?;
let size = qr.size;
let required = size * size;
if required > self.matrix_buf.len() {
return Err(JsValue::from_str(&format!(
"Matrix buffer too small: need {required} bytes for V{version}"
)));
}
for idx in 0..required {
self.matrix_buf[idx] = if qr.data[idx].value() { 1 } else { 0 };
}
self.matrix_size = size as u32;
Ok(self.matrix_size)
}
/// Raw pointer to the start of the RGBA buffer inside WASM linear memory.
/// Valid for the lifetime of this `QrRenderer` instance.
pub fn buf_ptr(&self) -> u32 {
self.buf.as_ptr() as u32
self.rgba_ptr()
}
/// Total capacity of the buffer in bytes (not the valid region use
/// `sidePx * sidePx * 4` after `render()` to get the valid byte count).
pub fn buf_len(&self) -> u32 {
self.buf.len() as u32
self.rgba_len()
}
/// Explicit raw pointer to the start of the RGBA buffer.
pub fn rgba_ptr(&self) -> u32 {
self.rgba_buf.as_ptr() as u32
}
/// Total capacity of the RGBA buffer in bytes.
pub fn rgba_len(&self) -> u32 {
self.rgba_buf.len() as u32
}
/// Raw pointer to the start of the 0/1 module matrix buffer.
pub fn matrix_ptr(&self) -> u32 {
self.matrix_buf.as_ptr() as u32
}
/// Total capacity of the matrix buffer in bytes.
pub fn matrix_len(&self) -> u32 {
self.matrix_buf.len() as u32
}
/// Side module count from the latest `render_matrix()` call.
pub fn last_matrix_size(&self) -> u32 {
self.matrix_size
}
}
"""
@@ -383,4 +477,4 @@ def run_shell(command: str) -> None:
if __name__ == "__main__":
main()
main()
+1 -1
View File
@@ -1,5 +1,5 @@
{
"builtAt": "2026-07-07T22:19:12.577608+00:00",
"builtAt": "2026-07-08T01:50:04.220654+00:00",
"fast_qr": "0.13",
"outputs": [
"package.json",
+44 -3
View File
@@ -15,10 +15,36 @@ export class QrRenderer {
*/
buf_ptr(): number;
/**
* Allocate the renderer and its fixed buffer once.
* No further heap allocation occurs inside `render()`.
* Side module count from the latest `render_matrix()` call.
*/
last_matrix_size(): number;
/**
* Total capacity of the matrix buffer in bytes.
*/
matrix_len(): number;
/**
* Raw pointer to the start of the 0/1 module matrix buffer.
*/
matrix_ptr(): number;
/**
* Allocate the renderer and its fixed buffers once.
*/
constructor();
/**
* Backward-compatible alias for the previous wrapper API.
* This is exactly `render_rgba()`: it outputs RGBA pixels into the fixed
* RGBA buffer and returns the side pixel count.
*/
render(data: Uint8Array, version: number, ecc: number, scale: number): number;
/**
* Generate a QR code in-place and write its raw module matrix to the
* fixed matrix buffer.
*
* Matrix values are one byte per module: 0 = light, 1 = dark. The matrix
* does not include quiet-zone modules. The valid byte region is
* `[matrix_ptr .. matrix_ptr + sideMods*sideMods)`.
*/
render_matrix(data: Uint8Array, version: number, ecc: number): number;
/**
* Generate a QR code in-place and write RGBA pixels to the fixed buffer.
*
@@ -34,7 +60,15 @@ export class QrRenderer {
*
* Throws a `JsValue` error string on failure.
*/
render(data: Uint8Array, version: number, ecc: number, scale: number): number;
render_rgba(data: Uint8Array, version: number, ecc: number, scale: number): number;
/**
* Total capacity of the RGBA buffer in bytes.
*/
rgba_len(): number;
/**
* Explicit raw pointer to the start of the RGBA buffer.
*/
rgba_ptr(): number;
}
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
@@ -44,8 +78,15 @@ export interface InitOutput {
readonly __wbg_qrrenderer_free: (a: number, b: number) => void;
readonly qrrenderer_buf_len: (a: number) => number;
readonly qrrenderer_buf_ptr: (a: number) => number;
readonly qrrenderer_last_matrix_size: (a: number) => number;
readonly qrrenderer_matrix_len: (a: number) => number;
readonly qrrenderer_matrix_ptr: (a: number) => number;
readonly qrrenderer_new: () => number;
readonly qrrenderer_render: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
readonly qrrenderer_render_matrix: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
readonly qrrenderer_render_rgba: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
readonly qrrenderer_rgba_len: (a: number) => number;
readonly qrrenderer_rgba_ptr: (a: number) => number;
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
readonly __wbindgen_export: (a: number, b: number) => number;
}
+110 -15
View File
@@ -30,8 +30,31 @@ export class QrRenderer {
return ret >>> 0;
}
/**
* Allocate the renderer and its fixed buffer once.
* No further heap allocation occurs inside `render()`.
* Side module count from the latest `render_matrix()` call.
* @returns {number}
*/
last_matrix_size() {
const ret = wasm.qrrenderer_last_matrix_size(this.__wbg_ptr);
return ret >>> 0;
}
/**
* Total capacity of the matrix buffer in bytes.
* @returns {number}
*/
matrix_len() {
const ret = wasm.qrrenderer_matrix_len(this.__wbg_ptr);
return ret >>> 0;
}
/**
* Raw pointer to the start of the 0/1 module matrix buffer.
* @returns {number}
*/
matrix_ptr() {
const ret = wasm.qrrenderer_matrix_ptr(this.__wbg_ptr);
return ret >>> 0;
}
/**
* Allocate the renderer and its fixed buffers once.
*/
constructor() {
const ret = wasm.qrrenderer_new();
@@ -40,19 +63,9 @@ export class QrRenderer {
return this;
}
/**
* Generate a QR code in-place and write RGBA pixels to the fixed buffer.
*
* # Parameters
* - `data` raw packet bytes to encode
* - `version` QR version 1-40
* - `ecc` error correction level (0=L, 1=M, 2=Q, 3=H)
* - `scale` pixels per module (1-8)
*
* # Returns
* Side pixel count (`sidePx`). The valid pixel region is
* `[buf_ptr .. buf_ptr + sidePx*sidePx*4)`.
*
* Throws a `JsValue` error string on failure.
* Backward-compatible alias for the previous wrapper API.
* This is exactly `render_rgba()`: it outputs RGBA pixels into the fixed
* RGBA buffer and returns the side pixel count.
* @param {Uint8Array} data
* @param {number} version
* @param {number} ecc
@@ -76,6 +89,88 @@ export class QrRenderer {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* Generate a QR code in-place and write its raw module matrix to the
* fixed matrix buffer.
*
* Matrix values are one byte per module: 0 = light, 1 = dark. The matrix
* does not include quiet-zone modules. The valid byte region is
* `[matrix_ptr .. matrix_ptr + sideMods*sideMods)`.
* @param {Uint8Array} data
* @param {number} version
* @param {number} ecc
* @returns {number}
*/
render_matrix(data, version, ecc) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_export);
const len0 = WASM_VECTOR_LEN;
wasm.qrrenderer_render_matrix(retptr, this.__wbg_ptr, ptr0, len0, version, ecc);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
return r0 >>> 0;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* Generate a QR code in-place and write RGBA pixels to the fixed buffer.
*
* # Parameters
* - `data` raw packet bytes to encode
* - `version` QR version 1-40
* - `ecc` error correction level (0=L, 1=M, 2=Q, 3=H)
* - `scale` pixels per module (1-8)
*
* # Returns
* Side pixel count (`sidePx`). The valid pixel region is
* `[buf_ptr .. buf_ptr + sidePx*sidePx*4)`.
*
* Throws a `JsValue` error string on failure.
* @param {Uint8Array} data
* @param {number} version
* @param {number} ecc
* @param {number} scale
* @returns {number}
*/
render_rgba(data, version, ecc, scale) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_export);
const len0 = WASM_VECTOR_LEN;
wasm.qrrenderer_render_rgba(retptr, this.__wbg_ptr, ptr0, len0, version, ecc, scale);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
return r0 >>> 0;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* Total capacity of the RGBA buffer in bytes.
* @returns {number}
*/
rgba_len() {
const ret = wasm.qrrenderer_rgba_len(this.__wbg_ptr);
return ret >>> 0;
}
/**
* Explicit raw pointer to the start of the RGBA buffer.
* @returns {number}
*/
rgba_ptr() {
const ret = wasm.qrrenderer_rgba_ptr(this.__wbg_ptr);
return ret >>> 0;
}
}
if (Symbol.dispose) QrRenderer.prototype[Symbol.dispose] = QrRenderer.prototype.free;
function __wbg_get_imports() {
Binary file not shown.
@@ -4,7 +4,14 @@ export const memory: WebAssembly.Memory;
export const __wbg_qrrenderer_free: (a: number, b: number) => void;
export const qrrenderer_buf_len: (a: number) => number;
export const qrrenderer_buf_ptr: (a: number) => number;
export const qrrenderer_last_matrix_size: (a: number) => number;
export const qrrenderer_matrix_len: (a: number) => number;
export const qrrenderer_matrix_ptr: (a: number) => number;
export const qrrenderer_new: () => number;
export const qrrenderer_render: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
export const qrrenderer_render_matrix: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
export const qrrenderer_render_rgba: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
export const qrrenderer_rgba_len: (a: number) => number;
export const qrrenderer_rgba_ptr: (a: number) => number;
export const __wbindgen_add_to_stack_pointer: (a: number) => number;
export const __wbindgen_export: (a: number, b: number) => number;
+11 -4
View File
@@ -190,7 +190,11 @@ describe('CLI Encoder Pipeline', () => {
it('should produce the same frames as the web app (reuse common logic)', async () => {
const { packetize } = await import('@/core/sender/packetizer');
const { scheduleFrames } = await import('@/core/sender/scheduler');
const { COMPATIBLE_QR_ENCODER, encodeQRCodeMatrix } = await import('@/core/qr/qr_encoder');
const {
DEFAULT_CLI_QR_ENCODER,
encodeQRCodeMatrix,
isFastQrNodeAvailable,
} = await import('@/core/qr/qr_encoder_node');
const { QR_VERSION, ECC_LEVEL } = await import('@/core/protocol/constants');
const { parseHeader } = await import('@/core/protocol/packet');
@@ -199,8 +203,11 @@ describe('CLI Encoder Pipeline', () => {
const ordered = scheduleFrames(result.packets, result.totalGenerations);
const genIndices = ordered.map((pkt) => parseHeader(pkt).generationIndex);
expect(DEFAULT_CLI_QR_ENCODER).toBe('fast-qr-wasm');
expect(isFastQrNodeAvailable()).toBe(true);
for (const pkt of ordered) {
const matrix = await encodeQRCodeMatrix(pkt, QR_VERSION, ECC_LEVEL, COMPATIBLE_QR_ENCODER);
const matrix = await encodeQRCodeMatrix(pkt, QR_VERSION, ECC_LEVEL);
expect(matrix.length).toBe(57);
expect(matrix[0]!.length).toBe(57);
}
@@ -236,7 +243,7 @@ describe('CLI Frame Cycle', () => {
it('should generate valid QR matrix for every scheduled frame', async () => {
const { packetize } = await import('@/core/sender/packetizer');
const { scheduleFrames } = await import('@/core/sender/scheduler');
const { COMPATIBLE_QR_ENCODER, encodeQRCodeMatrix } = await import('@/core/qr/qr_encoder');
const { encodeQRCodeMatrix } = await import('@/core/qr/qr_encoder_node');
const { QR_VERSION, ECC_LEVEL } = await import('@/core/protocol/constants');
const data = new TextEncoder().encode('Every frame QR test — medium payload');
@@ -244,7 +251,7 @@ describe('CLI Frame Cycle', () => {
const ordered = scheduleFrames(result.packets, result.totalGenerations);
for (const pkt of ordered) {
const matrix = await encodeQRCodeMatrix(pkt, QR_VERSION, ECC_LEVEL, COMPATIBLE_QR_ENCODER);
const matrix = await encodeQRCodeMatrix(pkt, QR_VERSION, ECC_LEVEL);
expect(matrix.length).toBe(57);
expect(matrix[0]!.length).toBe(57);
const hasDark = matrix.some((row) => row.some((cell) => cell));
+8 -5
View File
@@ -4,8 +4,7 @@
import { describe, it, expect } from 'vitest';
import { packetize } from '@/core/sender/packetizer';
import { scheduleFrames } from '@/core/sender/scheduler';
import { generateQRMatrix } from '@/core/qr/qr_encode';
import { rasterizeQR } from '@/core/qr/frame_raster';
import { renderQRCodeImageData } from '@/core/qr/qr_encoder_browser';
import { decodeQRFromCanvas } from '@/core/qr/qr_decode';
import { parsePacket } from '@/core/protocol/packet';
import { QR_VERSION, ECC_LEVEL } from '@/core/protocol/constants';
@@ -22,9 +21,13 @@ describe('Frame Decode', () => {
for (let i = 0; i < frames.length; i++) {
const originalPacket = frames[i]!;
// Encode as QR
const matrix = generateQRMatrix(originalPacket, QR_VERSION, ECC_LEVEL);
const imageData = rasterizeQR(matrix, 4);
const imageData = await renderQRCodeImageData(
originalPacket,
QR_VERSION,
ECC_LEVEL,
4,
'fast-qr-wasm',
);
// Decode QR
const decodedQR = await decodeQRFromCanvas(imageData);
+3 -5
View File
@@ -4,8 +4,7 @@
import { describe, it, expect } from 'vitest';
import { packetize } from '@/core/sender/packetizer';
import { scheduleFrames } from '@/core/sender/scheduler';
import { generateQRMatrix } from '@/core/qr/qr_encode';
import { rasterizeQR } from '@/core/qr/frame_raster';
import { renderQRCodeImageData } from '@/core/qr/qr_encoder_browser';
import { createQRGif } from '@/core/gif/gif_render';
import { parseGif, renderGifFrame } from '@/core/gif/gif_parser';
import { decodeQRFromCanvas } from '@/core/qr/qr_decode';
@@ -20,13 +19,12 @@ describe('GIF Roundtrip', () => {
const result = packetize(data, false, false);
const frames = scheduleFrames(result.packets, result.totalGenerations);
// Generate QR matrices and rasterize
// Generate QR image frames
const imageFrames: Uint8Array[] = [];
let width = 0;
let height = 0;
for (const frame of frames) {
const matrix = generateQRMatrix(frame, QR_VERSION, ECC_LEVEL);
const imageData = rasterizeQR(matrix, 4);
const imageData = await renderQRCodeImageData(frame, QR_VERSION, ECC_LEVEL, 4, 'fast-qr-wasm');
if (width === 0) {
width = imageData.width;
height = imageData.height;
+2 -4
View File
@@ -4,8 +4,7 @@
import { describe, it, expect } from 'vitest';
import { packetize } from '@/core/sender/packetizer';
import { scheduleFrames } from '@/core/sender/scheduler';
import { generateQRMatrix } from '@/core/qr/qr_encode';
import { rasterizeQR } from '@/core/qr/frame_raster';
import { renderQRCodeImageData } from '@/core/qr/qr_encoder_browser';
import { createQRGif } from '@/core/gif/gif_render';
import { parseGif, renderGifFrame } from '@/core/gif/gif_parser';
import { decodeQRFromCanvas } from '@/core/qr/qr_decode';
@@ -35,8 +34,7 @@ describe('Production Roundtrip', () => {
let width = 0;
let height = 0;
for (const frame of frames) {
const matrix = generateQRMatrix(frame, QR_VERSION, ECC_LEVEL);
const imageData = rasterizeQR(matrix, 4);
const imageData = await renderQRCodeImageData(frame, QR_VERSION, ECC_LEVEL, 4, 'fast-qr-wasm');
if (width === 0) {
width = imageData.width;
height = imageData.height;
+68 -20
View File
@@ -1,8 +1,6 @@
import {
generateQRMatrix,
getMaxByteCapacity,
getMaxZXingWriterByteCapacity,
getMinVersion,
} from '../core/qr/qr_encode.ts';
import { rasterizeQR, rasterizeToGrayscale, getRasterDimensions } from '../core/qr/frame_raster.ts';
import { decodeQRFromBuffer, decodeQRCodesFromCanvas } from '../core/qr/qr_decode.ts';
@@ -22,6 +20,31 @@ import { renderQRCodeImageDataWithZXing } from '../core/qr/qr_write_wasm.ts';
import { createQRGif, estimateGifSize } from '../core/gif/gif_render.ts';
import { describe, it, expect } from 'vitest';
function matrixFromModuleBytes(modules: Uint8Array, sideModules: number): boolean[][] {
const matrix: boolean[][] = [];
for (let row = 0; row < sideModules; row++) {
const outRow: boolean[] = [];
const rowOffset = row * sideModules;
for (let col = 0; col < sideModules; col++) {
outRow.push(modules[rowOffset + col] === 1);
}
matrix.push(outRow);
}
return matrix;
}
function makeTestMatrix(size = 21): boolean[][] {
return Array.from({ length: size }, (_, row) =>
Array.from({ length: size }, (_, col) =>
row === col || row === Math.floor(size / 2) || col === Math.floor(size / 2),
),
);
}
describe('QR encode', () => {
it('should compute capacities for profile versions', () => {
expect(getMaxByteCapacity(31, 'Q')).toBeGreaterThan(1000);
@@ -36,8 +59,6 @@ describe('QR encode', () => {
expect(getMaxZXingWriterByteCapacity(20, 'L')).toBe(856);
expect(getQRTransferProfile('v20-l').maxPacketSize).toBe(858);
expect(getQRTransferProfile('v20-l').maxPayloadSize).toBe(846);
expect(createQRTransferProfile(20, 'L', 'js-qrcode').maxPacketSize).toBe(858);
expect(createQRTransferProfile(20, 'L', 'js-qrcode').maxPayloadSize).toBe(846);
expect(createQRTransferProfile(20, 'L', 'fast-qr-wasm').maxPacketSize).toBe(858);
expect(createQRTransferProfile(20, 'L', 'fast-qr-wasm').maxPayloadSize).toBe(846);
expect(createQRTransferProfile(20, 'L', 'zxing-wasm').maxPacketSize).toBe(856);
@@ -54,18 +75,20 @@ describe('QR encode', () => {
expect(low.version).toBe(medium.version);
});
it('should throw on data too large', () => {
expect(() => generateQRMatrix(new Uint8Array(100), 1, 'L')).toThrow();
it('should throw on data too large', async () => {
await expect(
encodeQRCodeMatrix(new Uint8Array(100), 1, 'L', 'fast-qr-wasm'),
).rejects.toThrow();
});
it('should generate a matrix', () => {
it('should generate a matrix', async () => {
const data = new Uint8Array([72, 101, 108, 108, 111]); // 'Hello'
const matrix = generateQRMatrix(data, 1, 'L');
const matrix = await encodeQRCodeMatrix(data, 1, 'L', 'fast-qr-wasm');
expect(matrix.length).toBe(21);
expect(matrix[0]!.length).toBe(21);
});
it('should expose an encoder abstraction for JS QR, ZXing WASM, and fast_qr WASM', async () => {
it('should expose an encoder abstraction for ZXing WASM and fast_qr WASM', async () => {
const payload = new TextEncoder().encode('encoder facade');
expect(DEFAULT_QR_ENCODER).toBe('fast-qr-wasm');
@@ -87,16 +110,40 @@ describe('QR encode', () => {
expect(isFastQrAvailable()).toBe(true);
const renderer = new QrRenderer();
const sidePx = renderer.render(new TextEncoder().encode('fast qr buffer'), 10, 0, 2);
const payload = new TextEncoder().encode('fast qr buffer');
const sidePx = renderer.render_rgba(payload, 10, 0, 2);
const byteLength = sidePx * sidePx * 4;
const memory = getFastQrWasmMemory();
const view = new Uint8ClampedArray(memory.buffer, renderer.buf_ptr(), byteLength);
const view = new Uint8ClampedArray(memory.buffer, renderer.rgba_ptr(), byteLength);
expect(sidePx).toBe((10 * 4 + 17 + 8) * 2);
expect(renderer.buf_len()).toBeGreaterThanOrEqual(byteLength);
expect(renderer.rgba_len()).toBeGreaterThanOrEqual(byteLength);
expect(renderer.buf_ptr()).toBe(renderer.rgba_ptr());
expect(renderer.buf_len()).toBe(renderer.rgba_len());
expect(renderer.render(payload, 10, 0, 2)).toBe(sidePx);
expect(view.byteLength).toBe(byteLength);
});
it('should expose fast_qr WASM matrix output through the browser encoder facade', async () => {
await ensureFastQrWasm();
const payload = new TextEncoder().encode('fast qr matrix');
const renderer = new QrRenderer();
const sideModules = renderer.render_matrix(payload, 10, 0);
const byteLength = sideModules * sideModules;
const memory = getFastQrWasmMemory();
const modules = new Uint8Array(memory.buffer, renderer.matrix_ptr(), byteLength);
const expectedMatrix = matrixFromModuleBytes(modules.slice(), sideModules);
const matrix = await encodeQRCodeMatrix(payload, 10, 'L', 'fast-qr-wasm');
expect(sideModules).toBe(10 * 4 + 17);
expect(renderer.last_matrix_size()).toBe(sideModules);
expect(renderer.matrix_len()).toBeGreaterThanOrEqual(byteLength);
expect(modules.some((value) => value === 1)).toBe(true);
expect(modules.every((value) => value === 0 || value === 1)).toBe(true);
expect(matrix).toEqual(expectedMatrix);
});
it('should round-trip binary payloads through fast_qr WASM', async () => {
const payload = new Uint8Array([0, 1, 2, 3, 4, 5, 31, 127, 128, 200, 255]);
const image = await renderQRCodeImageData(payload, 10, 'L', 4, 'fast-qr-wasm');
@@ -133,14 +180,14 @@ describe('QR encode', () => {
describe('Frame raster', () => {
it('should produce correct dimensions', () => {
const matrix = generateQRMatrix(new Uint8Array([1,2,3]), 1, 'L');
const matrix = makeTestMatrix();
const dims = getRasterDimensions(matrix.length, 3);
expect(dims.width).toBe((21 + 8) * 3); // 87
expect(dims.height).toBe(87);
});
it('should have quiet zone white', () => {
const matrix = generateQRMatrix(new Uint8Array([1,2,3]), 1, 'L');
const matrix = makeTestMatrix();
const rgba = rasterizeQR(matrix, 3);
expect(rgba.data[0]).toBe(255); // corner pixel should be white
expect(rgba.data[1]).toBe(255);
@@ -150,7 +197,7 @@ describe('Frame raster', () => {
it('should round-trip through decode', async () => {
const original = 'Hello QR';
const data = new TextEncoder().encode(original);
const matrix = generateQRMatrix(data, 1, 'L');
const matrix = await encodeQRCodeMatrix(data, 1, 'L', 'fast-qr-wasm');
const gray = rasterizeToGrayscale(matrix, 3);
const decoded = await decodeQRFromBuffer(gray.data, gray.width, gray.height);
expect(decoded).not.toBeNull();
@@ -160,10 +207,11 @@ describe('Frame raster', () => {
it('should decode multiple QR codes from one image', async () => {
const payloads = ['left QR', 'right QR'];
const images = payloads.map((text) => {
const matrix = generateQRMatrix(new TextEncoder().encode(text), 1, 'L');
return rasterizeQR(matrix, 4);
});
const images = await Promise.all(
payloads.map((text) =>
renderQRCodeImageData(new TextEncoder().encode(text), 1, 'L', 4, 'fast-qr-wasm'),
),
);
const tileWidth = images[0]!.width;
const tileHeight = images[0]!.height;
const width = tileWidth * images.length;
@@ -222,7 +270,7 @@ describe('Frame raster', () => {
describe('GIF render', () => {
it('should produce valid GIF', () => {
const matrix = generateQRMatrix(new Uint8Array([1,2,3]), 1, 'L');
const matrix = makeTestMatrix();
const rgba = rasterizeQR(matrix, 3);
const gif = createQRGif([rgba.data], 100, rgba.width, rgba.height);
expect(gif[0]).toBe(0x47); // G
+11 -16
View File
@@ -14,14 +14,13 @@ import {
isFastQrAvailable,
getFastQrWasmMemory,
QrRenderer,
fastQrUnavailableMessage,
} from '@/core/qr/fast_qr_wasm';
import {
normalizeQREncoder,
renderQRCodeImageData,
type QREncoder,
} from '@/core/qr/qr_encoder_browser';
import { generateQRMatrix } from '@/core/qr/qr_encode';
import { rasterizeQR } from '@/core/qr/frame_raster';
import type { EccLevel } from '@/core/qr/qr_encode';
export interface RenderRequest {
@@ -57,12 +56,14 @@ const ECC_TO_NUM: Record<EccLevel, number> = {
let renderer: QrRenderer | null = null;
void ensureFastQrWasm()
const rendererReady = ensureFastQrWasm()
.then(() => {
renderer = new QrRenderer();
return renderer;
})
.catch(() => {
renderer = null;
return null;
});
self.onmessage = (e: MessageEvent<RenderRequest>) => {
@@ -82,13 +83,15 @@ async function renderPacket(msg: RenderRequest): Promise<void> {
let height: number;
if (shouldUseFastQrWasm(qrEncoder)) {
if (isFastQrAvailable() && renderer !== null) {
const activeRenderer = renderer ?? await rendererReady;
if (isFastQrAvailable() && activeRenderer !== null) {
const eccNum = ECC_TO_NUM[msg.ecc];
const sidePx = renderer.render(packet, msg.version, eccNum, msg.scale);
const sidePx = activeRenderer.render_rgba(packet, msg.version, eccNum, msg.scale);
const byteLen = sidePx * sidePx * 4;
const memory = getFastQrWasmMemory();
const ptr = renderer.buf_ptr();
const ptr = activeRenderer.rgba_ptr();
const view = new Uint8ClampedArray(memory.buffer, ptr, byteLen);
const copy = new Uint8ClampedArray(byteLen);
@@ -98,15 +101,7 @@ async function renderPacket(msg: RenderRequest): Promise<void> {
width = sidePx;
height = sidePx;
} else {
const matrix = generateQRMatrix(packet, msg.version, msg.ecc);
const imageData = rasterizeQR(matrix, msg.scale);
buffer = imageData.data.buffer.slice(
imageData.data.byteOffset,
imageData.data.byteOffset + imageData.data.byteLength,
) as ArrayBuffer;
width = imageData.width;
height = imageData.height;
throw new Error(fastQrUnavailableMessage());
}
} else {
const imageData = await renderQRCodeImageData(
@@ -147,4 +142,4 @@ async function renderPacket(msg: RenderRequest): Promise<void> {
function shouldUseFastQrWasm(qrEncoder: QREncoder): boolean {
const id = String(qrEncoder);
return id === 'fast-qr-wasm' || id === 'fast_qr_wasm' || id === 'fastQrWasm';
}
}