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 ? setRunning(false)}>Stop Capture : (
+
+ setRunning('screen')}>Start Screen Capture
+ setRunning('camera')}>Start Webcam Capture
+
+ )}
+ {(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,
+ }))
+ })
+})