feat: migrate PDF417 barcode decoder from @zxing/browser to zxing-wasm

Replace @zxing/browser with zxing-wasm for better performance and smaller WebAssembly bundle size (919KB). Added middleware exclusion for .wasm files to prevent i18n routing interference.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Knee Cola
2025-12-19 18:01:44 +01:00
parent 7467f9d595
commit 5b0497891a
7 changed files with 481 additions and 5 deletions

View File

@@ -16,7 +16,10 @@
"Bash(npm run build:*)",
"Bash(openssl rand:*)",
"Bash(ls:*)",
"Bash(find:*)"
"Bash(find:*)",
"mcp__context7__resolve-library-id",
"mcp__context7__get-library-docs",
"mcp__serena__create_text_file"
]
},
"enableAllProjectMcpServers": true,

View File

@@ -0,0 +1,403 @@
import { PDFPageProxy } from 'pdfjs-dist';
import { readBarcodes, prepareZXingModule, type ReaderOptions } from 'zxing-wasm/reader';
// Configure WASM file location (similar to how pdf.worker.min.mjs is configured)
prepareZXingModule({
overrides: {
locateFile: (path, prefix) => {
if (path.endsWith('.wasm')) {
return window.location.origin + '/zxing_reader.wasm';
}
return prefix + path;
}
}
});
export type BillInfo = {
header: string,
currency: string,
amount: number,
payerName: string,
payerAddress: string,
payerTown: string,
payeeName: string,
payeeAddress: string,
payeeTown: string,
IBAN: string,
model: string,
reference: string,
code: string,
description: string,
};
/** Breaks current microtask execution and gives the UI thread a chance to do a re-paint */
const yieldToBrowser = (_label:string) => new Promise<boolean>((resolve) => {
setTimeout(() => {
resolve(true);
}, 0);
});
/**
* Decodes a PDF417 barcode
* @param text
* @returns
* @description
* Example text: "HRVHUB30\nEUR\n000000000012422\nDEREŽIĆ NIKOLA\nULICA DIVKA BUDAKA 17/17\n10000 ZAGREB\nGPZ-Opskrba d.o.o.\nRadnička cesta 1\n10000 Zagreb\nHR3623400091110343158\nHR05\n02964686-0307\nGASB\nAkontacijska rata za 01.2024.\n"
*
* Decoded into:
* header: HRVHUB30
* currency:EUR
* amount:000000000012422
* payerName:DEREŽIĆ NIKOLA
* payerAddress:ULICA DIVKA BUDAKA 17/17
* payerTown:10000 ZAGREB
* payeeName:GPZ-Opskrba d.o.o.
* payeeAddress:Radnička cesta 1
* payeeTown:10000 Zagreb
* IBAN:HR3623400091110343158
* model:HR05
* reference:02964686-0307
* code:GASB
* description:Akontacijska rata za 01.2024.
*
*/
const parseHubText = (text: string):BillInfo => {
const [
header,
currency,
amount,
payerName,
payerAddress,
payerTown,
payeeName,
payeeAddress,
payeeTown,
IBAN,
model,
reference,
code,
description,
] = text.split('\n');
return {
header,
currency,
amount: parseInt(amount, 10),
payerName,
payerAddress,
payerTown,
payeeName,
payeeAddress,
payeeTown,
IBAN,
model,
reference,
code,
description,
};
}
/**
* Render an image from the given file onto a canvas.
* @param {File} imageFile - a file containing an image
* @return {Promise<HTMLCanvasElement>} the canvas with the image rendered onto it
*/
const file2canvas = async function (imageFile:File): Promise<HTMLCanvasElement> {
const reader = new FileReader();
const canvas = await new Promise<HTMLCanvasElement>((resolve, reject) => {
reader.onload = (progressEvent:ProgressEvent<FileReader>) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
if(!ctx) {
reject("Context is not set")
return;
}
ctx.drawImage(img, 0, 0);
resolve(canvas);
};
const result = (progressEvent.target as FileReader).result;
img.src = result as string;
};
reader.onerror = (e) => reject(e);
reader.readAsDataURL(imageFile);
});
return(canvas);
}
/**
* Render an image from onto a canvas.
* @param {String} imageBase64 - base64 encoded image string
* @return {Promise<HTMLCanvasElement>} the canvas with the image rendered onto it
*/
const image2canvas = async function (imageBase64:string): Promise<HTMLCanvasElement> {
const canvas = await new Promise<HTMLCanvasElement>((resolve, reject) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
if(!ctx) {
reject("Context is not set")
return;
}
ctx.drawImage(img, 0, 0);
resolve(canvas);
};
img.src = imageBase64;
});
return(canvas);
};
/**
* Render the first page of a PDF document onto a new canvas.
* @param {File} pdfFile - a file containing a PDF document
* @return {Promise<HTMLCanvasElement>} the canvas with the first page of the PDF
*/
const pdf2canvas = async function (pdfFile:File): Promise<Array<HTMLCanvasElement>> {
const reader = new FileReader();
const data = await new Promise<Uint8Array>((resolve, reject) => {
reader.onload = (e) => resolve(new Uint8Array((e.target as FileReader).result as ArrayBuffer));
reader.onerror = (e) => reject(e);
reader.readAsArrayBuffer(pdfFile);
});
const pdfJS = await import('pdfjs-dist');
// worker file was manually copied to the `public` folder
pdfJS.GlobalWorkerOptions.workerSrc = window.location.origin + '/pdf.worker.min.mjs';
const pdf = await pdfJS.getDocument(data).promise;
const canvases: Array<HTMLCanvasElement> = [];
for(let i = 0; i< pdf.numPages; i++) {
const page: PDFPageProxy = await pdf.getPage(i+1);
const scale = 4;
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context as CanvasRenderingContext2D, viewport }).promise;
canvases.push(canvas);
}
return(canvases);
}
export type DecodeResult = {
hub3aText: string,
billInfo: BillInfo,
};
/**
* Convert canvas to ImageData for zxing-wasm
* @param canvas - HTMLCanvasElement to convert
* @returns ImageData object
*/
const canvasToImageData = (canvas: HTMLCanvasElement): ImageData => {
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Failed to get canvas context');
}
return ctx.getImageData(0, 0, canvas.width, canvas.height);
};
/**
* Searches the given canvas for all PDF417 codes and decodes them.
* @param {HTMLCanvasElement} canvas - the canvas to search for PDF417 codes
* @return {Promise<Array<DecodeResult> | null>} - an array of decoded results
* */
const decodeFromCanvas = async (canvas:HTMLCanvasElement): Promise<Array<DecodeResult> | null> => {
try {
const readerOptions: ReaderOptions = {
tryHarder: true,
formats: ['PDF417'],
maxNumberOfSymbols: 255,
};
const width = canvas.width;
const height = canvas.height;
// Canvas can contain multiple PDF417 codes, so we need to try to find them all
// The issue is that zxing-wasm can only decode one code at a time in some cases
// and it will throw an error if it finds more than one code.
// To solve this, we will try splitting the canvas into different number of subsections
// and decode each subsection separately. The best result will be the one with the most codes found.
const splits = [5,4,3,2,1,0];
let bestResult: Array<DecodeResult>|null = null;
for(let splitIx = 0; splitIx < splits.length; splitIx++) {
const split = splits[splitIx];
const overlap = split === 0 ? 0 : Math.round(height / 50); // 50% overlap ensuring that we don't miss any codes that might be split between sections
const sectionHeight = split === 0 ? height : (Math.floor( Math.floor(height / split) + overlap));
const canvasSections = Array.from({ length: split+1 }, (_, i) => {
const sectionCanvas = document.createElement('canvas');
sectionCanvas.width = width;
sectionCanvas.height = sectionHeight;
const sectionContext = sectionCanvas.getContext('2d');
if (!sectionContext) {
throw new Error('Failed to get canvas context');
}
// Calculate the starting Y position for each section
const startY = i===0 ? 0 : i * (sectionHeight) - overlap;
// Draw the section of the original canvas onto the new section canvas
sectionContext.drawImage(canvas, 0, startY, width, sectionHeight, 0, 0, width, sectionHeight);
return sectionCanvas;
});
const codesFoundInSection: Array<DecodeResult> = [];
// Try to decode each section
for (const sectionCanvas of canvasSections) {
try {
// give browser a chance to re-paint
// this is needed to avoid UI freezing when decoding large images
await yieldToBrowser('decodeFromCanvas');
const imageData = canvasToImageData(sectionCanvas);
const results = await readBarcodes(imageData, readerOptions);
for (const result of results) {
const hub3aText = result.text;
if (hub3aText) {
codesFoundInSection.push({
hub3aText,
billInfo: parseHubText(hub3aText),
});
}
}
} catch (error) {
// If no code was found in the current section an error will be thrown
// -> we can ignore it
} finally {
}
}
await yieldToBrowser('after decodeFromCanvas');
// IF in this iteration we found less codes than in the previous best result,
// we can stop searching for more codes
// This is because the number of codes found in each section will only decrease
// as we increase the number of sections (split)
if(bestResult && codesFoundInSection.length <= bestResult.length) {
return(bestResult);
}
bestResult = codesFoundInSection;
};
return(bestResult);
} catch(ex:any) {
console.log(ex);
return(null);
}
}
/** Finds PDF417 code within a base64 encoded image and decodes it */
export const decodeFromImage = async (imageBase64:string): Promise<DecodeResult|null> => {
const canvas = await image2canvas(imageBase64);
const readerOptions: ReaderOptions = {
tryHarder: true,
formats: ['PDF417'],
maxNumberOfSymbols: 1,
};
const imageData = canvasToImageData(canvas);
const results = await readBarcodes(imageData, readerOptions);
if (results.length === 0) {
return null;
}
const hub3aText = results[0].text;
return({
hub3aText,
billInfo: parseHubText(hub3aText)
});
}
/** Finds PDF417 code within a file and decodes it */
const decodeFromFile = async (file:File): Promise<DecodeResult[]|null> => {
switch(file.type) {
case 'image/png':
case 'image/jpeg':
return(await decodeFromCanvas( await file2canvas(file) ));
case 'application/pdf':
const pageCanvas = await pdf2canvas(file);
// go through each page of the PDF and decode the PDF417 codes
// if there are multiple pages, we will decode each page separately
// and return the results from all pages
const results = (await Promise.all(pageCanvas.map(async (canvas) => {
await yieldToBrowser('decodeFromCanvas');
return await decodeFromCanvas(canvas);
})))
// remove null results (pages with no PDF417 codes)
.filter((result) => result !== null)
// flatten the array of arrays into a single array
.flat() as DecodeResult[];
return(results);
default:
console.error(file.name, 'is not a .pdf file.');
return null;
}
}
/**
* Render the first page of a PDF document onto a new canvas.
* @param {Event} event - The change event from an HTMLInputElement.
* @return {Promise<HTMLCanvasElement | null>} The canvas with the first page of the PDF, or null if the document is not a PDF.
*/
export async function findDecodePdf417(event: React.ChangeEvent<HTMLInputElement>): Promise<Array<DecodeResult>|null> {
const file = (event.target as HTMLInputElement).files?.[0];
if(!file) {
console.error('No file was selected.');
return null;
}
return(await decodeFromFile(file));
}

