diff --git a/.claude/settings.local.json b/.claude/settings.local.json index afef7dc..25fa374 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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, diff --git a/app/lib/pdf/barcodeDecoderWasm.ts b/app/lib/pdf/barcodeDecoderWasm.ts new file mode 100644 index 0000000..2f83849 --- /dev/null +++ b/app/lib/pdf/barcodeDecoderWasm.ts @@ -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((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} the canvas with the image rendered onto it + */ +const file2canvas = async function (imageFile:File): Promise { + + const reader = new FileReader(); + + const canvas = await new Promise((resolve, reject) => { + reader.onload = (progressEvent:ProgressEvent) => { + 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} the canvas with the image rendered onto it + */ +const image2canvas = async function (imageBase64:string): Promise { + + const canvas = await new Promise((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} the canvas with the first page of the PDF + */ +const pdf2canvas = async function (pdfFile:File): Promise> { + + const reader = new FileReader(); + const data = await new Promise((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 = []; + + 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 | null>} - an array of decoded results + * */ +const decodeFromCanvas = async (canvas:HTMLCanvasElement): Promise | 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|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 = []; + + // 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 => { + 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 => { + 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} 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): Promise|null> { + const file = (event.target as HTMLInputElement).files?.[0]; + + if(!file) { + console.error('No file was selected.'); + return null; + } + + return(await decodeFromFile(file)); +} diff --git a/app/ui/BillEditForm.tsx b/app/ui/BillEditForm.tsx index e95d14b..79909db 100644 --- a/app/ui/BillEditForm.tsx +++ b/app/ui/BillEditForm.tsx @@ -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"; diff --git a/middleware.ts b/middleware.ts index ec2ffdf..4fdfc6d 100644 --- a/middleware.ts +++ b/middleware.ts @@ -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$).*)', ], }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index dd5824f..91010ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" + } } } } diff --git a/package.json b/package.json index 23adc57..1a0f52e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/zxing_reader.wasm b/public/zxing_reader.wasm new file mode 100644 index 0000000..112f8ca Binary files /dev/null and b/public/zxing_reader.wasm differ