mirror of
https://github.com/infrost/RaptorQR.git
synced 2026-08-31 23:17:57 +08:00
delete js-qrcode dependencies (no needed anymore)
This commit is contained in:
+1
-2
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
-8
@@ -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
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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,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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user