Remove canvas splitting logic since zxing-wasm natively supports multiple barcode detection with maxNumberOfSymbols parameter. Reduces code by 69 lines and improves performance by requiring only a single decode call per canvas. Set maxNumberOfSymbols to 10 for realistic utility bill use case. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
335 lines
9.8 KiB
TypeScript
335 lines
9.8 KiB
TypeScript
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: 10,
|
|
};
|
|
|
|
// give browser a chance to re-paint
|
|
// this is needed to avoid UI freezing when decoding large images
|
|
await yieldToBrowser('decodeFromCanvas');
|
|
|
|
const imageData = canvasToImageData(canvas);
|
|
const results = await readBarcodes(imageData, readerOptions);
|
|
|
|
const codesFound: Array<DecodeResult> = results
|
|
.filter(result => result.text)
|
|
.map((result) => ({
|
|
hub3aText: result.text,
|
|
billInfo: parseHubText(result.text),
|
|
}));
|
|
|
|
return (codesFound);
|
|
|
|
} catch (error) {
|
|
console.log(error);
|
|
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));
|
|
}
|