qrcode-transmitter 1.0.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [1.0.0] - 2026-03-03
11
+
12
+ ### Added
13
+
14
+ - Added `encodeBytesToQRCodes` to split `Uint8Array` payloads into protocol frames and generate QR SVG output
15
+ - Added `startVideoQRReceiver` to scan QR frames from camera input and reconstruct complete data automatically
16
+ - Added chunk protocol and aggregation logic (frame parsing, deduplication, and out-of-order reassembly)
17
+ - Added example app and tests covering protocol and main flow
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # qrcode-transmitter
2
+
3
+ [![npm downloads](https://img.shields.io/npm/dm/qrcode-transmitter?logo=npm)](https://www.npmjs.com/package/qrcode-transmitter)
4
+
5
+ A lightweight browser-oriented library for splitting arbitrary binary data into multiple QR frames and reassembling it on scan.
6
+
7
+ ## Features
8
+
9
+ - Splits `Uint8Array` payloads into protocol frames and encodes each frame as QR SVG
10
+ - Scans QR codes from camera input and reassembles payloads automatically
11
+ - Handles duplicate frames and out-of-order frame delivery (`msgId + frameIndex` aggregation)
12
+ - Includes a runnable example app for local testing and demos
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pnpm add qrcode-transmitter
18
+ ```
19
+
20
+ You can also install it with `npm` or `yarn`.
21
+
22
+ ## Quick Start
23
+
24
+ ```ts
25
+ import { encodeBytesToQRCodes, startVideoQRReceiver } from "qrcode-transmitter";
26
+
27
+ // 1) Sender: encode data into multiple QR frames
28
+ const bytes = new TextEncoder().encode("Hello QR");
29
+ const frames = encodeBytesToQRCodes(bytes);
30
+ // frames[i].svg can be injected into the DOM directly
31
+
32
+ // 2) Receiver: scan with a video element and reassemble
33
+ const video = document.querySelector("video") as HTMLVideoElement;
34
+ const receiver = startVideoQRReceiver(video, {
35
+ onFrame: (progress) => {
36
+ console.log(progress.frameIndex + 1, "/", progress.totalFrames);
37
+ },
38
+ onComplete: (data) => {
39
+ console.log(new TextDecoder().decode(data));
40
+ },
41
+ });
42
+
43
+ // Stop scanning when needed
44
+ receiver.stop();
45
+ ```
46
+
47
+ ## API
48
+
49
+ ### `encodeBytesToQRCodes(bytes: Uint8Array): EncodedFrame[]`
50
+
51
+ Encodes a raw byte array and returns QR frame objects:
52
+
53
+ - `frameIndex`: frame index (starting from 0)
54
+ - `totalFrames`: total number of frames
55
+ - `svg`: QR code SVG string for this frame
56
+ - `payload`: base64 payload of this frame
57
+
58
+ Encoding settings are fixed to:
59
+
60
+ - QR `TypeNumber = 15`
61
+ - error correction level `L`
62
+ - `Byte` mode for writing payload data
63
+
64
+ ### `startVideoQRReceiver(video, options): { stop(): void }`
65
+
66
+ Starts scanning and reassembles data by protocol:
67
+
68
+ - `video`: `HTMLVideoElement`
69
+ - `options.onFrame`: called when each new (non-duplicate) frame is parsed
70
+ - `options.onComplete`: called when all frames are received, with the complete `Uint8Array`
71
+
72
+ Returns an object with `stop()` to stop and destroy the scanner.
73
+
74
+ ## Protocol
75
+
76
+ Frame text format:
77
+
78
+ `msgId|idx/total|payloadBase64`
79
+
80
+ - `msgId`: 8-character random hexadecimal string
81
+ - `idx`: current chunk index
82
+ - `total`: total chunk count
83
+ - `payloadBase64`: base64 payload of the chunk
84
+
85
+ ## Local Development
86
+
87
+ ```bash
88
+ pnpm install
89
+ pnpm build
90
+ pnpm test
91
+ pnpm example
92
+ ```
93
+
94
+ Common scripts:
95
+
96
+ - `pnpm build`: build to `dist/` with `tsup`
97
+ - `pnpm dev`: watch build
98
+ - `pnpm typecheck`: run TypeScript type checks
99
+ - `pnpm test`: run Vitest
100
+ - `pnpm example`: start the example app (Vite)
101
+
102
+ ## Run the Example
103
+
104
+ After running `pnpm example`:
105
+
106
+ - In the "Encode" section, enter text or select a file to render QR frames at 5fps
107
+ - In the "Decode" section, allow camera permission and scan to see reconstructed output
@@ -0,0 +1,36 @@
1
+ interface EncodedFrame {
2
+ frameIndex: number;
3
+ totalFrames: number;
4
+ svg: string;
5
+ payload: string;
6
+ }
7
+ /**
8
+ * Encode byte array to QR codes with TypeNumber=15 and L error correction
9
+ * @param bytes Raw byte array
10
+ * @returns Array of encoded results with frame indices
11
+ */
12
+ declare function encodeBytesToQRCodes(bytes: Uint8Array): EncodedFrame[];
13
+ interface FrameProgress {
14
+ frameIndex: number;
15
+ totalFrames: number;
16
+ msgId: string;
17
+ receivedCount: number;
18
+ }
19
+ interface VideoQRReceiverOptions {
20
+ /** Fired when each frame is parsed */
21
+ onFrame?: (progress: FrameProgress) => void;
22
+ /** Fired when the complete message is received */
23
+ onComplete?: (data: Uint8Array) => void;
24
+ }
25
+ interface VideoQRReceiver {
26
+ stop(): void;
27
+ }
28
+ /**
29
+ * Start video stream QR code parsing, automatically reassemble complete message per protocol
30
+ * @param video Video element for camera rendering
31
+ * @param options Callbacks and configuration
32
+ * @returns Returns { stop } to stop scanning
33
+ */
34
+ declare function startVideoQRReceiver(video: HTMLVideoElement, options: VideoQRReceiverOptions): VideoQRReceiver;
35
+
36
+ export { type EncodedFrame, type FrameProgress, type VideoQRReceiver, type VideoQRReceiverOptions, encodeBytesToQRCodes, startVideoQRReceiver };
@@ -0,0 +1,36 @@
1
+ interface EncodedFrame {
2
+ frameIndex: number;
3
+ totalFrames: number;
4
+ svg: string;
5
+ payload: string;
6
+ }
7
+ /**
8
+ * Encode byte array to QR codes with TypeNumber=15 and L error correction
9
+ * @param bytes Raw byte array
10
+ * @returns Array of encoded results with frame indices
11
+ */
12
+ declare function encodeBytesToQRCodes(bytes: Uint8Array): EncodedFrame[];
13
+ interface FrameProgress {
14
+ frameIndex: number;
15
+ totalFrames: number;
16
+ msgId: string;
17
+ receivedCount: number;
18
+ }
19
+ interface VideoQRReceiverOptions {
20
+ /** Fired when each frame is parsed */
21
+ onFrame?: (progress: FrameProgress) => void;
22
+ /** Fired when the complete message is received */
23
+ onComplete?: (data: Uint8Array) => void;
24
+ }
25
+ interface VideoQRReceiver {
26
+ stop(): void;
27
+ }
28
+ /**
29
+ * Start video stream QR code parsing, automatically reassemble complete message per protocol
30
+ * @param video Video element for camera rendering
31
+ * @param options Callbacks and configuration
32
+ * @returns Returns { stop } to stop scanning
33
+ */
34
+ declare function startVideoQRReceiver(video: HTMLVideoElement, options: VideoQRReceiverOptions): VideoQRReceiver;
35
+
36
+ export { type EncodedFrame, type FrameProgress, type VideoQRReceiver, type VideoQRReceiverOptions, encodeBytesToQRCodes, startVideoQRReceiver };
package/dist/index.js ADDED
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ encodeBytesToQRCodes: () => encodeBytesToQRCodes,
34
+ startVideoQRReceiver: () => startVideoQRReceiver
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+ var import_qrcode_generator = __toESM(require("qrcode-generator"));
38
+ var import_qr_scanner = __toESM(require("qr-scanner"));
39
+
40
+ // src/protocol.ts
41
+ var TYPE15_BYTE_CAPACITY = 520;
42
+ function randomMsgId() {
43
+ return Math.random().toString(16).slice(2, 10);
44
+ }
45
+ function estimateHeaderLength(total) {
46
+ const idxDigits = String(Math.max(0, total - 1)).length;
47
+ const totalDigits = String(total).length;
48
+ return 8 + 1 + idxDigits + 1 + totalDigits + 1;
49
+ }
50
+ function splitIntoFrames(bytes) {
51
+ if (bytes.length === 0) {
52
+ return [{ frameIndex: 0, totalFrames: 1, frame: encodeFrame(randomMsgId(), 0, 1, ""), payloadBase64: "" }];
53
+ }
54
+ const msgId = randomMsgId();
55
+ const headerOverhead = estimateHeaderLength(1) + 4;
56
+ const maxPayloadChars = TYPE15_BYTE_CAPACITY - headerOverhead;
57
+ const maxPayloadBytes = Math.floor(maxPayloadChars * 3 / 4);
58
+ if (maxPayloadBytes <= 0) {
59
+ throw new Error("Protocol header too large for Type 15 QR capacity");
60
+ }
61
+ const total = Math.ceil(bytes.length / maxPayloadBytes);
62
+ const headerLen = estimateHeaderLength(total) + 4;
63
+ const maxPayloadCharsActual = TYPE15_BYTE_CAPACITY - headerLen;
64
+ const maxPayloadBytesActual = Math.floor(maxPayloadCharsActual * 3 / 4);
65
+ const frames = [];
66
+ let offset = 0;
67
+ while (offset < bytes.length) {
68
+ const chunk = bytes.subarray(offset, Math.min(offset + maxPayloadBytesActual, bytes.length));
69
+ const chunkBase64 = btoa(String.fromCharCode.apply(null, Array.from(chunk)));
70
+ const frame = encodeFrame(msgId, frames.length, total, chunkBase64);
71
+ frames.push({ frameIndex: frames.length, totalFrames: total, frame, payloadBase64: chunkBase64 });
72
+ offset += chunk.length;
73
+ }
74
+ return frames;
75
+ }
76
+ function encodeFrame(msgId, idx, total, payloadBase64) {
77
+ return `${msgId}|${idx}/${total}|${payloadBase64}`;
78
+ }
79
+ function parseFrame(text) {
80
+ const pipe1 = text.indexOf("|");
81
+ if (pipe1 < 0) return null;
82
+ const msgId = text.slice(0, pipe1);
83
+ const rest2 = text.slice(pipe1 + 1);
84
+ const slash = rest2.indexOf("/");
85
+ const pipe2 = rest2.indexOf("|");
86
+ if (slash < 0 || pipe2 < 0 || slash > pipe2) return null;
87
+ const idx = parseInt(rest2.slice(0, slash), 10);
88
+ const total = parseInt(rest2.slice(slash + 1, pipe2), 10);
89
+ const payloadBase64 = rest2.slice(pipe2 + 1);
90
+ if (isNaN(idx) || isNaN(total) || idx < 0 || total < 1 || idx >= total) return null;
91
+ return { msgId, idx, total, payloadBase64 };
92
+ }
93
+ function base64ToBytes(base64) {
94
+ const binary = atob(base64);
95
+ const bytes = new Uint8Array(binary.length);
96
+ for (let i = 0; i < binary.length; i++) {
97
+ bytes[i] = binary.charCodeAt(i);
98
+ }
99
+ return bytes;
100
+ }
101
+ var FrameAggregator = class {
102
+ constructor() {
103
+ this.chunks = /* @__PURE__ */ new Map();
104
+ }
105
+ add(parsed) {
106
+ const { msgId, idx, total, payloadBase64 } = parsed;
107
+ let map = this.chunks.get(msgId);
108
+ if (!map) {
109
+ map = /* @__PURE__ */ new Map();
110
+ this.chunks.set(msgId, map);
111
+ }
112
+ const isNew = !map.has(idx);
113
+ if (isNew) map.set(idx, payloadBase64);
114
+ const receivedCount = map.size;
115
+ if (receivedCount !== total) {
116
+ return {
117
+ isNew,
118
+ frameIndex: idx,
119
+ totalFrames: total,
120
+ msgId,
121
+ receivedCount,
122
+ complete: false
123
+ };
124
+ }
125
+ const parts = [];
126
+ for (let i = 0; i < total; i++) {
127
+ const b64 = map.get(i);
128
+ if (!b64) {
129
+ return { isNew, frameIndex: idx, totalFrames: total, msgId, receivedCount, complete: false };
130
+ }
131
+ parts.push(base64ToBytes(b64));
132
+ }
133
+ this.chunks.delete(msgId);
134
+ const totalLen = parts.reduce((s, p) => s + p.length, 0);
135
+ const data = new Uint8Array(totalLen);
136
+ let off = 0;
137
+ for (const p of parts) {
138
+ data.set(p, off);
139
+ off += p.length;
140
+ }
141
+ return {
142
+ isNew,
143
+ frameIndex: idx,
144
+ totalFrames: total,
145
+ msgId,
146
+ receivedCount,
147
+ complete: true,
148
+ data
149
+ };
150
+ }
151
+ };
152
+
153
+ // src/index.ts
154
+ var TYPE_NUMBER = 15;
155
+ var ERROR_CORRECTION = "L";
156
+ function encodeBytesToQRCodes(bytes) {
157
+ const frames = splitIntoFrames(bytes);
158
+ const result = [];
159
+ for (const { frameIndex, totalFrames, frame, payloadBase64 } of frames) {
160
+ const qr = (0, import_qrcode_generator.default)(TYPE_NUMBER, ERROR_CORRECTION);
161
+ qr.addData(frame, "Byte");
162
+ qr.make();
163
+ result.push({
164
+ frameIndex,
165
+ totalFrames,
166
+ svg: qr.createSvgTag(),
167
+ payload: payloadBase64
168
+ });
169
+ }
170
+ return result;
171
+ }
172
+ function startVideoQRReceiver(video, options) {
173
+ const { onFrame, onComplete } = options;
174
+ const aggregator = new FrameAggregator();
175
+ const qrScanner = new import_qr_scanner.default(
176
+ video,
177
+ (result) => {
178
+ const text = typeof result === "string" ? result : result.data;
179
+ const parsed = parseFrame(text);
180
+ if (!parsed) return;
181
+ const res = aggregator.add(parsed);
182
+ if (res.isNew && onFrame) {
183
+ onFrame({
184
+ frameIndex: res.frameIndex,
185
+ totalFrames: res.totalFrames,
186
+ msgId: res.msgId,
187
+ receivedCount: res.receivedCount
188
+ });
189
+ }
190
+ if (res.complete && res.data && onComplete) {
191
+ onComplete(res.data);
192
+ }
193
+ },
194
+ {
195
+ returnDetailedScanResult: true,
196
+ maxScansPerSecond: 20,
197
+ calculateScanRegion: (v) => {
198
+ const w = v.videoWidth;
199
+ const h = v.videoHeight;
200
+ const target = 1e3;
201
+ const scale = w >= h ? target / w : target / h;
202
+ return {
203
+ x: 0,
204
+ y: 0,
205
+ width: w,
206
+ height: h,
207
+ downScaledWidth: Math.round(w * scale),
208
+ downScaledHeight: Math.round(h * scale)
209
+ };
210
+ }
211
+ }
212
+ );
213
+ qrScanner.start();
214
+ return {
215
+ stop() {
216
+ qrScanner.stop();
217
+ qrScanner.destroy();
218
+ }
219
+ };
220
+ }
221
+ // Annotate the CommonJS export names for ESM import in node:
222
+ 0 && (module.exports = {
223
+ encodeBytesToQRCodes,
224
+ startVideoQRReceiver
225
+ });
226
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/protocol.ts"],"sourcesContent":["import qrcode from \"qrcode-generator\";\nimport QrScanner from \"qr-scanner\";\nimport { splitIntoFrames, parseFrame, FrameAggregator } from \"./protocol\";\n\nconst TYPE_NUMBER = 15;\nconst ERROR_CORRECTION = \"L\";\n\nexport interface EncodedFrame {\n frameIndex: number;\n totalFrames: number;\n svg: string;\n payload: string;\n}\n\n/**\n * Encode byte array to QR codes with TypeNumber=15 and L error correction\n * @param bytes Raw byte array\n * @returns Array of encoded results with frame indices\n */\nexport function encodeBytesToQRCodes(bytes: Uint8Array): EncodedFrame[] {\n const frames = splitIntoFrames(bytes);\n const result: EncodedFrame[] = [];\n\n for (const { frameIndex, totalFrames, frame, payloadBase64 } of frames) {\n const qr = qrcode(TYPE_NUMBER, ERROR_CORRECTION);\n qr.addData(frame, \"Byte\");\n qr.make();\n result.push({\n frameIndex,\n totalFrames,\n svg: qr.createSvgTag(),\n payload: payloadBase64,\n });\n }\n\n return result;\n}\n\nexport interface FrameProgress {\n frameIndex: number;\n totalFrames: number;\n msgId: string;\n receivedCount: number;\n}\n\nexport interface VideoQRReceiverOptions {\n /** Fired when each frame is parsed */\n onFrame?: (progress: FrameProgress) => void;\n /** Fired when the complete message is received */\n onComplete?: (data: Uint8Array) => void;\n}\n\nexport interface VideoQRReceiver {\n stop(): void;\n}\n\n/**\n * Start video stream QR code parsing, automatically reassemble complete message per protocol\n * @param video Video element for camera rendering\n * @param options Callbacks and configuration\n * @returns Returns { stop } to stop scanning\n */\nexport function startVideoQRReceiver(\n video: HTMLVideoElement,\n options: VideoQRReceiverOptions\n): VideoQRReceiver {\n const { onFrame, onComplete } = options;\n const aggregator = new FrameAggregator();\n\n const qrScanner = new QrScanner(\n video,\n (result) => {\n const text = typeof result === \"string\" ? result : result.data;\n const parsed = parseFrame(text);\n if (!parsed) return;\n const res = aggregator.add(parsed);\n if (res.isNew && onFrame) {\n onFrame({\n frameIndex: res.frameIndex,\n totalFrames: res.totalFrames,\n msgId: res.msgId,\n receivedCount: res.receivedCount,\n });\n }\n if (res.complete && res.data && onComplete) {\n onComplete(res.data);\n }\n },\n {\n returnDetailedScanResult: true,\n maxScansPerSecond: 20,\n calculateScanRegion: (v: HTMLVideoElement) => {\n const w = v.videoWidth;\n const h = v.videoHeight;\n const target = 1000;\n const scale = w >= h ? target / w : target / h;\n return {\n x: 0,\n y: 0,\n width: w,\n height: h,\n downScaledWidth: Math.round(w * scale),\n downScaledHeight: Math.round(h * scale),\n };\n },\n }\n );\n\n qrScanner.start();\n\n return {\n stop() {\n qrScanner.stop();\n qrScanner.destroy();\n },\n };\n}\n","/**\n * Chunking protocol\n * Frame format: msgId|idx/total|payloadBase64\n */\n\nconst TYPE15_BYTE_CAPACITY = 520; // TypeNumber 15, L error correction\n\nfunction randomMsgId(): string {\n return Math.random().toString(16).slice(2, 10);\n}\n\nfunction estimateHeaderLength(total: number): number {\n const idxDigits = String(Math.max(0, total - 1)).length;\n const totalDigits = String(total).length;\n return 8 + 1 + idxDigits + 1 + totalDigits + 1;\n}\n\nexport interface FrameInfo {\n frameIndex: number;\n totalFrames: number;\n frame: string;\n payloadBase64: string;\n}\n\n/**\n * Split byte array into chunks, each encoded as a protocol frame\n */\nexport function splitIntoFrames(bytes: Uint8Array): FrameInfo[] {\n if (bytes.length === 0) {\n return [{ frameIndex: 0, totalFrames: 1, frame: encodeFrame(randomMsgId(), 0, 1, \"\"), payloadBase64: \"\" }];\n }\n\n const msgId = randomMsgId();\n const headerOverhead = estimateHeaderLength(1) + 4;\n const maxPayloadChars = TYPE15_BYTE_CAPACITY - headerOverhead;\n const maxPayloadBytes = Math.floor((maxPayloadChars * 3) / 4);\n\n if (maxPayloadBytes <= 0) {\n throw new Error(\"Protocol header too large for Type 15 QR capacity\");\n }\n\n const total = Math.ceil(bytes.length / maxPayloadBytes);\n const headerLen = estimateHeaderLength(total) + 4;\n const maxPayloadCharsActual = TYPE15_BYTE_CAPACITY - headerLen;\n const maxPayloadBytesActual = Math.floor((maxPayloadCharsActual * 3) / 4);\n\n const frames: FrameInfo[] = [];\n let offset = 0;\n\n while (offset < bytes.length) {\n const chunk = bytes.subarray(offset, Math.min(offset + maxPayloadBytesActual, bytes.length));\n const chunkBase64 = btoa(String.fromCharCode.apply(null, Array.from(chunk)));\n const frame = encodeFrame(msgId, frames.length, total, chunkBase64);\n frames.push({ frameIndex: frames.length, totalFrames: total, frame, payloadBase64: chunkBase64 });\n offset += chunk.length;\n }\n\n return frames;\n}\n\nfunction encodeFrame(msgId: string, idx: number, total: number, payloadBase64: string): string {\n return `${msgId}|${idx}/${total}|${payloadBase64}`;\n}\n\nexport interface ParsedFrame {\n msgId: string;\n idx: number;\n total: number;\n payloadBase64: string;\n}\n\n/**\n * Parse protocol frame, returns null for invalid format\n */\nexport function parseFrame(text: string): ParsedFrame | null {\n const pipe1 = text.indexOf(\"|\");\n if (pipe1 < 0) return null;\n const msgId = text.slice(0, pipe1);\n const rest2 = text.slice(pipe1 + 1);\n const slash = rest2.indexOf(\"/\");\n const pipe2 = rest2.indexOf(\"|\");\n if (slash < 0 || pipe2 < 0 || slash > pipe2) return null;\n const idx = parseInt(rest2.slice(0, slash), 10);\n const total = parseInt(rest2.slice(slash + 1, pipe2), 10);\n const payloadBase64 = rest2.slice(pipe2 + 1);\n if (isNaN(idx) || isNaN(total) || idx < 0 || total < 1 || idx >= total) return null;\n return { msgId, idx, total, payloadBase64 };\n}\n\n/**\n * Decode base64 to Uint8Array\n */\nexport function base64ToBytes(base64: string): Uint8Array {\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n\nexport interface AddFrameResult {\n isNew: boolean;\n frameIndex: number;\n totalFrames: number;\n msgId: string;\n receivedCount: number;\n complete: boolean;\n data?: Uint8Array;\n}\n\n/**\n * Message aggregation state: collect frames by msgId, reassemble when complete\n */\nexport class FrameAggregator {\n private chunks = new Map<string, Map<number, string>>();\n\n add(parsed: ParsedFrame): AddFrameResult {\n const { msgId, idx, total, payloadBase64 } = parsed;\n let map = this.chunks.get(msgId);\n if (!map) {\n map = new Map();\n this.chunks.set(msgId, map);\n }\n const isNew = !map.has(idx);\n if (isNew) map.set(idx, payloadBase64);\n const receivedCount = map.size;\n\n if (receivedCount !== total) {\n return {\n isNew,\n frameIndex: idx,\n totalFrames: total,\n msgId,\n receivedCount,\n complete: false,\n };\n }\n\n const parts: Uint8Array[] = [];\n for (let i = 0; i < total; i++) {\n const b64 = map.get(i);\n if (!b64) {\n return { isNew, frameIndex: idx, totalFrames: total, msgId, receivedCount, complete: false };\n }\n parts.push(base64ToBytes(b64));\n }\n this.chunks.delete(msgId);\n const totalLen = parts.reduce((s, p) => s + p.length, 0);\n const data = new Uint8Array(totalLen);\n let off = 0;\n for (const p of parts) {\n data.set(p, off);\n off += p.length;\n }\n return {\n isNew,\n frameIndex: idx,\n totalFrames: total,\n msgId,\n receivedCount,\n complete: true,\n data,\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,8BAAmB;AACnB,wBAAsB;;;ACItB,IAAM,uBAAuB;AAE7B,SAAS,cAAsB;AAC7B,SAAO,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE;AAC/C;AAEA,SAAS,qBAAqB,OAAuB;AACnD,QAAM,YAAY,OAAO,KAAK,IAAI,GAAG,QAAQ,CAAC,CAAC,EAAE;AACjD,QAAM,cAAc,OAAO,KAAK,EAAE;AAClC,SAAO,IAAI,IAAI,YAAY,IAAI,cAAc;AAC/C;AAYO,SAAS,gBAAgB,OAAgC;AAC9D,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO,CAAC,EAAE,YAAY,GAAG,aAAa,GAAG,OAAO,YAAY,YAAY,GAAG,GAAG,GAAG,EAAE,GAAG,eAAe,GAAG,CAAC;AAAA,EAC3G;AAEA,QAAM,QAAQ,YAAY;AAC1B,QAAM,iBAAiB,qBAAqB,CAAC,IAAI;AACjD,QAAM,kBAAkB,uBAAuB;AAC/C,QAAM,kBAAkB,KAAK,MAAO,kBAAkB,IAAK,CAAC;AAE5D,MAAI,mBAAmB,GAAG;AACxB,UAAM,IAAI,MAAM,mDAAmD;AAAA,EACrE;AAEA,QAAM,QAAQ,KAAK,KAAK,MAAM,SAAS,eAAe;AACtD,QAAM,YAAY,qBAAqB,KAAK,IAAI;AAChD,QAAM,wBAAwB,uBAAuB;AACrD,QAAM,wBAAwB,KAAK,MAAO,wBAAwB,IAAK,CAAC;AAExE,QAAM,SAAsB,CAAC;AAC7B,MAAI,SAAS;AAEb,SAAO,SAAS,MAAM,QAAQ;AAC5B,UAAM,QAAQ,MAAM,SAAS,QAAQ,KAAK,IAAI,SAAS,uBAAuB,MAAM,MAAM,CAAC;AAC3F,UAAM,cAAc,KAAK,OAAO,aAAa,MAAM,MAAM,MAAM,KAAK,KAAK,CAAC,CAAC;AAC3E,UAAM,QAAQ,YAAY,OAAO,OAAO,QAAQ,OAAO,WAAW;AAClE,WAAO,KAAK,EAAE,YAAY,OAAO,QAAQ,aAAa,OAAO,OAAO,eAAe,YAAY,CAAC;AAChG,cAAU,MAAM;AAAA,EAClB;AAEA,SAAO;AACT;AAEA,SAAS,YAAY,OAAe,KAAa,OAAe,eAA+B;AAC7F,SAAO,GAAG,KAAK,IAAI,GAAG,IAAI,KAAK,IAAI,aAAa;AAClD;AAYO,SAAS,WAAW,MAAkC;AAC3D,QAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,MAAI,QAAQ,EAAG,QAAO;AACtB,QAAM,QAAQ,KAAK,MAAM,GAAG,KAAK;AACjC,QAAM,QAAQ,KAAK,MAAM,QAAQ,CAAC;AAClC,QAAM,QAAQ,MAAM,QAAQ,GAAG;AAC/B,QAAM,QAAQ,MAAM,QAAQ,GAAG;AAC/B,MAAI,QAAQ,KAAK,QAAQ,KAAK,QAAQ,MAAO,QAAO;AACpD,QAAM,MAAM,SAAS,MAAM,MAAM,GAAG,KAAK,GAAG,EAAE;AAC9C,QAAM,QAAQ,SAAS,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,EAAE;AACxD,QAAM,gBAAgB,MAAM,MAAM,QAAQ,CAAC;AAC3C,MAAI,MAAM,GAAG,KAAK,MAAM,KAAK,KAAK,MAAM,KAAK,QAAQ,KAAK,OAAO,MAAO,QAAO;AAC/E,SAAO,EAAE,OAAO,KAAK,OAAO,cAAc;AAC5C;AAKO,SAAS,cAAc,QAA4B;AACxD,QAAM,SAAS,KAAK,MAAM;AAC1B,QAAM,QAAQ,IAAI,WAAW,OAAO,MAAM;AAC1C,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,UAAM,CAAC,IAAI,OAAO,WAAW,CAAC;AAAA,EAChC;AACA,SAAO;AACT;AAeO,IAAM,kBAAN,MAAsB;AAAA,EAAtB;AACL,SAAQ,SAAS,oBAAI,IAAiC;AAAA;AAAA,EAEtD,IAAI,QAAqC;AACvC,UAAM,EAAE,OAAO,KAAK,OAAO,cAAc,IAAI;AAC7C,QAAI,MAAM,KAAK,OAAO,IAAI,KAAK;AAC/B,QAAI,CAAC,KAAK;AACR,YAAM,oBAAI,IAAI;AACd,WAAK,OAAO,IAAI,OAAO,GAAG;AAAA,IAC5B;AACA,UAAM,QAAQ,CAAC,IAAI,IAAI,GAAG;AAC1B,QAAI,MAAO,KAAI,IAAI,KAAK,aAAa;AACrC,UAAM,gBAAgB,IAAI;AAE1B,QAAI,kBAAkB,OAAO;AAC3B,aAAO;AAAA,QACL;AAAA,QACA,YAAY;AAAA,QACZ,aAAa;AAAA,QACb;AAAA,QACA;AAAA,QACA,UAAU;AAAA,MACZ;AAAA,IACF;AAEA,UAAM,QAAsB,CAAC;AAC7B,aAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,YAAM,MAAM,IAAI,IAAI,CAAC;AACrB,UAAI,CAAC,KAAK;AACR,eAAO,EAAE,OAAO,YAAY,KAAK,aAAa,OAAO,OAAO,eAAe,UAAU,MAAM;AAAA,MAC7F;AACA,YAAM,KAAK,cAAc,GAAG,CAAC;AAAA,IAC/B;AACA,SAAK,OAAO,OAAO,KAAK;AACxB,UAAM,WAAW,MAAM,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,QAAQ,CAAC;AACvD,UAAM,OAAO,IAAI,WAAW,QAAQ;AACpC,QAAI,MAAM;AACV,eAAW,KAAK,OAAO;AACrB,WAAK,IAAI,GAAG,GAAG;AACf,aAAO,EAAE;AAAA,IACX;AACA,WAAO;AAAA,MACL;AAAA,MACA,YAAY;AAAA,MACZ,aAAa;AAAA,MACb;AAAA,MACA;AAAA,MACA,UAAU;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF;;;ADjKA,IAAM,cAAc;AACpB,IAAM,mBAAmB;AAclB,SAAS,qBAAqB,OAAmC;AACtE,QAAM,SAAS,gBAAgB,KAAK;AACpC,QAAM,SAAyB,CAAC;AAEhC,aAAW,EAAE,YAAY,aAAa,OAAO,cAAc,KAAK,QAAQ;AACtE,UAAM,SAAK,wBAAAA,SAAO,aAAa,gBAAgB;AAC/C,OAAG,QAAQ,OAAO,MAAM;AACxB,OAAG,KAAK;AACR,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,MACA,KAAK,GAAG,aAAa;AAAA,MACrB,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AA0BO,SAAS,qBACd,OACA,SACiB;AACjB,QAAM,EAAE,SAAS,WAAW,IAAI;AAChC,QAAM,aAAa,IAAI,gBAAgB;AAEvC,QAAM,YAAY,IAAI,kBAAAC;AAAA,IACpB;AAAA,IACA,CAAC,WAAW;AACV,YAAM,OAAO,OAAO,WAAW,WAAW,SAAS,OAAO;AAC1D,YAAM,SAAS,WAAW,IAAI;AAC9B,UAAI,CAAC,OAAQ;AACb,YAAM,MAAM,WAAW,IAAI,MAAM;AACjC,UAAI,IAAI,SAAS,SAAS;AACxB,gBAAQ;AAAA,UACN,YAAY,IAAI;AAAA,UAChB,aAAa,IAAI;AAAA,UACjB,OAAO,IAAI;AAAA,UACX,eAAe,IAAI;AAAA,QACrB,CAAC;AAAA,MACH;AACA,UAAI,IAAI,YAAY,IAAI,QAAQ,YAAY;AAC1C,mBAAW,IAAI,IAAI;AAAA,MACrB;AAAA,IACF;AAAA,IACA;AAAA,MACE,0BAA0B;AAAA,MAC1B,mBAAmB;AAAA,MACnB,qBAAqB,CAAC,MAAwB;AAC5C,cAAM,IAAI,EAAE;AACZ,cAAM,IAAI,EAAE;AACZ,cAAM,SAAS;AACf,cAAM,QAAQ,KAAK,IAAI,SAAS,IAAI,SAAS;AAC7C,eAAO;AAAA,UACL,GAAG;AAAA,UACH,GAAG;AAAA,UACH,OAAO;AAAA,UACP,QAAQ;AAAA,UACR,iBAAiB,KAAK,MAAM,IAAI,KAAK;AAAA,UACrC,kBAAkB,KAAK,MAAM,IAAI,KAAK;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,YAAU,MAAM;AAEhB,SAAO;AAAA,IACL,OAAO;AACL,gBAAU,KAAK;AACf,gBAAU,QAAQ;AAAA,IACpB;AAAA,EACF;AACF;","names":["qrcode","QrScanner"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,190 @@
1
+ // src/index.ts
2
+ import qrcode from "qrcode-generator";
3
+ import QrScanner from "qr-scanner";
4
+
5
+ // src/protocol.ts
6
+ var TYPE15_BYTE_CAPACITY = 520;
7
+ function randomMsgId() {
8
+ return Math.random().toString(16).slice(2, 10);
9
+ }
10
+ function estimateHeaderLength(total) {
11
+ const idxDigits = String(Math.max(0, total - 1)).length;
12
+ const totalDigits = String(total).length;
13
+ return 8 + 1 + idxDigits + 1 + totalDigits + 1;
14
+ }
15
+ function splitIntoFrames(bytes) {
16
+ if (bytes.length === 0) {
17
+ return [{ frameIndex: 0, totalFrames: 1, frame: encodeFrame(randomMsgId(), 0, 1, ""), payloadBase64: "" }];
18
+ }
19
+ const msgId = randomMsgId();
20
+ const headerOverhead = estimateHeaderLength(1) + 4;
21
+ const maxPayloadChars = TYPE15_BYTE_CAPACITY - headerOverhead;
22
+ const maxPayloadBytes = Math.floor(maxPayloadChars * 3 / 4);
23
+ if (maxPayloadBytes <= 0) {
24
+ throw new Error("Protocol header too large for Type 15 QR capacity");
25
+ }
26
+ const total = Math.ceil(bytes.length / maxPayloadBytes);
27
+ const headerLen = estimateHeaderLength(total) + 4;
28
+ const maxPayloadCharsActual = TYPE15_BYTE_CAPACITY - headerLen;
29
+ const maxPayloadBytesActual = Math.floor(maxPayloadCharsActual * 3 / 4);
30
+ const frames = [];
31
+ let offset = 0;
32
+ while (offset < bytes.length) {
33
+ const chunk = bytes.subarray(offset, Math.min(offset + maxPayloadBytesActual, bytes.length));
34
+ const chunkBase64 = btoa(String.fromCharCode.apply(null, Array.from(chunk)));
35
+ const frame = encodeFrame(msgId, frames.length, total, chunkBase64);
36
+ frames.push({ frameIndex: frames.length, totalFrames: total, frame, payloadBase64: chunkBase64 });
37
+ offset += chunk.length;
38
+ }
39
+ return frames;
40
+ }
41
+ function encodeFrame(msgId, idx, total, payloadBase64) {
42
+ return `${msgId}|${idx}/${total}|${payloadBase64}`;
43
+ }
44
+ function parseFrame(text) {
45
+ const pipe1 = text.indexOf("|");
46
+ if (pipe1 < 0) return null;
47
+ const msgId = text.slice(0, pipe1);
48
+ const rest2 = text.slice(pipe1 + 1);
49
+ const slash = rest2.indexOf("/");
50
+ const pipe2 = rest2.indexOf("|");
51
+ if (slash < 0 || pipe2 < 0 || slash > pipe2) return null;
52
+ const idx = parseInt(rest2.slice(0, slash), 10);
53
+ const total = parseInt(rest2.slice(slash + 1, pipe2), 10);
54
+ const payloadBase64 = rest2.slice(pipe2 + 1);
55
+ if (isNaN(idx) || isNaN(total) || idx < 0 || total < 1 || idx >= total) return null;
56
+ return { msgId, idx, total, payloadBase64 };
57
+ }
58
+ function base64ToBytes(base64) {
59
+ const binary = atob(base64);
60
+ const bytes = new Uint8Array(binary.length);
61
+ for (let i = 0; i < binary.length; i++) {
62
+ bytes[i] = binary.charCodeAt(i);
63
+ }
64
+ return bytes;
65
+ }
66
+ var FrameAggregator = class {
67
+ constructor() {
68
+ this.chunks = /* @__PURE__ */ new Map();
69
+ }
70
+ add(parsed) {
71
+ const { msgId, idx, total, payloadBase64 } = parsed;
72
+ let map = this.chunks.get(msgId);
73
+ if (!map) {
74
+ map = /* @__PURE__ */ new Map();
75
+ this.chunks.set(msgId, map);
76
+ }
77
+ const isNew = !map.has(idx);
78
+ if (isNew) map.set(idx, payloadBase64);
79
+ const receivedCount = map.size;
80
+ if (receivedCount !== total) {
81
+ return {
82
+ isNew,
83
+ frameIndex: idx,
84
+ totalFrames: total,
85
+ msgId,
86
+ receivedCount,
87
+ complete: false
88
+ };
89
+ }
90
+ const parts = [];
91
+ for (let i = 0; i < total; i++) {
92
+ const b64 = map.get(i);
93
+ if (!b64) {
94
+ return { isNew, frameIndex: idx, totalFrames: total, msgId, receivedCount, complete: false };
95
+ }
96
+ parts.push(base64ToBytes(b64));
97
+ }
98
+ this.chunks.delete(msgId);
99
+ const totalLen = parts.reduce((s, p) => s + p.length, 0);
100
+ const data = new Uint8Array(totalLen);
101
+ let off = 0;
102
+ for (const p of parts) {
103
+ data.set(p, off);
104
+ off += p.length;
105
+ }
106
+ return {
107
+ isNew,
108
+ frameIndex: idx,
109
+ totalFrames: total,
110
+ msgId,
111
+ receivedCount,
112
+ complete: true,
113
+ data
114
+ };
115
+ }
116
+ };
117
+
118
+ // src/index.ts
119
+ var TYPE_NUMBER = 15;
120
+ var ERROR_CORRECTION = "L";
121
+ function encodeBytesToQRCodes(bytes) {
122
+ const frames = splitIntoFrames(bytes);
123
+ const result = [];
124
+ for (const { frameIndex, totalFrames, frame, payloadBase64 } of frames) {
125
+ const qr = qrcode(TYPE_NUMBER, ERROR_CORRECTION);
126
+ qr.addData(frame, "Byte");
127
+ qr.make();
128
+ result.push({
129
+ frameIndex,
130
+ totalFrames,
131
+ svg: qr.createSvgTag(),
132
+ payload: payloadBase64
133
+ });
134
+ }
135
+ return result;
136
+ }
137
+ function startVideoQRReceiver(video, options) {
138
+ const { onFrame, onComplete } = options;
139
+ const aggregator = new FrameAggregator();
140
+ const qrScanner = new QrScanner(
141
+ video,
142
+ (result) => {
143
+ const text = typeof result === "string" ? result : result.data;
144
+ const parsed = parseFrame(text);
145
+ if (!parsed) return;
146
+ const res = aggregator.add(parsed);
147
+ if (res.isNew && onFrame) {
148
+ onFrame({
149
+ frameIndex: res.frameIndex,
150
+ totalFrames: res.totalFrames,
151
+ msgId: res.msgId,
152
+ receivedCount: res.receivedCount
153
+ });
154
+ }
155
+ if (res.complete && res.data && onComplete) {
156
+ onComplete(res.data);
157
+ }
158
+ },
159
+ {
160
+ returnDetailedScanResult: true,
161
+ maxScansPerSecond: 20,
162
+ calculateScanRegion: (v) => {
163
+ const w = v.videoWidth;
164
+ const h = v.videoHeight;
165
+ const target = 1e3;
166
+ const scale = w >= h ? target / w : target / h;
167
+ return {
168
+ x: 0,
169
+ y: 0,
170
+ width: w,
171
+ height: h,
172
+ downScaledWidth: Math.round(w * scale),
173
+ downScaledHeight: Math.round(h * scale)
174
+ };
175
+ }
176
+ }
177
+ );
178
+ qrScanner.start();
179
+ return {
180
+ stop() {
181
+ qrScanner.stop();
182
+ qrScanner.destroy();
183
+ }
184
+ };
185
+ }
186
+ export {
187
+ encodeBytesToQRCodes,
188
+ startVideoQRReceiver
189
+ };
190
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/protocol.ts"],"sourcesContent":["import qrcode from \"qrcode-generator\";\nimport QrScanner from \"qr-scanner\";\nimport { splitIntoFrames, parseFrame, FrameAggregator } from \"./protocol\";\n\nconst TYPE_NUMBER = 15;\nconst ERROR_CORRECTION = \"L\";\n\nexport interface EncodedFrame {\n frameIndex: number;\n totalFrames: number;\n svg: string;\n payload: string;\n}\n\n/**\n * Encode byte array to QR codes with TypeNumber=15 and L error correction\n * @param bytes Raw byte array\n * @returns Array of encoded results with frame indices\n */\nexport function encodeBytesToQRCodes(bytes: Uint8Array): EncodedFrame[] {\n const frames = splitIntoFrames(bytes);\n const result: EncodedFrame[] = [];\n\n for (const { frameIndex, totalFrames, frame, payloadBase64 } of frames) {\n const qr = qrcode(TYPE_NUMBER, ERROR_CORRECTION);\n qr.addData(frame, \"Byte\");\n qr.make();\n result.push({\n frameIndex,\n totalFrames,\n svg: qr.createSvgTag(),\n payload: payloadBase64,\n });\n }\n\n return result;\n}\n\nexport interface FrameProgress {\n frameIndex: number;\n totalFrames: number;\n msgId: string;\n receivedCount: number;\n}\n\nexport interface VideoQRReceiverOptions {\n /** Fired when each frame is parsed */\n onFrame?: (progress: FrameProgress) => void;\n /** Fired when the complete message is received */\n onComplete?: (data: Uint8Array) => void;\n}\n\nexport interface VideoQRReceiver {\n stop(): void;\n}\n\n/**\n * Start video stream QR code parsing, automatically reassemble complete message per protocol\n * @param video Video element for camera rendering\n * @param options Callbacks and configuration\n * @returns Returns { stop } to stop scanning\n */\nexport function startVideoQRReceiver(\n video: HTMLVideoElement,\n options: VideoQRReceiverOptions\n): VideoQRReceiver {\n const { onFrame, onComplete } = options;\n const aggregator = new FrameAggregator();\n\n const qrScanner = new QrScanner(\n video,\n (result) => {\n const text = typeof result === \"string\" ? result : result.data;\n const parsed = parseFrame(text);\n if (!parsed) return;\n const res = aggregator.add(parsed);\n if (res.isNew && onFrame) {\n onFrame({\n frameIndex: res.frameIndex,\n totalFrames: res.totalFrames,\n msgId: res.msgId,\n receivedCount: res.receivedCount,\n });\n }\n if (res.complete && res.data && onComplete) {\n onComplete(res.data);\n }\n },\n {\n returnDetailedScanResult: true,\n maxScansPerSecond: 20,\n calculateScanRegion: (v: HTMLVideoElement) => {\n const w = v.videoWidth;\n const h = v.videoHeight;\n const target = 1000;\n const scale = w >= h ? target / w : target / h;\n return {\n x: 0,\n y: 0,\n width: w,\n height: h,\n downScaledWidth: Math.round(w * scale),\n downScaledHeight: Math.round(h * scale),\n };\n },\n }\n );\n\n qrScanner.start();\n\n return {\n stop() {\n qrScanner.stop();\n qrScanner.destroy();\n },\n };\n}\n","/**\n * Chunking protocol\n * Frame format: msgId|idx/total|payloadBase64\n */\n\nconst TYPE15_BYTE_CAPACITY = 520; // TypeNumber 15, L error correction\n\nfunction randomMsgId(): string {\n return Math.random().toString(16).slice(2, 10);\n}\n\nfunction estimateHeaderLength(total: number): number {\n const idxDigits = String(Math.max(0, total - 1)).length;\n const totalDigits = String(total).length;\n return 8 + 1 + idxDigits + 1 + totalDigits + 1;\n}\n\nexport interface FrameInfo {\n frameIndex: number;\n totalFrames: number;\n frame: string;\n payloadBase64: string;\n}\n\n/**\n * Split byte array into chunks, each encoded as a protocol frame\n */\nexport function splitIntoFrames(bytes: Uint8Array): FrameInfo[] {\n if (bytes.length === 0) {\n return [{ frameIndex: 0, totalFrames: 1, frame: encodeFrame(randomMsgId(), 0, 1, \"\"), payloadBase64: \"\" }];\n }\n\n const msgId = randomMsgId();\n const headerOverhead = estimateHeaderLength(1) + 4;\n const maxPayloadChars = TYPE15_BYTE_CAPACITY - headerOverhead;\n const maxPayloadBytes = Math.floor((maxPayloadChars * 3) / 4);\n\n if (maxPayloadBytes <= 0) {\n throw new Error(\"Protocol header too large for Type 15 QR capacity\");\n }\n\n const total = Math.ceil(bytes.length / maxPayloadBytes);\n const headerLen = estimateHeaderLength(total) + 4;\n const maxPayloadCharsActual = TYPE15_BYTE_CAPACITY - headerLen;\n const maxPayloadBytesActual = Math.floor((maxPayloadCharsActual * 3) / 4);\n\n const frames: FrameInfo[] = [];\n let offset = 0;\n\n while (offset < bytes.length) {\n const chunk = bytes.subarray(offset, Math.min(offset + maxPayloadBytesActual, bytes.length));\n const chunkBase64 = btoa(String.fromCharCode.apply(null, Array.from(chunk)));\n const frame = encodeFrame(msgId, frames.length, total, chunkBase64);\n frames.push({ frameIndex: frames.length, totalFrames: total, frame, payloadBase64: chunkBase64 });\n offset += chunk.length;\n }\n\n return frames;\n}\n\nfunction encodeFrame(msgId: string, idx: number, total: number, payloadBase64: string): string {\n return `${msgId}|${idx}/${total}|${payloadBase64}`;\n}\n\nexport interface ParsedFrame {\n msgId: string;\n idx: number;\n total: number;\n payloadBase64: string;\n}\n\n/**\n * Parse protocol frame, returns null for invalid format\n */\nexport function parseFrame(text: string): ParsedFrame | null {\n const pipe1 = text.indexOf(\"|\");\n if (pipe1 < 0) return null;\n const msgId = text.slice(0, pipe1);\n const rest2 = text.slice(pipe1 + 1);\n const slash = rest2.indexOf(\"/\");\n const pipe2 = rest2.indexOf(\"|\");\n if (slash < 0 || pipe2 < 0 || slash > pipe2) return null;\n const idx = parseInt(rest2.slice(0, slash), 10);\n const total = parseInt(rest2.slice(slash + 1, pipe2), 10);\n const payloadBase64 = rest2.slice(pipe2 + 1);\n if (isNaN(idx) || isNaN(total) || idx < 0 || total < 1 || idx >= total) return null;\n return { msgId, idx, total, payloadBase64 };\n}\n\n/**\n * Decode base64 to Uint8Array\n */\nexport function base64ToBytes(base64: string): Uint8Array {\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n\nexport interface AddFrameResult {\n isNew: boolean;\n frameIndex: number;\n totalFrames: number;\n msgId: string;\n receivedCount: number;\n complete: boolean;\n data?: Uint8Array;\n}\n\n/**\n * Message aggregation state: collect frames by msgId, reassemble when complete\n */\nexport class FrameAggregator {\n private chunks = new Map<string, Map<number, string>>();\n\n add(parsed: ParsedFrame): AddFrameResult {\n const { msgId, idx, total, payloadBase64 } = parsed;\n let map = this.chunks.get(msgId);\n if (!map) {\n map = new Map();\n this.chunks.set(msgId, map);\n }\n const isNew = !map.has(idx);\n if (isNew) map.set(idx, payloadBase64);\n const receivedCount = map.size;\n\n if (receivedCount !== total) {\n return {\n isNew,\n frameIndex: idx,\n totalFrames: total,\n msgId,\n receivedCount,\n complete: false,\n };\n }\n\n const parts: Uint8Array[] = [];\n for (let i = 0; i < total; i++) {\n const b64 = map.get(i);\n if (!b64) {\n return { isNew, frameIndex: idx, totalFrames: total, msgId, receivedCount, complete: false };\n }\n parts.push(base64ToBytes(b64));\n }\n this.chunks.delete(msgId);\n const totalLen = parts.reduce((s, p) => s + p.length, 0);\n const data = new Uint8Array(totalLen);\n let off = 0;\n for (const p of parts) {\n data.set(p, off);\n off += p.length;\n }\n return {\n isNew,\n frameIndex: idx,\n totalFrames: total,\n msgId,\n receivedCount,\n complete: true,\n data,\n };\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "qrcode-transmitter",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight browser library for transmitting binary data across multi-frame QR codes.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "CHANGELOG.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "dev": "tsup --watch",
22
+ "typecheck": "tsc --noEmit",
23
+ "test": "vitest",
24
+ "test:run": "vitest run",
25
+ "example": "vite --config example/vite.config.ts"
26
+ },
27
+ "keywords": [],
28
+ "author": "",
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "qr-scanner": "^1.4.2",
32
+ "qrcode-generator": "^2.0.4"
33
+ },
34
+ "devDependencies": {
35
+ "tsup": "^8.0.0",
36
+ "typescript": "^5.3.0",
37
+ "vite": "^6.0.0",
38
+ "vitest": "^2.0.0"
39
+ }
40
+ }