From 0b02d77c6e77bb3bdfbee665111a8f5dc2ffe3dd Mon Sep 17 00:00:00 2001 From: Antonio Ventilii Date: Thu, 19 Mar 2026 15:45:37 +0100 Subject: [PATCH 01/10] feat: Improve QR scanning reliability for dark borderless codes --- src/lib/components/QRCodeReader.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/QRCodeReader.svelte b/src/lib/components/QRCodeReader.svelte index db7c0c1d1..bb00d16e7 100644 --- a/src/lib/components/QRCodeReader.svelte +++ b/src/lib/components/QRCodeReader.svelte @@ -23,7 +23,7 @@ viewfinderWidth: number, viewfinderHeight: number, ) => { - const minEdgePercentage = 0.7; // 70% + const minEdgePercentage = 0.9; // 90% const minEdgeSize = Math.min(viewfinderWidth, viewfinderHeight); const qrboxSize = Math.floor(minEdgeSize * minEdgePercentage); return { From 5afa2fde489de79867a3d9b624b19a7bf93e37dc Mon Sep 17 00:00:00 2001 From: Antonio Ventilii Date: Mon, 23 Mar 2026 09:09:25 +0100 Subject: [PATCH 02/10] more --- package-lock.json | 87 ++++++-- package.json | 4 +- src/lib/components/QRCode.svelte | 67 ++++-- src/lib/components/QRCodeReader.svelte | 204 ++++++++++++------ .../components/qr-code-reader/+page.md | 2 +- 5 files changed, 266 insertions(+), 98 deletions(-) diff --git a/package-lock.json b/package-lock.json index 01fa30dd1..22eb6c306 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,9 @@ "dependencies": { "decimal.js": "^10.6.0", "dompurify": "^3.3.1", - "html5-qrcode": "^2.3.8", "marked": "^9.1.0", - "qr-creator": "^1.0.0" + "qr-creator": "^1.0.0", + "zxing-wasm": "^3.0.1" }, "devDependencies": { "@dfinity/eslint-config-oisy-wallet": "^0.2.2", @@ -2232,6 +2232,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/emscripten": { + "version": "1.41.5", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz", + "integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -5171,11 +5177,6 @@ "node": ">=18" } }, - "node_modules/html5-qrcode": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz", - "integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==" - }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -8021,6 +8022,18 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tapable": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", @@ -8303,6 +8316,21 @@ "node": ">=4" } }, + "node_modules/type-fest": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz", + "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -9097,6 +9125,19 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zxing-wasm": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-3.0.1.tgz", + "integrity": "sha512-3CLj6iaGkpqPWXAB4pIWkFOR63MwqGekpMzaROFKto4dFowiPmLlC56KoMoOSXzqOCOpI5DAvMdB8ku2va6fUg==", + "license": "MIT", + "dependencies": { + "@types/emscripten": "^1.41.5", + "type-fest": "^5.4.4" + }, + "peerDependencies": { + "@types/emscripten": ">=1.39.6" + } } }, "dependencies": { @@ -10254,6 +10295,11 @@ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true }, + "@types/emscripten": { + "version": "1.41.5", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz", + "integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==" + }, "@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -12184,11 +12230,6 @@ "whatwg-encoding": "^3.1.1" } }, - "html5-qrcode": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz", - "integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==" - }, "http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -14012,6 +14053,11 @@ "@pkgr/core": "^0.2.9" } }, + "tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==" + }, "tapable": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", @@ -14191,6 +14237,14 @@ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, + "type-fest": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz", + "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", + "requires": { + "tagged-tag": "^1.0.0" + } + }, "typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -14636,6 +14690,15 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "peer": true + }, + "zxing-wasm": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-3.0.1.tgz", + "integrity": "sha512-3CLj6iaGkpqPWXAB4pIWkFOR63MwqGekpMzaROFKto4dFowiPmLlC56KoMoOSXzqOCOpI5DAvMdB8ku2va6fUg==", + "requires": { + "@types/emscripten": "^1.41.5", + "type-fest": "^5.4.4" + } } } } diff --git a/package.json b/package.json index d36e616c0..a7c08d74a 100644 --- a/package.json +++ b/package.json @@ -73,9 +73,9 @@ "dependencies": { "decimal.js": "^10.6.0", "dompurify": "^3.3.1", - "html5-qrcode": "^2.3.8", "marked": "^9.1.0", - "qr-creator": "^1.0.0" + "qr-creator": "^1.0.0", + "zxing-wasm": "^3.0.1" }, "keywords": [ "ui", diff --git a/src/lib/components/QRCode.svelte b/src/lib/components/QRCode.svelte index 061a5b803..a940de3d1 100644 --- a/src/lib/components/QRCode.svelte +++ b/src/lib/components/QRCode.svelte @@ -19,6 +19,11 @@ radius?: number; // https://www.qrcode.com/en/about/error_correction.html ecLevel?: "L" | "M" | "Q" | "H"; + // Quiet zone (clear margin) around the QR code in CSS pixels. + // ISO 18004 requires at least 4 modules of quiet zone for reliable scanning. + // Set to a non-zero value to improve mobile scanner compatibility, + // especially for dark-themed or borderless QR codes. + quietZone?: number; onQRCodeRendered?: () => void; logo?: Snippet; } @@ -30,6 +35,7 @@ backgroundColor = "white", radius = 0, ecLevel = "H", + quietZone = 0, onQRCodeRendered, logo, }: Props = $props(); @@ -88,22 +94,55 @@ }); const renderCanvas = () => { - if (isNullish(canvas) || isNullish(size)) { + if (isNullish(canvas) || isNullish(size) || isNullish(QrCreator)) { return; } - QrCreator?.render( - { - text: value, - radius, - ecLevel, - fill: fillColor, - background: backgroundColor, - // We draw the canvas larger and scale its container down to avoid blurring on high-density displays - size: size.width * 2, - }, - canvas, - ); + // We draw the canvas larger and scale its container down to avoid blurring on high-density displays + const scaleFactor = 2; + const totalSize = size.width * scaleFactor; + const quietZonePx = Math.max(quietZone, 0) * scaleFactor; + const qrSize = totalSize - 2 * quietZonePx; + + if (quietZonePx > 0 && qrSize > 0) { + // qr-creator renders edge-to-edge without a quiet zone. + // Render to a temporary canvas, then composite onto the main canvas + // with a background-colored margin to create the required quiet zone. + const tempCanvas = document.createElement("canvas"); + QrCreator.render( + { + text: value, + radius, + ecLevel, + fill: fillColor, + background: backgroundColor, + size: qrSize, + }, + tempCanvas, + ); + + canvas.width = totalSize; + canvas.height = totalSize; + + const ctx = canvas.getContext("2d"); + if (ctx) { + ctx.fillStyle = backgroundColor; + ctx.fillRect(0, 0, totalSize, totalSize); + ctx.drawImage(tempCanvas, quietZonePx, quietZonePx); + } + } else { + QrCreator.render( + { + text: value, + radius, + ecLevel, + fill: fillColor, + background: backgroundColor, + size: totalSize, + }, + canvas, + ); + } onQRCodeRendered?.(); }; @@ -111,7 +150,7 @@ let canvas = $state(); $effect(() => { - [QrCreator, value, canvas]; + [QrCreator, value, canvas, quietZone]; (() => renderCanvas())(); }); diff --git a/src/lib/components/QRCodeReader.svelte b/src/lib/components/QRCodeReader.svelte index bb00d16e7..a4c020c2b 100644 --- a/src/lib/components/QRCodeReader.svelte +++ b/src/lib/components/QRCodeReader.svelte @@ -1,5 +1,4 @@ -
+
+ + + + +
+
+
+
diff --git a/src/routes/(split)/components/qr-code-reader/+page.md b/src/routes/(split)/components/qr-code-reader/+page.md index 64ae114e9..905b7aa13 100644 --- a/src/routes/(split)/components/qr-code-reader/+page.md +++ b/src/routes/(split)/components/qr-code-reader/+page.md @@ -32,7 +32,7 @@ If used in a modal, prefer the wrapper ``. ## Library -This component uses the library [jsQR](https://github.com/cozmo/jsQR). +This component uses [zxing-wasm](https://github.com/nicbarker/zxing-wasm) (ZXing C++ compiled to WebAssembly) for QR code decoding, with native camera access via `getUserMedia`. ## Events From 9187ef0be73b7bc8ab4ceac835956cc86ff6a152 Mon Sep 17 00:00:00 2001 From: Antonio Ventilii Date: Tue, 24 Mar 2026 15:17:04 +0100 Subject: [PATCH 03/10] lint --- src/lib/components/QRCodeReader.svelte | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/lib/components/QRCodeReader.svelte b/src/lib/components/QRCodeReader.svelte index a4c020c2b..6a885db1e 100644 --- a/src/lib/components/QRCodeReader.svelte +++ b/src/lib/components/QRCodeReader.svelte @@ -44,7 +44,9 @@ try { const { readBarcodes } = await import("zxing-wasm/reader"); - if (isDestroyed) return; + if (isDestroyed) { + return; + } const scan = async () => { if ( @@ -61,7 +63,9 @@ try { const { videoWidth, videoHeight } = videoElement; - if (videoWidth === 0 || videoHeight === 0) return; + if (videoWidth === 0 || videoHeight === 0) { + return; + } // Use full video resolution for maximum decoding accuracy on mobile canvasElement.width = videoWidth; @@ -70,7 +74,9 @@ const ctx = canvasElement.getContext("2d", { willReadFrequently: true, }); - if (!ctx) return; + if (!ctx) { + return; + } ctx.drawImage(videoElement, 0, 0, videoWidth, videoHeight); const imageData = ctx.getImageData(0, 0, videoWidth, videoHeight); @@ -124,7 +130,7 @@
- + From c4b1e02af16cc64f501fc5e62c19d98d238507f9 Mon Sep 17 00:00:00 2001 From: Antonio Ventilii Date: Tue, 24 Mar 2026 15:21:07 +0100 Subject: [PATCH 04/10] more --- src/lib/components/QRCodeReader.svelte | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/lib/components/QRCodeReader.svelte b/src/lib/components/QRCodeReader.svelte index 6a885db1e..a84901a87 100644 --- a/src/lib/components/QRCodeReader.svelte +++ b/src/lib/components/QRCodeReader.svelte @@ -1,4 +1,5 @@