qr-kit 2.1.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.
@@ -0,0 +1,102 @@
1
+ // utils/poster.js
2
+ // Browser-side image compositing: background image + QR code.
3
+ // No external dependencies.
4
+
5
+ import { svgToPngDataURL, dataURLToImage, loadImage } from './raster.js';
6
+
7
+ // ─── Data primitive ───────────────────────────────────────────────────────────
8
+
9
+ /**
10
+ * Composites a QR code SVG onto a background image and returns the result as a Blob.
11
+ * Use this when you need the image data without triggering a download
12
+ * (upload to server, put in <img> src, test in Node, etc.)
13
+ *
14
+ * @param {object} opts
15
+ * @param {SVGSVGElement} opts.svgEl
16
+ * @param {string} opts.templateSrc
17
+ * @param {{ x: number, y: number, size: number }} [opts.qr]
18
+ * @param {string} [opts.mime='image/jpeg']
19
+ * @param {number} [opts.quality=0.92]
20
+ * @param {number} [opts.scale=1]
21
+ * @param {boolean} [opts.debug=false]
22
+ * @returns {Promise<Blob>}
23
+ */
24
+ export async function buildQrCompositeBlob({
25
+ svgEl,
26
+ templateSrc,
27
+ qr = { x: 50, y: 50, size: 260 },
28
+ mime = 'image/jpeg',
29
+ quality = 0.92,
30
+ scale = 1,
31
+ debug = false,
32
+ signal,
33
+ }) {
34
+ if (!svgEl) throw new Error('buildQrCompositeBlob: svgEl is required');
35
+ if (!templateSrc) throw new Error('buildQrCompositeBlob: templateSrc is required');
36
+ signal?.throwIfAborted?.();
37
+
38
+ const bg = await loadImage(templateSrc, { signal });
39
+ const cw = Math.round(bg.width * scale);
40
+ const ch = Math.round(bg.height * scale);
41
+
42
+ const canvas = document.createElement('canvas');
43
+ canvas.width = cw; canvas.height = ch;
44
+ const ctx = canvas.getContext('2d');
45
+ ctx.imageSmoothingEnabled = false;
46
+ ctx.drawImage(bg, 0, 0, cw, ch);
47
+
48
+ const qrPng = await svgToPngDataURL(svgEl, Math.max(2, Math.ceil(scale * 3)));
49
+ const qrImg = await dataURLToImage(qrPng);
50
+ const qx = Math.round(qr.x * scale);
51
+ const qy = Math.round(qr.y * scale);
52
+ const qs = Math.round(qr.size * scale);
53
+ ctx.drawImage(qrImg, qx, qy, qs, qs);
54
+
55
+ if (debug) {
56
+ ctx.strokeStyle = 'rgba(0,0,0,0.6)';
57
+ ctx.lineWidth = Math.max(1, scale);
58
+ ctx.strokeRect(qx + 0.5, qy + 0.5, qs - 1, qs - 1);
59
+ }
60
+
61
+ return new Promise((res, rej) =>
62
+ canvas.toBlob(b => b ? res(b) : rej(new Error('canvas.toBlob failed')), mime, quality),
63
+ );
64
+ }
65
+
66
+ // ─── Download wrappers ────────────────────────────────────────────────────────
67
+
68
+ /**
69
+ * Composites a QR code SVG onto a background image and triggers a file download.
70
+ *
71
+ * Coordinate system: (0, 0) is the TOP-LEFT corner of the background image.
72
+ * Units are pixels of the background image at scale=1.
73
+ *
74
+ * @param {object} opts
75
+ * @param {SVGSVGElement} opts.svgEl - QR SVG element (ref from QRCodeGenerator).
76
+ * @param {string} opts.templateSrc - URL or imported asset path (JPEG or PNG).
77
+ * @param {{ x: number, y: number, size: number }} [opts.qr]
78
+ * @param {string} [opts.fileName='poster.jpg']
79
+ * @param {string} [opts.mime='image/jpeg']
80
+ * @param {number} [opts.quality=0.92]
81
+ * @param {number} [opts.scale=1]
82
+ * @param {boolean} [opts.debug=false] - Draw a border around the QR area.
83
+ */
84
+ export async function downloadQrComposite({
85
+ svgEl, templateSrc,
86
+ qr = { x: 50, y: 50, size: 260 },
87
+ fileName = 'poster.jpg',
88
+ mime = 'image/jpeg',
89
+ quality = 0.92,
90
+ scale = 1,
91
+ debug = false,
92
+ }) {
93
+ const blob = await buildQrCompositeBlob({ svgEl, templateSrc, qr, mime, quality, scale, debug });
94
+ const url = URL.createObjectURL(blob);
95
+ const a = document.createElement('a');
96
+ a.href = url; a.download = fileName;
97
+ document.body.appendChild(a); a.click(); a.remove();
98
+ URL.revokeObjectURL(url);
99
+ }
100
+
101
+ /** @deprecated Use `downloadQrComposite` instead. */
102
+ export const downloadLeafletImage = downloadQrComposite;
@@ -0,0 +1,140 @@
1
+ // utils/raster.js
2
+ // Browser-side raster helpers: SVG → PNG, PNG → JPEG, image loading.
3
+ // No external dependencies.
4
+
5
+ /**
6
+ * Serialises an SVG element to a PNG data URL at the given pixel scale.
7
+ * @param {SVGSVGElement} svgEl
8
+ * @param {number} [scale=2]
9
+ * @returns {Promise<string>} PNG data URL
10
+ */
11
+ export async function svgToPngDataURL(svgEl, scale = 2) {
12
+ const serializer = new XMLSerializer();
13
+ let svg = serializer.serializeToString(svgEl);
14
+
15
+ // Ensure xmlns attribute is present (required for img.src rendering)
16
+ if (!svg.match(/^<svg[^>]+xmlns=/)) {
17
+ svg = svg.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
18
+ }
19
+
20
+ const vbox = svgEl.viewBox?.baseVal;
21
+ const w = Math.floor((vbox?.width || svgEl.width.baseVal.value) * scale);
22
+ const h = Math.floor((vbox?.height || svgEl.height.baseVal.value) * scale);
23
+
24
+ const img = new Image();
25
+ const dataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
26
+ await new Promise((res, rej) => { img.onload = res; img.onerror = rej; img.src = dataUrl; });
27
+
28
+ const canvas = document.createElement('canvas');
29
+ canvas.width = w;
30
+ canvas.height = h;
31
+
32
+ const ctx = canvas.getContext('2d');
33
+ ctx.imageSmoothingEnabled = false;
34
+ ctx.drawImage(img, 0, 0, w, h);
35
+
36
+ return canvas.toDataURL('image/png');
37
+ }
38
+
39
+ /**
40
+ * Converts a base64 data URL to a Uint8Array of raw bytes.
41
+ * @param {string} dataUrl
42
+ * @returns {Uint8Array}
43
+ */
44
+ export function dataURLToBytes(dataUrl) {
45
+ const b64 = dataUrl.split(',')[1];
46
+ const bin = atob(b64);
47
+ const u8 = new Uint8Array(bin.length);
48
+ for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
49
+ return u8;
50
+ }
51
+
52
+ /**
53
+ * Re-encodes a PNG data URL as JPEG bytes.
54
+ * @param {string} pngDataURL
55
+ * @param {number} [quality=0.92]
56
+ * @returns {Promise<Uint8Array>}
57
+ */
58
+ export async function pngDataURLtoJpegBytes(pngDataURL, quality = 0.92) {
59
+ const img = new Image();
60
+ await new Promise((res, rej) => { img.onload = res; img.onerror = rej; img.src = pngDataURL; });
61
+
62
+ const canvas = document.createElement('canvas');
63
+ canvas.width = img.width;
64
+ canvas.height = img.height;
65
+ canvas.getContext('2d').drawImage(img, 0, 0);
66
+
67
+ const jpg = canvas.toDataURL('image/jpeg', quality);
68
+ return dataURLToBytes(jpg);
69
+ }
70
+
71
+ /**
72
+ * Loads an image via a URL with crossOrigin="anonymous".
73
+ * @param {string} src
74
+ * @returns {Promise<HTMLImageElement>}
75
+ */
76
+ export function loadImage(src, { signal } = {}) {
77
+ return new Promise((resolve, reject) => {
78
+ if (signal?.aborted) { reject(signal.reason ?? new DOMException('Aborted', 'AbortError')); return; }
79
+ const img = new Image();
80
+ img.crossOrigin = 'anonymous';
81
+ img.onload = () => resolve(img);
82
+ img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
83
+ if (signal) {
84
+ signal.addEventListener('abort', () => {
85
+ img.src = '';
86
+ reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
87
+ }, { once: true });
88
+ }
89
+ img.src = src;
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Loads an image from a URL and returns it as JPEG bytes.
95
+ * @param {string} src
96
+ * @param {number} [quality=0.9]
97
+ * @returns {Promise<Uint8Array>}
98
+ */
99
+ export async function imageURLtoJpegBytes(src, quality = 0.9) {
100
+ const img = await loadImage(src);
101
+ const canvas = document.createElement('canvas');
102
+ canvas.width = img.width;
103
+ canvas.height = img.height;
104
+ canvas.getContext('2d').drawImage(img, 0, 0);
105
+ const jpg = canvas.toDataURL('image/jpeg', quality);
106
+ return dataURLToBytes(jpg);
107
+ }
108
+
109
+ /**
110
+ * Resolves a data URL string into an HTMLImageElement.
111
+ * @param {string} dataUrl
112
+ * @returns {Promise<HTMLImageElement>}
113
+ */
114
+ export function dataURLToImage(dataUrl) {
115
+ return new Promise((resolve, reject) => {
116
+ const img = new Image();
117
+ img.onload = () => resolve(img);
118
+ img.onerror = reject;
119
+ img.src = dataUrl;
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Downloads the QR SVG element as a PNG file.
125
+ * @param {SVGSVGElement} svgEl
126
+ * @param {object} [opts]
127
+ * @param {number} [opts.scale=3]
128
+ * @param {string} [opts.fileName='qr.png']
129
+ * @returns {Promise<void>}
130
+ */
131
+ export async function downloadQrPng(svgEl, { scale = 3, fileName = 'qr.png' } = {}) {
132
+ if (!svgEl) throw new Error('downloadQrPng: svgEl is required');
133
+ const dataUrl = await svgToPngDataURL(svgEl, scale);
134
+ const a = document.createElement('a');
135
+ a.href = dataUrl;
136
+ a.download = fileName;
137
+ document.body.appendChild(a);
138
+ a.click();
139
+ a.remove();
140
+ }
package/utils/url.js ADDED
@@ -0,0 +1,20 @@
1
+ // utils/url.js
2
+ // URL sanitisation utilities for QR codes.
3
+ // Strips tracking parameters (UTM, gclid, fbclid, …) and optionally the protocol
4
+ // to reduce QR byte usage. No external dependencies.
5
+
6
+ export function sanitizeUrlForQR(input, { whitelist = [], aggressive = true, preserveProtocol = true } = {}){
7
+ try{
8
+ const u = new URL(input);
9
+ const TRACKING = new Set(['utm_source','utm_medium','utm_campaign','utm_term','utm_content','utm_id','gclid','gbraid','wbraid','fbclid','yclid','msclkid','mc_cid','mc_eid','_hsenc','_hsmi','ref','ref_src']);
10
+ const keep = new Set(whitelist);
11
+ const next = new URLSearchParams();
12
+ const entries = Array.from(u.searchParams.entries());
13
+ if(aggressive){ for(const [k,v] of entries) if(keep.has(k)) next.append(k,v); }
14
+ else { for(const [k,v] of entries) if(!TRACKING.has(k) || keep.has(k)) next.append(k,v); }
15
+ u.search = next.toString()? `?${next}` : '';
16
+ if(!preserveProtocol && (u.protocol==='https:'||u.protocol==='http:')) return `${u.host}${u.pathname}${u.search}${u.hash}`;
17
+ return u.toString();
18
+ }catch{ return String(input||'').trim(); }
19
+ }
20
+ export function utf8ByteLen(s){ return new TextEncoder().encode(String(s)).length; }
@@ -0,0 +1,45 @@
1
+ // worker/qr.worker.js
2
+ // Web Worker entry point for makeQr.
3
+ //
4
+ // Why a Worker?
5
+ // makeQr is synchronous and CPU-bound. For v10-12 QR codes on low-end mobile
6
+ // it can take 5-15 ms, which blocks the main thread and causes frame jank during
7
+ // real-time input. Running it in a Worker keeps the UI smooth.
8
+ //
9
+ // The flat Uint8Array output (model.modules, model.functionMask) is Transferable —
10
+ // it can be moved to the main thread with zero copying via postMessage transfer.
11
+ //
12
+ // Usage from main thread:
13
+ //
14
+ // const worker = new Worker(new URL('./worker/qr.worker.js', import.meta.url), { type: 'module' });
15
+ //
16
+ // worker.postMessage({ id: 1, value: 'https://example.com', eccLevel: 'L', maxVersion: 6 });
17
+ //
18
+ // worker.onmessage = ({ data }) => {
19
+ // if (data.error) { console.error(data.error); return; }
20
+ // // data.model is the full QRModel, modules and functionMask are Uint8Arrays
21
+ // renderQrToCanvas(data.model, canvas);
22
+ // };
23
+ //
24
+ // Message protocol:
25
+ //
26
+ // → { id, value, eccLevel?, maxVersion? }
27
+ // ← { id, model: { version, size, modules: Uint8Array, functionMask: Uint8Array, eccLevel } }
28
+ // ← { id, error: string }
29
+
30
+ import { makeQr } from '../qr/qr-core.js';
31
+
32
+ self.onmessage = ({ data }) => {
33
+ const { id, value, eccLevel = 'M', maxVersion = 6 } = data;
34
+ try {
35
+ const model = makeQr(value, { eccLevel, maxVersion });
36
+
37
+ // Transfer the Uint8Arrays to avoid copying (~size² bytes each)
38
+ self.postMessage(
39
+ { id, model },
40
+ [model.modules.buffer, model.functionMask.buffer],
41
+ );
42
+ } catch (e) {
43
+ self.postMessage({ id, error: String(e) });
44
+ }
45
+ };