diff --git a/app/lib/pdf/barcodeDecoder.ts b/app/lib/pdf/barcodeDecoder.ts new file mode 100644 index 0000000..99e3b08 --- /dev/null +++ b/app/lib/pdf/barcodeDecoder.ts @@ -0,0 +1,271 @@ +import { PDFPageProxy } from 'pdfjs-dist'; +import { BrowserPDF417Reader, BrowserMultiFormatReader } from '@zxing/browser'; + +import * as pdfJSx from 'pdfjs-dist'; +import { BarcodeFormat, DecodeHintType, Result } from '@zxing/library'; + +export type BillInfo = { + header: string, + currency: string, + ammount: number, + payerName: string, + payerAddress: string, + payerTown: string, + payeeName: string, + payeeAddress: string, + payeeTown: string, + IBAN: string, + model: string, + reference: string, + code: string, + description: string, +}; + + +/** + * 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 + * ammount: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) => { + const [ + header, + currency, + ammount, + payerName, + payerAddress, + payerTown, + payeeName, + payeeAddress, + payeeTown, + IBAN, + model, + reference, + code, + description, + ] = text.split('\n'); + + return { + header, + currency, + ammount: parseInt(ammount, 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 image2canvas = 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 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 page: PDFPageProxy = await pdf.getPage(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; + + return(canvas); +} + +/** + * Decodes PDF417 from image contained within the given canvas + * */ +const decodeFromCanvas = async (canvas:HTMLCanvasElement) => { + 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 result = await codeReader.decodeFromCanvas(canvas); + + return({ + billInfo: parseHubText(result.getText()), + barcodeImage: copyBarcodeImage(canvas, result) + }) + } catch(ex:any) { + console.log(ex); + return(null); + } +} + +/** + * Copies bar code from the given canvas + * +*/ +const copyBarcodeImage = (canvas:HTMLCanvasElement, decoderResult:Result) => { + + // get coordinates of bar code + const points = decoderResult.getResultPoints(); + + // get outter coordinates of the bar code + const codeLocation = points.reduce((acc, point) => { + + const x = point.getX(); + const y = point.getY(); + let result = { + top: y < acc.top ? y: acc.top, + left: x < acc.left ? x: acc.left, + bottom: y > acc.bottom ? y: acc.bottom, + right: x > acc.right ? x: acc.right + }; + + return({ + ...result, + width: result.right - result.left, + height: result.bottom - result.top, + }); + }, { + top: Number.MAX_SAFE_INTEGER, + left: Number.MAX_SAFE_INTEGER, + bottom: 0, + right: 0, + width: 0, + height: 0 + }); + + // copy section of the canvas containing bar code to another canvas + const tempCanvas = document.createElement('canvas'); + const tempContext = tempCanvas.getContext('2d'); + + tempCanvas.width = codeLocation.width; + tempCanvas.height = codeLocation.height; + + // Draw the portion of the original canvas onto the temporary canvas + // Assuming you want to copy a 100x100 pixels square starting from (50, 50) of the original canvas + tempContext?.drawImage(canvas, codeLocation.left, codeLocation.top, codeLocation.width, codeLocation.height, 0, 0, codeLocation.width, codeLocation.height); + + // Convert the temporary canvas to a data URL + const dataURL = tempCanvas.toDataURL(); + + // Create a new Image object + const barcodeImage = new Image(); + + // Set the src of the image object to the data URL + barcodeImage.src = dataURL; + + return(barcodeImage); +} + +/** Finds PDF417 code within a file and decodes it */ +const decodeFromFile = async (file:File) => { + switch(file.type) { + case 'image/png': + case 'image/jpeg': + return(await decodeFromCanvas( await image2canvas(file) )); + case 'application/pdf': + return(await decodeFromCanvas( await pdf2canvas(file) )); + 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<{ billInfo: BillInfo, barcodeImage:HTMLImageElement } | 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/app/lib/pdf/pdf2png.ts b/app/lib/pdf/pdf2png.ts deleted file mode 100644 index 9f0ef74..0000000 --- a/app/lib/pdf/pdf2png.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { PDFPageProxy } from 'pdfjs-dist'; -import { BrowserPDF417Reader, BrowserMultiFormatReader } from '@zxing/browser'; - -import * as pdfJSx from 'pdfjs-dist'; -import { BarcodeFormat, DecodeHintType } from '@zxing/library'; - - -/** - * 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 pdf2canvas(event: React.ChangeEvent): Promise { - const file = (event.target as HTMLInputElement).files?.[0]; - - if(!file) { - console.error('No file was selected.'); - return null; - } - - if (file.type !== 'application/pdf') { - console.error(file.name, 'is not a .pdf file.'); - return null; - } - - 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(file); - }); - - 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 page: PDFPageProxy = await pdf.getPage(1); - - const scale = 1.5; - 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; - - const hints = new Map(); - hints.set(DecodeHintType.POSSIBLE_FORMATS, [ BarcodeFormat.PDF_417 ]); - // hints.set(DecodeHintType.TRY_HARDER, true); - hints.set(DecodeHintType.PURE_BARCODE, false); - - const codeReader = new BrowserPDF417Reader(hints); - // const codeReader = new BrowserMultiFormatReader(hints); - const result = await codeReader.decodeFromCanvas(canvas); - - // TODO: Try Next: Quagga - - console.log(result); - console.log(result.getResultPoints()); - - // codeReader.decode(imageData); - - return null; -} \ No newline at end of file diff --git a/app/lib/pdf/pdfjs.ts b/app/lib/pdf/pdfjs.ts deleted file mode 100644 index a92e5e0..0000000 --- a/app/lib/pdf/pdfjs.ts +++ /dev/null @@ -1,9 +0,0 @@ -"use client"; - -import * as pdfjsModule from 'pdfjs-dist'; - -const pdfjs = ( - 'default' in pdfjsModule ? pdfjsModule['default'] : pdfjsModule -) as typeof pdfjsModule; - -export default pdfjs; \ No newline at end of file diff --git a/app/ui/BillEditForm.tsx b/app/ui/BillEditForm.tsx index 83f6323..b01932f 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 { pdf2canvas } from "../lib/pdf/pdf2png"; +import { findDecodePdf417 } from "../lib/pdf/barcodeDecoder"; // Next.js does not encode an utf-8 file name correctly when sending a form with a file attachment // This is a workaround for that @@ -40,7 +40,7 @@ export const BillEditForm:FC = ({ location, bill }) => { } const billAttachment_handleChange = (event: React.ChangeEvent) => { - pdf2canvas(event); + findDecodePdf417(event).then(result => console.log(result)); } return(