diff --git a/web/lukegbcom/next.config.js b/web/lukegbcom/next.config.js index 299b06360b..5c34f8bbca 100644 --- a/web/lukegbcom/next.config.js +++ b/web/lukegbcom/next.config.js @@ -10,6 +10,22 @@ const nextConfig = { loader: 'custom', disableStaticImages: true, }, + + webpack(config) { + config.resolve.fallback = { + ...config.resolve.fallback, + fs: false, + }; + config.module.rules.push({ + test: /\.wasm(\.bin)?$/i, + type: 'asset/resource', + generator: { + filename: 'static/[hash][ext][query]' + } + }) + + return config; + }, } module.exports = withPlugins([ diff --git a/web/lukegbcom/node-packages.nix b/web/lukegbcom/node-packages.nix index 1711ca40e3..4aafabbea1 100644 --- a/web/lukegbcom/node-packages.nix +++ b/web/lukegbcom/node-packages.nix @@ -4414,6 +4414,15 @@ let sha1 = "7e32f75b41381291d04611f1bf14109ac00651d7"; }; }; + "qrcode.react-3.0.1" = { + name = "qrcode.react"; + packageName = "qrcode.react"; + version = "3.0.1"; + src = fetchurl { + url = "https://registry.npmjs.org/qrcode.react/-/qrcode.react-3.0.1.tgz"; + sha512 = "uCNm16ClMCrdM2R20c/zqmdwHcbMQf3K7ey39EiK/UgEKbqWeM0iH2QxW3iDVFzjQKFzH23ICgOyG4gNsJ0/gw=="; + }; + }; "quantize-1.0.2" = { name = "quantize"; packageName = "quantize"; @@ -5449,6 +5458,15 @@ let sha512 = "r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="; }; }; + "zbar.wasm-2.1.1" = { + name = "zbar.wasm"; + packageName = "zbar.wasm"; + version = "2.1.1"; + src = fetchurl { + url = "https://registry.npmjs.org/zbar.wasm/-/zbar.wasm-2.1.1.tgz"; + sha512 = "iXX2tMFRRt2R4+mbKBhNa3TrN0fd5CbzLceasGTBOofA7eF1VvwZeAHW3UkEMDU8w3rSvwx9N3cyosX55UJ+Xw=="; + }; + }; "zwitch-2.0.2" = { name = "zwitch"; packageName = "zwitch"; @@ -6094,6 +6112,7 @@ let sources."pump-3.0.0" sources."punycode-2.1.1" sources."q-1.5.1" + sources."qrcode.react-3.0.1" sources."quantize-1.0.2" (sources."rc-1.2.8" // { dependencies = [ @@ -6235,6 +6254,7 @@ let sources."wrappy-1.0.2" sources."yallist-4.0.0" sources."yaml-1.10.2" + sources."zbar.wasm-2.1.1" sources."zwitch-2.0.2" ]; buildInputs = globalBuildInputs; diff --git a/web/lukegbcom/package-lock.json b/web/lukegbcom/package-lock.json index 13f57a462d..2ae02367dd 100644 --- a/web/lukegbcom/package-lock.json +++ b/web/lukegbcom/package-lock.json @@ -18,6 +18,7 @@ "next-compose-plugins": "^2.2.1", "next-mdx-remote": "^4.0.2", "next-optimized-images": "^3.0.0-canary.10", + "qrcode.react": "^3.0.1", "react": "^17.0.2", "react-dom": "^17.0.2", "rehype-highlight": "^5.0.2", @@ -26,7 +27,8 @@ "remark-parse": "^10.0.1", "remark-rehype": "^10.1.0", "sass": "^1.49.11", - "unified": "^10.1.2" + "unified": "^10.1.2", + "zbar.wasm": "^2.1.1" }, "devDependencies": { "eslint": "8.12.0", @@ -7519,6 +7521,14 @@ "teleport": ">=0.2.0" } }, + "node_modules/qrcode.react": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-3.0.1.tgz", + "integrity": "sha512-uCNm16ClMCrdM2R20c/zqmdwHcbMQf3K7ey39EiK/UgEKbqWeM0iH2QxW3iDVFzjQKFzH23ICgOyG4gNsJ0/gw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/quantize": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/quantize/-/quantize-1.0.2.tgz", @@ -9085,6 +9095,11 @@ "node": ">= 6" } }, + "node_modules/zbar.wasm": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/zbar.wasm/-/zbar.wasm-2.1.1.tgz", + "integrity": "sha512-iXX2tMFRRt2R4+mbKBhNa3TrN0fd5CbzLceasGTBOofA7eF1VvwZeAHW3UkEMDU8w3rSvwx9N3cyosX55UJ+Xw==" + }, "node_modules/zwitch": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz", @@ -14394,6 +14409,12 @@ "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" }, + "qrcode.react": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-3.0.1.tgz", + "integrity": "sha512-uCNm16ClMCrdM2R20c/zqmdwHcbMQf3K7ey39EiK/UgEKbqWeM0iH2QxW3iDVFzjQKFzH23ICgOyG4gNsJ0/gw==", + "requires": {} + }, "quantize": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/quantize/-/quantize-1.0.2.tgz", @@ -15522,6 +15543,11 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" }, + "zbar.wasm": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/zbar.wasm/-/zbar.wasm-2.1.1.tgz", + "integrity": "sha512-iXX2tMFRRt2R4+mbKBhNa3TrN0fd5CbzLceasGTBOofA7eF1VvwZeAHW3UkEMDU8w3rSvwx9N3cyosX55UJ+Xw==" + }, "zwitch": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz", diff --git a/web/lukegbcom/package.json b/web/lukegbcom/package.json index 71aec44a68..db40b8d7aa 100644 --- a/web/lukegbcom/package.json +++ b/web/lukegbcom/package.json @@ -19,6 +19,7 @@ "next-compose-plugins": "^2.2.1", "next-mdx-remote": "^4.0.2", "next-optimized-images": "^3.0.0-canary.10", + "qrcode.react": "^3.0.1", "react": "^17.0.2", "react-dom": "^17.0.2", "rehype-highlight": "^5.0.2", @@ -27,7 +28,8 @@ "remark-parse": "^10.0.1", "remark-rehype": "^10.1.0", "sass": "^1.49.11", - "unified": "^10.1.2" + "unified": "^10.1.2", + "zbar.wasm": "^2.1.1" }, "devDependencies": { "eslint": "8.12.0", diff --git a/web/lukegbcom/pages/tools.js b/web/lukegbcom/pages/tools.js index ef631a8aa7..b0f2153331 100644 --- a/web/lukegbcom/pages/tools.js +++ b/web/lukegbcom/pages/tools.js @@ -23,6 +23,9 @@ export default function Toolbox() { diff --git a/web/lukegbcom/pages/tools/ee-qrcode.js b/web/lukegbcom/pages/tools/ee-qrcode.js new file mode 100644 index 0000000000..3d29cbe9c3 --- /dev/null +++ b/web/lukegbcom/pages/tools/ee-qrcode.js @@ -0,0 +1,167 @@ +import Head from 'next/head' +import Link from 'next/link' +import dynamic from 'next/dynamic' +import React, { useEffect, useRef, useState } from 'react' +import { QRCodeSVG } from 'qrcode.react' +import styles from '../../styles/Post.module.scss' +import localStyles from './ee-qrcode.module.scss' +import HeaderNav from '../../lib/HeaderNav' +import HeroImage from '../../lib/HeroImage' + +let scannerWorker = null + +async function startCapture(video, canvas, captureType) { + if (typeof window !== 'undefined') { + let captureStream = null + + try { + if (captureType === 'screen') + captureStream = await navigator.mediaDevices.getDisplayMedia({ audio: false, video: true }) + else + captureStream = await navigator.mediaDevices.getUserMedia({ audio: false, video: true }) + } catch (e) { + alert(`Failed to start screen capture: ${e}`) + return + } + + video.srcObject = captureStream + video.play() + await new Promise(resolve => { + video.addEventListener('loadedmetadata', resolve) + }) + canvas.width = video.videoWidth + canvas.height = video.videoHeight + + return (() => { + captureStream.getTracks().forEach(track => track.stop()) + video.srcObject = null + }) + } +} + +function scanImage(imageData) { + if (scannerWorker === null) { + scannerWorker = new Worker(new URL('./ee-qrcode.worker.js', import.meta.url)) + } + + return new Promise(resolve => { + scannerWorker.onmessage = e => resolve(e.data.response) + scannerWorker.postMessage({imageData}) + }) +} + +async function processImage(video, hiddenCanvas, previewCanvas, filterSymbols, onResult) { + const hiddenCtx = hiddenCanvas.getContext('2d') + hiddenCtx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight) + + const allSymbols = await scanImage(hiddenCtx.getImageData(0, 0, video.videoWidth, video.videoHeight)) + + previewCanvas.width = video.videoWidth + previewCanvas.height = video.videoHeight + const previewCtx = previewCanvas.getContext('2d') + previewCtx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight) + const symbols = allSymbols.filter(filterSymbols || (() => true)) + previewCtx.strokeStyle = 'rgba(255, 0, 0, 0.7)' + previewCtx.lineWidth = 6 + symbols.map((sym) => { + if (sym.points < 2) return + const [firstPoint, ...points] = sym.points + + previewCtx.beginPath() + previewCtx.moveTo(firstPoint.x, firstPoint.y) + points.forEach(point => previewCtx.lineTo(point.x, point.y)) + previewCtx.closePath() + + previewCtx.stroke() + }) + if (symbols.length > 0) { + onResult(symbols[0].value) + } +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +async function runScanner({ video, canvas, previewCanvas, filterSymbols, onResult, running, captureType }) { + const stopCapture = await startCapture(video, canvas, captureType) + + while (running()) { + await processImage(video, canvas, previewCanvas, filterSymbols, onResult) + await sleep(500) + } + + stopCapture() +} + +function CodeScanner({ filterSymbols, onResult, captureType }) { + const videoRef = useRef(null) + const canvasRef = useRef(null) + const previewCanvasRef = useRef(null) + + useEffect(() => { + let running = true; + if (videoRef.current && canvasRef.current && previewCanvasRef.current) { + runScanner({ video: videoRef.current, canvas: canvasRef.current, previewCanvas: previewCanvasRef.current, running: (() => running), filterSymbols, onResult, captureType }) + } + return (() => { + running = false; + }); + }, [videoRef, canvasRef, previewCanvasRef, captureType]) + + return ( +
+
+ ) +} + +function esimQrcodeFilter(symbol) { + if (symbol.typeName !== 'ZBAR_QRCODE') return false + if (!symbol.value.match(/^LPA:/)) return false + return true +} + +function fixData(srcData) { + return srcData.replace(/[$][$]$/, '') +} + +export default function EsimMangler() { + const [running, setRunning] = useState(false) + const [scannedData, setScannedData] = useState(null) + + return ( +
+ + + EE QR Code Mangler + + + EE QR Code Mangler | Luke Granger-Brown + + + +
+
+

EE eSIM QR Code Fixer

+

EE, as of 2022-05-01, still generates not-quite-spec-compliant eSIM QR Codes, which get rejected by some Android devices - including the Pixel series of devices.

+

This page is intended to generate a fixed version of the QR code (which happens entirely on your computer!).

+

{running ? : ( + + + + + )}

+ {(scannedData !== null) ?

: null} + {(scannedData !== null) ?

{fixData(scannedData)}

: null} + {(running != false) ? { + setScannedData(result) + setRunning(false) + }} captureType={running} /> : null} +
+
+
+ ) +} diff --git a/web/lukegbcom/pages/tools/ee-qrcode.module.scss b/web/lukegbcom/pages/tools/ee-qrcode.module.scss new file mode 100644 index 0000000000..e8de10e896 --- /dev/null +++ b/web/lukegbcom/pages/tools/ee-qrcode.module.scss @@ -0,0 +1,10 @@ +.video { + position: absolute; + left: -10000px; + top: -10000px; + visibility: hidden; +} + +.canvas { + max-width: 100%; +} diff --git a/web/lukegbcom/pages/tools/ee-qrcode.worker.js b/web/lukegbcom/pages/tools/ee-qrcode.worker.js new file mode 100644 index 0000000000..c205c0fe49 --- /dev/null +++ b/web/lukegbcom/pages/tools/ee-qrcode.worker.js @@ -0,0 +1,13 @@ +import { scanImageData } from 'zbar.wasm' + +self.addEventListener('message', async e => { + const symbols = await scanImageData(e.data.imageData) + self.postMessage({ + response: symbols.map((sym, n) => ({ + index: n, + typeName: sym.typeName, + value: sym.decode(), + points: sym.points, + })) + }) +})