View File

@@ -7,7 +7,7 @@ import { useFormState } from "react-dom";
import { updateOrAddBill } from "../lib/actions/billActions";
import Link from "next/link";
import { formatYearMonth } from "../lib/format";
import { decodeFromImage, DecodeResult, findDecodePdf417 } from "../lib/pdf/barcodeDecoder";
import { DecodeResult, findDecodePdf417 } from "../lib/pdf/barcodeDecoderWasm";
import { useLocale, useTranslations } from "next-intl";
import { Pdf417Barcode } from "./Pdf417Barcode";
import { InfoBox } from "./InfoBox";

View File

@@ -44,7 +44,8 @@ export default async function middleware(req: NextRequest) {
export const config = {
// for these paths middleware will not be called
// `pdf.worker.min.mjs` is a web worker code used by pdf.js
// `*.wasm` files are WebAssembly modules used by zxing-wasm
matcher: [
'/((?!api|_next/static|_next/image|.*\\.png$|pdf.worker.min.mjs$|.*\\.webm$).*)',
'/((?!api|_next/static|_next/image|.*\\.png$|pdf.worker.min.mjs$|.*\\.wasm$|.*\\.webm$).*)',
],
};

70
package-lock.json generated
View File

@@ -39,7 +39,8 @@
"tailwindcss": "^3.4.0",
"typescript": "5.2.2",
"use-debounce": "^10.0.0",
"zod": "^3.22.2"
"zod": "^3.22.2",
"zxing-wasm": "^2.2.4"
},
"devDependencies": {
"@types/bcrypt": "^5.0.1",
@@ -148,6 +149,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz",
"integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==",
"dev": true,
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.23.5",
@@ -501,6 +503,7 @@
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -544,6 +547,7 @@
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -1070,6 +1074,7 @@
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz",
"integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4",
"@mui/core-downloads-tracker": "^7.3.5",
@@ -1470,6 +1475,7 @@
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.1.0.tgz",
"integrity": "sha512-x4FavbNEeXx/baD/zC/SdrvkjSby8nBn8KcCREqk6UuwvwoAPZmaV8TFCAuo/cpovBRTIY67mHhe86MQQm/68Q==",
"dev": true,
"peer": true,
"dependencies": {
"glob": "10.3.10"
}
@@ -1744,6 +1750,12 @@
"@types/node": "*"
}
},
"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/iban": {
"version": "0.0.35",
"resolved": "https://registry.npmjs.org/@types/iban/-/iban-0.0.35.tgz",
@@ -1800,6 +1812,7 @@
"version": "18.2.21",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz",
"integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -1921,6 +1934,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
"dev": true,
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "6.21.0",
"@typescript-eslint/types": "6.21.0",
@@ -2220,6 +2234,7 @@
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.21.3.tgz",
"integrity": "sha512-hZHqFe2JyH/ZxviJZosZjV+2s6EDSY0O24R+FQmlWZBZXP9IqMo7S3nb3+2LBWxodJQkSurdQGnqE7KXqrYgow==",
"peer": true,
"dependencies": {
"ts-custom-error": "^3.2.1"
},
@@ -2246,6 +2261,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"dev": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2665,6 +2681,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.668",
@@ -3343,6 +3360,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
"integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
"dev": true,
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -3538,6 +3556,7 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
"integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
"dev": true,
"peer": true,
"dependencies": {
"array-includes": "^3.1.7",
"array.prototype.findlastindex": "^1.2.3",
@@ -5977,6 +5996,7 @@
"resolved": "https://registry.npmjs.org/next/-/next-14.2.33.tgz",
"integrity": "sha512-GiKHLsD00t4ACm1p00VgrI0rUFAC9cRDGReKyERlM57aeEZkOQGcZTpIbsGn0b562FTPJWmYfKwplfO9EaT6ng==",
"license": "MIT",
"peer": true,
"dependencies": {
"@next/env": "14.2.33",
"@swc/helpers": "0.5.5",
@@ -6494,6 +6514,7 @@
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz",
"integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==",
"peer": true,
"dependencies": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
@@ -6695,6 +6716,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
@@ -6880,6 +6902,7 @@
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@@ -6908,6 +6931,7 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"dev": true,
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -7054,6 +7078,7 @@
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -7065,6 +7090,7 @@
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.0"
@@ -7997,10 +8023,23 @@
"url": "https://opencollective.com/unts"
}
},
"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/tailwindcss": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz",
"integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==",
"peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -8306,6 +8345,7 @@
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -8704,6 +8744,34 @@
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zxing-wasm": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-2.2.4.tgz",
"integrity": "sha512-1gq5zs4wuNTs5umWLypzNNeuJoluFvwmvjiiT3L9z/TMlVveeJRWy7h90xyUqCe+Qq0zL0w7o5zkdDMWDr9aZA==",
"license": "MIT",
"dependencies": {
"@types/emscripten": "^1.41.5",
"type-fest": "^5.2.0"
},
"peerDependencies": {
"@types/emscripten": ">=1.39.6"
}
},
"node_modules/zxing-wasm/node_modules/type-fest": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.1.tgz",
"integrity": "sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==",
"license": "(MIT OR CC0-1.0)",
"dependencies": {
"tagged-tag": "^1.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}
}
}

View File

@@ -41,7 +41,8 @@
"tailwindcss": "^3.4.0",
"typescript": "5.2.2",
"use-debounce": "^10.0.0",
"zod": "^3.22.2"
"zod": "^3.22.2",
"zxing-wasm": "^2.2.4"
},
"devDependencies": {
"@types/bcrypt": "^5.0.1",

BIN
public/zxing_reader.wasm Normal file

Binary file not shown.