@zymbol/qr 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/types.ts ADDED
@@ -0,0 +1,264 @@
1
+ // Public TypeScript API for @zymbol/qr.
2
+
3
+ export type QrVersion =
4
+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
5
+ | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20
6
+ | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30
7
+ | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40;
8
+
9
+ export type MicroVersion = 'M1' | 'M2' | 'M3' | 'M4';
10
+ export type ErrorCorrection = 'L' | 'M' | 'Q' | 'H';
11
+ export type MicroErrorCorrection = Exclude<ErrorCorrection, 'H'>;
12
+ export type QrMask = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
13
+ export type MicroMask = 0 | 1 | 2 | 3;
14
+
15
+ type VersionSelection<V> =
16
+ | { readonly version?: V; readonly minVersion?: never; readonly maxVersion?: never }
17
+ | { readonly version?: never; readonly minVersion?: V; readonly maxVersion?: V };
18
+
19
+ export type Fnc1 =
20
+ | { readonly position: 'first' }
21
+ | {
22
+ readonly position: 'second';
23
+ /** An integer from 0 to 99, or one ASCII letter. */
24
+ readonly applicationIndicator: number | string;
25
+ };
26
+
27
+ export interface StructuredAppend {
28
+ /** Zero-based symbol index, less than count. */
29
+ readonly index: number;
30
+ /** Number of symbols, from 1 to 16. */
31
+ readonly count: number;
32
+ /** XOR parity of the complete message, from 0 to 255. */
33
+ readonly parity: number;
34
+ }
35
+
36
+ interface QrControls {
37
+ readonly fnc1?: Fnc1;
38
+ readonly structuredAppend?: StructuredAppend;
39
+ }
40
+
41
+ export type QrOptions = VersionSelection<QrVersion> & QrControls & {
42
+ readonly family?: 'qr';
43
+ /** Minimum level when boosting is enabled. Default: M. */
44
+ readonly errorCorrection?: ErrorCorrection;
45
+ readonly boostErrorCorrection?: boolean;
46
+ readonly mask?: QrMask;
47
+ };
48
+
49
+ export type MicroOptions = VersionSelection<MicroVersion> & {
50
+ readonly family: 'micro';
51
+ /** Minimum level when boosting is enabled. Default: L. M1 detects errors only. */
52
+ readonly errorCorrection?: MicroErrorCorrection;
53
+ readonly boostErrorCorrection?: boolean;
54
+ readonly mask?: MicroMask;
55
+ readonly fnc1?: never;
56
+ readonly structuredAppend?: never;
57
+ };
58
+
59
+ export type EncodeOptions = QrOptions | MicroOptions;
60
+ export type Input = string | Uint8Array;
61
+
62
+ export type MicroSegment =
63
+ | { readonly mode: 'numeric'; readonly data: string }
64
+ | { readonly mode: 'alphanumeric'; readonly data: string }
65
+ | { readonly mode: 'byte'; readonly data: Uint8Array }
66
+ | { readonly mode: 'kanji'; readonly data: Uint8Array };
67
+
68
+ export type QrSegment = MicroSegment | {
69
+ readonly mode: 'eci';
70
+ /** ECI assignment number, from 0 to 999999. */
71
+ readonly assignment: number;
72
+ };
73
+
74
+ /** Manual QR segments use the exact version and level, without boosting. */
75
+ export interface QrSegmentOptions extends QrControls {
76
+ readonly family?: 'qr';
77
+ readonly version: QrVersion;
78
+ readonly errorCorrection: ErrorCorrection;
79
+ readonly mask?: QrMask;
80
+ readonly boostErrorCorrection?: never;
81
+ readonly minVersion?: never;
82
+ readonly maxVersion?: never;
83
+ }
84
+
85
+ /** A sampled grid in its correct orientation, without a quiet zone. */
86
+ export interface ModuleGrid {
87
+ readonly size: number;
88
+ /** Exactly size * size row-major values: 0 is light, 1 is dark. */
89
+ readonly modules: Uint8Array;
90
+ }
91
+
92
+ export interface QrSymbol extends ModuleGrid {
93
+ readonly family: 'qr';
94
+ readonly version: QrVersion;
95
+ readonly errorCorrection: ErrorCorrection;
96
+ readonly mask: QrMask;
97
+ /** Caller-owned bytes. Mutation cannot affect the WASM instance. */
98
+ readonly modules: Uint8Array<ArrayBuffer>;
99
+ }
100
+
101
+ export interface MicroSymbol extends ModuleGrid {
102
+ readonly family: 'micro';
103
+ readonly version: MicroVersion;
104
+ readonly errorCorrection: MicroErrorCorrection;
105
+ readonly mask: MicroMask;
106
+ readonly modules: Uint8Array<ArrayBuffer>;
107
+ }
108
+
109
+ export type Symbol = QrSymbol | MicroSymbol;
110
+
111
+ export type EciState =
112
+ | { readonly kind: 'none' }
113
+ | { readonly kind: 'assignment'; readonly assignment: number }
114
+ | { readonly kind: 'multiple' };
115
+
116
+ interface DecodedPayload {
117
+ readonly bytes: Uint8Array<ArrayBuffer>;
118
+ readonly mirrored: boolean;
119
+ readonly reflectanceReversed: boolean;
120
+ readonly errorsCorrected: number;
121
+ }
122
+
123
+ export interface QrDecodeResult extends DecodedPayload {
124
+ readonly family: 'qr';
125
+ readonly version: QrVersion;
126
+ readonly errorCorrection: ErrorCorrection;
127
+ readonly mask: QrMask;
128
+ readonly eci: EciState;
129
+ readonly fnc1: Fnc1 | null;
130
+ readonly structuredAppend: StructuredAppend | null;
131
+ readonly symbologyIdentifier: ']Q1' | ']Q2' | ']Q3' | ']Q4' | ']Q5' | ']Q6';
132
+ }
133
+
134
+ export interface MicroDecodeResult extends DecodedPayload {
135
+ readonly family: 'micro';
136
+ readonly version: MicroVersion;
137
+ readonly errorCorrection: MicroErrorCorrection;
138
+ readonly mask: MicroMask;
139
+ readonly symbologyIdentifier: ']Q1';
140
+ }
141
+
142
+ export type DecodeResult = QrDecodeResult | MicroDecodeResult;
143
+ export type Rgb = readonly [red: number, green: number, blue: number];
144
+
145
+ export interface RenderOptions {
146
+ /** Modules on each edge. Default: 4 for QR, 2 for Micro QR. */
147
+ readonly quietZone?: number;
148
+ /** Integer RGB channels from 0 to 255. Default: black. */
149
+ readonly foreground?: Rgb;
150
+ /** Default: white. Null makes the background transparent. */
151
+ readonly background?: Rgb | null;
152
+ readonly reflectance?: 'normal' | 'reversed';
153
+ }
154
+
155
+ export interface SvgOptions extends RenderOptions {
156
+ /** Intrinsic square size; omitted for a responsive SVG. */
157
+ readonly size?: number;
158
+ }
159
+
160
+ export interface PngOptions extends RenderOptions {
161
+ /** Positive integer pixels per module. Default: 4. */
162
+ readonly scale?: number;
163
+ }
164
+
165
+ export interface RasterOptions extends RenderOptions {
166
+ /** Positive integer pixels per module. Default: 1. */
167
+ readonly scale?: number;
168
+ }
169
+
170
+ export interface Raster {
171
+ readonly width: number;
172
+ readonly height: number;
173
+ /** Caller-owned row-major, non-premultiplied RGBA bytes. */
174
+ readonly data: Uint8ClampedArray<ArrayBuffer>;
175
+ }
176
+
177
+ export interface PngEncodeOptions {
178
+ readonly encode?: EncodeOptions;
179
+ readonly render?: PngOptions;
180
+ }
181
+
182
+ export interface SvgEncodeOptions {
183
+ readonly encode?: EncodeOptions;
184
+ readonly render?: SvgOptions;
185
+ }
186
+
187
+ export interface Limits {
188
+ /** Maximum bytes in a single output. Proposed default: 16 MiB. */
189
+ readonly maxOutputBytes?: number;
190
+ /** Maximum raster/PNG side in pixels. Proposed default: 4096. */
191
+ readonly maxImageSide?: number;
192
+ }
193
+
194
+ export type WasmSource = string | URL | Response | ArrayBuffer | Uint8Array;
195
+
196
+ interface InitBase {
197
+ readonly limits?: Limits;
198
+ }
199
+
200
+ interface ModuleInit extends InitBase {
201
+ /** A WebAssembly.Module. The runtime verifies its brand and ABI. */
202
+ readonly module: object;
203
+ readonly wasm?: never;
204
+ readonly signal?: never;
205
+ }
206
+
207
+ interface SourceInit extends InitBase {
208
+ readonly wasm?: WasmSource;
209
+ readonly module?: never;
210
+ /** Cancels loading, not compilation or synchronous operations. */
211
+ readonly signal?: AbortSignal;
212
+ }
213
+
214
+ export type InitOptions = SourceInit | ModuleInit;
215
+ export type CoreInitOptions = (SourceInit & { readonly wasm: WasmSource }) | ModuleInit;
216
+
217
+ export type ZymbolErrorCode =
218
+ | 'INVALID_INPUT'
219
+ | 'INVALID_OPTIONS'
220
+ | 'DATA_TOO_LONG'
221
+ | 'INVALID_SYMBOL'
222
+ | 'DECODE_FAILED'
223
+ | 'OUTPUT_LIMIT'
224
+ | 'OUT_OF_MEMORY'
225
+ | 'WASM_LOAD_FAILED'
226
+ | 'WASM_ABI_MISMATCH'
227
+ | 'WASM_TRAP'
228
+ | 'INSTANCE_UNUSABLE'
229
+ | 'INTERNAL_ERROR';
230
+
231
+ export type Operation =
232
+ | 'initialize'
233
+ | 'encode'
234
+ | 'encodeSegments'
235
+ | 'decode'
236
+ | 'renderSvg'
237
+ | 'renderPng'
238
+ | 'renderRaster'
239
+ | 'svg'
240
+ | 'png'
241
+ | 'structuredAppendParity';
242
+
243
+ export interface ZymbolErrorOptions {
244
+ readonly reason?: string;
245
+ readonly cause?: unknown;
246
+ }
247
+
248
+
249
+ export interface Zymbol {
250
+ encode(input: Input, options?: QrOptions): QrSymbol;
251
+ encode(input: Input, options: MicroOptions): MicroSymbol;
252
+ encode(input: Input, options: EncodeOptions): Symbol;
253
+ encodeSegments(segments: readonly QrSegment[], options: QrSegmentOptions): QrSymbol;
254
+ encodeSegments(segments: readonly MicroSegment[], options: MicroOptions): MicroSymbol;
255
+ decode(grid: ModuleGrid): DecodeResult;
256
+ renderSvg(symbol: Symbol, options?: SvgOptions): string;
257
+ renderPng(symbol: Symbol, options?: PngOptions): Uint8Array<ArrayBuffer>;
258
+ renderRaster(symbol: Symbol, options?: RasterOptions): Raster;
259
+ svg(input: Input, options?: SvgEncodeOptions): string;
260
+ png(input: Input, options?: PngEncodeOptions): Uint8Array<ArrayBuffer>;
261
+ structuredAppendParity(bytes: Uint8Array): number;
262
+ }
263
+
264
+
package/src/wasm.ts ADDED
@@ -0,0 +1,115 @@
1
+ import { ZymbolError } from './errors.js';
2
+
3
+ export const ABI_VERSION = 1;
4
+ export type WasmSource = string | URL | Response | ArrayBuffer | Uint8Array;
5
+
6
+ export type WasmInit =
7
+ | { readonly wasm: WasmSource; readonly module?: never; readonly signal?: AbortSignal }
8
+ | { readonly module: object; readonly wasm?: never; readonly signal?: never };
9
+
10
+ export interface WasmExports {
11
+ readonly memory: WebAssembly.Memory;
12
+ readonly zymbol_abi_version: () => number;
13
+ }
14
+
15
+ const WASM_MAGIC = [0, 0x61, 0x73, 0x6d];
16
+
17
+ function loadError(reason: string, cause?: unknown): ZymbolError {
18
+ return new ZymbolError('WASM_LOAD_FAILED', 'initialize', `Unable to load Zymbol WebAssembly: ${reason}`, { cause, reason });
19
+ }
20
+
21
+ function isByteBuffer(value: unknown): value is Uint8Array | ArrayBuffer {
22
+ return value instanceof Uint8Array || value instanceof ArrayBuffer;
23
+ }
24
+
25
+ function isAbsoluteHttpUrl(value: string): boolean {
26
+ try {
27
+ const url = new URL(value);
28
+ return url.protocol === 'https:' || url.protocol === 'http:';
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ function usableSource(value: unknown): value is WasmSource {
35
+ return typeof value === 'string' || value instanceof URL || value instanceof Response || isByteBuffer(value);
36
+ }
37
+
38
+ async function compileResponse(response: Response): Promise<WebAssembly.Module> {
39
+ if (!response.ok) {
40
+ throw loadError(`HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ''}`);
41
+ }
42
+ const mediaType = response.headers.get('Content-Type')?.split(';', 1)[0]?.trim().toLowerCase();
43
+ if (mediaType === 'application/wasm' && typeof WebAssembly.compileStreaming === 'function') {
44
+ return WebAssembly.compileStreaming(response);
45
+ }
46
+ return WebAssembly.compile(await response.arrayBuffer());
47
+ }
48
+
49
+ async function compileSource(wasm: WasmSource, signal?: AbortSignal): Promise<WebAssembly.Module> {
50
+ if (isByteBuffer(wasm)) {
51
+ // Snapshot caller input so compilation does not observe later mutations.
52
+ const bytes = new Uint8Array(wasm instanceof ArrayBuffer ? wasm.slice(0) : wasm.slice());
53
+ if (bytes.length < 8 || WASM_MAGIC.some((byte, index) => bytes[index] !== byte)) {
54
+ throw loadError('invalid WASM header');
55
+ }
56
+ return WebAssembly.compile(bytes);
57
+ }
58
+ if (wasm instanceof Response) return compileResponse(wasm);
59
+ if (typeof wasm === 'string' && !isAbsoluteHttpUrl(wasm)) {
60
+ throw loadError('a WASM string location must be an absolute HTTP(S) URL');
61
+ }
62
+ const url = typeof wasm === 'string' ? new URL(wasm) : wasm;
63
+ if (!['http:', 'https:'].includes(url.protocol)) {
64
+ throw loadError('only HTTP(S) URLs are supported by the portable loader');
65
+ }
66
+ return compileResponse(await fetch(url, signal ? { signal } : {}));
67
+ }
68
+
69
+ /** Compile a WASM source. Never caches explicit caller-owned sources. */
70
+ export async function compileWasm(options: WasmInit): Promise<WebAssembly.Module> {
71
+ if (!options || typeof options !== 'object') throw loadError('source options are required');
72
+ const hasModule = Object.hasOwn(options, 'module');
73
+ const hasWasm = Object.hasOwn(options, 'wasm');
74
+ if (hasModule === hasWasm) throw loadError('provide exactly one WASM source or compiled module');
75
+ if (hasModule) {
76
+ if (!(options.module instanceof WebAssembly.Module)) {
77
+ throw loadError('module must be a WebAssembly.Module');
78
+ }
79
+ return options.module;
80
+ }
81
+ if (!usableSource(options.wasm)) throw loadError('unsupported WASM source');
82
+ try {
83
+ return await compileSource(options.wasm, options.signal);
84
+ } catch (error) {
85
+ if (error instanceof ZymbolError) throw error;
86
+ throw loadError('compilation failed', error);
87
+ }
88
+ }
89
+
90
+ /** Instantiate a compatible import-free module, isolating memory for every caller. */
91
+ export async function instantiateWasm(module: WebAssembly.Module): Promise<WasmExports> {
92
+ if (WebAssembly.Module.imports(module).length !== 0) {
93
+ throw new ZymbolError('WASM_ABI_MISMATCH', 'initialize', 'WebAssembly module requires host imports');
94
+ }
95
+ let instance: WebAssembly.Instance;
96
+ try {
97
+ instance = await WebAssembly.instantiate(module, {});
98
+ } catch (cause) {
99
+ throw loadError('instantiation failed', cause);
100
+ }
101
+ const exports = instance.exports;
102
+ if (!(exports.memory instanceof WebAssembly.Memory) || typeof exports.zymbol_abi_version !== 'function') {
103
+ throw new ZymbolError('WASM_ABI_MISMATCH', 'initialize', 'Missing WebAssembly memory or ABI version export');
104
+ }
105
+ let version: number;
106
+ try {
107
+ version = (exports.zymbol_abi_version as () => number)();
108
+ } catch (cause) {
109
+ throw new ZymbolError('WASM_TRAP', 'initialize', 'WASM ABI probe failed', { cause });
110
+ }
111
+ if (version !== ABI_VERSION) {
112
+ throw new ZymbolError('WASM_ABI_MISMATCH', 'initialize', `Unsupported WebAssembly ABI version ${version}; expected ${ABI_VERSION}`);
113
+ }
114
+ return exports as unknown as WasmExports;
115
+ }