implemented scanning PDF docs for multiple 2D codes
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { PDFPageProxy } from 'pdfjs-dist';
|
||||
import { BrowserPDF417Reader, BrowserMultiFormatReader } from '@zxing/browser';
|
||||
import { BrowserPDF417Reader } from '@zxing/browser';
|
||||
|
||||
import * as pdfJSx from 'pdfjs-dist';
|
||||
import { BarcodeFormat, DecodeHintType, Result } from '@zxing/library';
|
||||
@@ -21,6 +21,12 @@ export type BillInfo = {
|
||||
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
|
||||
@@ -46,7 +52,7 @@ export type BillInfo = {
|
||||
* description:Akontacijska rata za 01.2024.
|
||||
*
|
||||
*/
|
||||
const parseHubText = (text: string) => {
|
||||
const parseHubText = (text: string):BillInfo => {
|
||||
const [
|
||||
header,
|
||||
currency,
|
||||
@@ -129,7 +135,7 @@ const image2canvas = async function (imageFile:File): Promise<HTMLCanvasElement>
|
||||
* @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<HTMLCanvasElement> {
|
||||
const pdf2canvas = async function (pdfFile:File): Promise<Array<HTMLCanvasElement>> {
|
||||
|
||||
const reader = new FileReader();
|
||||
const data = await new Promise<Uint8Array>((resolve, reject) => {
|
||||
@@ -144,38 +150,127 @@ const pdf2canvas = async function (pdfFile:File): Promise<HTMLCanvasElement> {
|
||||
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 canvases: Array<HTMLCanvasElement> = [];
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
for(let i = 0; i< pdf.numPages; i++) {
|
||||
const page: PDFPageProxy = await pdf.getPage(i+1);
|
||||
|
||||
await page.render({ canvasContext: context as CanvasRenderingContext2D, viewport }).promise;
|
||||
const scale = 4;
|
||||
const viewport = page.getViewport({ scale });
|
||||
|
||||
return(canvas);
|
||||
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 = {
|
||||
billInfo: BillInfo,
|
||||
barcodeImage: string,
|
||||
};
|
||||
|
||||
/**
|
||||
* Decodes PDF417 from image contained within the given canvas
|
||||
* 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) => {
|
||||
const decodeFromCanvas = async (canvas:HTMLCanvasElement): Promise<Array<DecodeResult> | 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 result = await codeReader.decodeFromCanvas(canvas);
|
||||
|
||||
return({
|
||||
billInfo: parseHubText(result.getText()),
|
||||
barcodeImage: copyBarcodeImage(canvas, result)
|
||||
})
|
||||
|
||||
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<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 result = await codeReader.decodeFromCanvas(sectionCanvas);
|
||||
|
||||
if (result) {
|
||||
codesFoundInSection.push({
|
||||
billInfo: parseHubText(result.getText()),
|
||||
barcodeImage: copyBarcodeImage(sectionCanvas, result)
|
||||
});
|
||||
}
|
||||
|
||||
} 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);
|
||||
@@ -186,7 +281,7 @@ const decodeFromCanvas = async (canvas:HTMLCanvasElement) => {
|
||||
* Copies bar code from the given canvas
|
||||
*
|
||||
*/
|
||||
const copyBarcodeImage = (canvas:HTMLCanvasElement, decoderResult:Result) => {
|
||||
const copyBarcodeImage = (canvas:HTMLCanvasElement, decoderResult:Result):string => {
|
||||
|
||||
// get coordinates of bar code
|
||||
const points = decoderResult.getResultPoints();
|
||||
@@ -241,7 +336,20 @@ const decodeFromFile = async (file:File) => {
|
||||
case 'image/jpeg':
|
||||
return(await decodeFromCanvas( await image2canvas(file) ));
|
||||
case 'application/pdf':
|
||||
return(await decodeFromCanvas( await pdf2canvas(file) ));
|
||||
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();
|
||||
|
||||
return(results);
|
||||
default:
|
||||
console.error(file.name, 'is not a .pdf file.');
|
||||
return null;
|
||||
@@ -253,13 +361,13 @@ const decodeFromFile = async (file:File) => {
|
||||
* @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<{ billInfo: BillInfo, barcodeImage:string } | null> {
|
||||
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));
|
||||
}
|
||||
Reference in New Issue
Block a user