diff --git a/app/lib/pdf/barcodeDecoder.ts b/app/lib/pdf/barcodeDecoder.ts deleted file mode 100644 index 2110bcc..0000000 --- a/app/lib/pdf/barcodeDecoder.ts +++ /dev/null @@ -1,370 +0,0 @@ -import { PDFPageProxy } from 'pdfjs-dist'; -import { BrowserPDF417Reader } from '@zxing/browser'; - -import { BarcodeFormat, DecodeHintType, Result } from '@zxing/library'; - -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Ä\u008dka 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Ä\u008dka 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} image - 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, -}; - -/** - * 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 hints = new Map(); - hints.set(DecodeHintType.POSSIBLE_FORMATS, [ BarcodeFormat.PDF_417 ]); - hints.set(DecodeHintType.PURE_BARCODE, false); - - const codeReader = new BrowserPDF417Reader(hints); - - 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 `BrowserPDF417Reader` can only decode one code at a time - // 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 result = await codeReader.decodeFromCanvas(sectionCanvas); - const hub3aText = result.getText() - - if (result) { - 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 hints = new Map(); - hints.set(DecodeHintType.POSSIBLE_FORMATS, [ BarcodeFormat.PDF_417 ]); - hints.set(DecodeHintType.PURE_BARCODE, false); - - const codeReader = new BrowserPDF417Reader(hints); - const result = await codeReader.decodeFromCanvas(canvas); - const hub3aText = result.getText(); - - 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)); -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 91010ea..4c2f91e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,6 @@ "@tailwindcss/typography": "^0.5.10", "@types/iban": "^0.0.35", "@types/node": "20.5.7", - "@zxing/browser": "^0.1.5", - "@zxing/library": "^0.21.3", "autoprefixer": "10.4.15", "bcrypt": "^5.1.1", "clsx": "^2.0.0", @@ -2219,38 +2217,6 @@ } } }, - "node_modules/@zxing/browser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@zxing/browser/-/browser-0.1.5.tgz", - "integrity": "sha512-4Lmrn/il4+UNb87Gk8h1iWnhj39TASEHpd91CwwSJtY5u+wa0iH9qS0wNLAWbNVYXR66WmT5uiMhZ7oVTrKfxw==", - "optionalDependencies": { - "@zxing/text-encoding": "^0.9.0" - }, - "peerDependencies": { - "@zxing/library": "^0.21.0" - } - }, - "node_modules/@zxing/library": { - "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" - }, - "engines": { - "node": ">= 10.4.0" - }, - "optionalDependencies": { - "@zxing/text-encoding": "~0.9.0" - } - }, - "node_modules/@zxing/text-encoding": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", - "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", - "optional": true - }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -8191,14 +8157,6 @@ "typescript": ">=4.2.0" } }, - "node_modules/ts-custom-error": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz", - "integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", diff --git a/package.json b/package.json index 1a0f52e..b5521c8 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,6 @@ "@tailwindcss/typography": "^0.5.10", "@types/iban": "^0.0.35", "@types/node": "20.5.7", - "@zxing/browser": "^0.1.5", - "@zxing/library": "^0.21.3", "autoprefixer": "10.4.15", "bcrypt": "^5.1.1", "clsx": "^2.0.0",