refactor: simplify barcode detection by removing canvas splitting
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>
This commit is contained in:
@@ -31,7 +31,7 @@ export type BillInfo = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** Breaks current microtask execution and gives the UI thread a chance to do a re-paint */
|
/** Breaks current microtask execution and gives the UI thread a chance to do a re-paint */
|
||||||
const yieldToBrowser = (_label:string) => new Promise<boolean>((resolve) => {
|
const yieldToBrowser = (_label: string) => new Promise<boolean>((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve(true);
|
resolve(true);
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -61,7 +61,7 @@ const yieldToBrowser = (_label:string) => new Promise<boolean>((resolve) => {
|
|||||||
* description:Akontacijska rata za 01.2024.
|
* description:Akontacijska rata za 01.2024.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const parseHubText = (text: string):BillInfo => {
|
const parseHubText = (text: string): BillInfo => {
|
||||||
const [
|
const [
|
||||||
header,
|
header,
|
||||||
currency,
|
currency,
|
||||||
@@ -102,12 +102,12 @@ const parseHubText = (text: string):BillInfo => {
|
|||||||
* @param {File} imageFile - a file containing an image
|
* @param {File} imageFile - a file containing an image
|
||||||
* @return {Promise<HTMLCanvasElement>} the canvas with the image rendered onto it
|
* @return {Promise<HTMLCanvasElement>} the canvas with the image rendered onto it
|
||||||
*/
|
*/
|
||||||
const file2canvas = async function (imageFile:File): Promise<HTMLCanvasElement> {
|
const file2canvas = async function (imageFile: File): Promise<HTMLCanvasElement> {
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
const canvas = await new Promise<HTMLCanvasElement>((resolve, reject) => {
|
const canvas = await new Promise<HTMLCanvasElement>((resolve, reject) => {
|
||||||
reader.onload = (progressEvent:ProgressEvent<FileReader>) => {
|
reader.onload = (progressEvent: ProgressEvent<FileReader>) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
|
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
@@ -116,7 +116,7 @@ const file2canvas = async function (imageFile:File): Promise<HTMLCanvasElement>
|
|||||||
canvas.width = img.width;
|
canvas.width = img.width;
|
||||||
canvas.height = img.height;
|
canvas.height = img.height;
|
||||||
|
|
||||||
if(!ctx) {
|
if (!ctx) {
|
||||||
reject("Context is not set")
|
reject("Context is not set")
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -135,7 +135,7 @@ const file2canvas = async function (imageFile:File): Promise<HTMLCanvasElement>
|
|||||||
reader.readAsDataURL(imageFile);
|
reader.readAsDataURL(imageFile);
|
||||||
});
|
});
|
||||||
|
|
||||||
return(canvas);
|
return (canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -143,7 +143,7 @@ const file2canvas = async function (imageFile:File): Promise<HTMLCanvasElement>
|
|||||||
* @param {String} imageBase64 - base64 encoded image string
|
* @param {String} imageBase64 - base64 encoded image string
|
||||||
* @return {Promise<HTMLCanvasElement>} the canvas with the image rendered onto it
|
* @return {Promise<HTMLCanvasElement>} the canvas with the image rendered onto it
|
||||||
*/
|
*/
|
||||||
const image2canvas = async function (imageBase64:string): Promise<HTMLCanvasElement> {
|
const image2canvas = async function (imageBase64: string): Promise<HTMLCanvasElement> {
|
||||||
|
|
||||||
const canvas = await new Promise<HTMLCanvasElement>((resolve, reject) => {
|
const canvas = await new Promise<HTMLCanvasElement>((resolve, reject) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
@@ -154,7 +154,7 @@ const image2canvas = async function (imageBase64:string): Promise<HTMLCanvasElem
|
|||||||
canvas.width = img.width;
|
canvas.width = img.width;
|
||||||
canvas.height = img.height;
|
canvas.height = img.height;
|
||||||
|
|
||||||
if(!ctx) {
|
if (!ctx) {
|
||||||
reject("Context is not set")
|
reject("Context is not set")
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -167,7 +167,7 @@ const image2canvas = async function (imageBase64:string): Promise<HTMLCanvasElem
|
|||||||
img.src = imageBase64;
|
img.src = imageBase64;
|
||||||
});
|
});
|
||||||
|
|
||||||
return(canvas);
|
return (canvas);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -175,7 +175,7 @@ const image2canvas = async function (imageBase64:string): Promise<HTMLCanvasElem
|
|||||||
* @param {File} pdfFile - a file containing a PDF document
|
* @param {File} pdfFile - a file containing a PDF document
|
||||||
* @return {Promise<HTMLCanvasElement>} the canvas with the first page of the PDF
|
* @return {Promise<HTMLCanvasElement>} the canvas with the first page of the PDF
|
||||||
*/
|
*/
|
||||||
const pdf2canvas = async function (pdfFile:File): Promise<Array<HTMLCanvasElement>> {
|
const pdf2canvas = async function (pdfFile: File): Promise<Array<HTMLCanvasElement>> {
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
const data = await new Promise<Uint8Array>((resolve, reject) => {
|
const data = await new Promise<Uint8Array>((resolve, reject) => {
|
||||||
@@ -193,8 +193,8 @@ const pdf2canvas = async function (pdfFile:File): Promise<Array<HTMLCanvasElemen
|
|||||||
|
|
||||||
const canvases: Array<HTMLCanvasElement> = [];
|
const canvases: Array<HTMLCanvasElement> = [];
|
||||||
|
|
||||||
for(let i = 0; i< pdf.numPages; i++) {
|
for (let i = 0; i < pdf.numPages; i++) {
|
||||||
const page: PDFPageProxy = await pdf.getPage(i+1);
|
const page: PDFPageProxy = await pdf.getPage(i + 1);
|
||||||
|
|
||||||
const scale = 4;
|
const scale = 4;
|
||||||
const viewport = page.getViewport({ scale });
|
const viewport = page.getViewport({ scale });
|
||||||
@@ -209,7 +209,7 @@ const pdf2canvas = async function (pdfFile:File): Promise<Array<HTMLCanvasElemen
|
|||||||
canvases.push(canvas);
|
canvases.push(canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
return(canvases);
|
return (canvases);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DecodeResult = {
|
export type DecodeResult = {
|
||||||
@@ -235,107 +235,38 @@ const canvasToImageData = (canvas: HTMLCanvasElement): ImageData => {
|
|||||||
* @param {HTMLCanvasElement} canvas - the canvas to search for PDF417 codes
|
* @param {HTMLCanvasElement} canvas - the canvas to search for PDF417 codes
|
||||||
* @return {Promise<Array<DecodeResult> | null>} - an array of decoded results
|
* @return {Promise<Array<DecodeResult> | null>} - an array of decoded results
|
||||||
* */
|
* */
|
||||||
const decodeFromCanvas = async (canvas:HTMLCanvasElement): Promise<Array<DecodeResult> | null> => {
|
const decodeFromCanvas = async (canvas: HTMLCanvasElement): Promise<Array<DecodeResult> | null> => {
|
||||||
try {
|
try {
|
||||||
const readerOptions: ReaderOptions = {
|
const readerOptions: ReaderOptions = {
|
||||||
tryHarder: true,
|
tryHarder: true,
|
||||||
formats: ['PDF417'],
|
formats: ['PDF417'],
|
||||||
maxNumberOfSymbols: 255,
|
maxNumberOfSymbols: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
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<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
|
// give browser a chance to re-paint
|
||||||
// this is needed to avoid UI freezing when decoding large images
|
// this is needed to avoid UI freezing when decoding large images
|
||||||
await yieldToBrowser('decodeFromCanvas');
|
await yieldToBrowser('decodeFromCanvas');
|
||||||
|
|
||||||
const imageData = canvasToImageData(sectionCanvas);
|
const imageData = canvasToImageData(canvas);
|
||||||
const results = await readBarcodes(imageData, readerOptions);
|
const results = await readBarcodes(imageData, readerOptions);
|
||||||
|
|
||||||
for (const result of results) {
|
const codesFound: Array<DecodeResult> = results
|
||||||
const hub3aText = result.text;
|
.filter(result => result.text)
|
||||||
|
.map((result) => ({
|
||||||
|
hub3aText: result.text,
|
||||||
|
billInfo: parseHubText(result.text),
|
||||||
|
}));
|
||||||
|
|
||||||
if (hub3aText) {
|
return (codesFound);
|
||||||
codesFoundInSection.push({
|
|
||||||
hub3aText,
|
|
||||||
billInfo: parseHubText(hub3aText),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If no code was found in the current section an error will be thrown
|
console.log(error);
|
||||||
// -> we can ignore it
|
return (null);
|
||||||
|
|
||||||
} 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 */
|
/** Finds PDF417 code within a base64 encoded image and decodes it */
|
||||||
export const decodeFromImage = async (imageBase64:string): Promise<DecodeResult|null> => {
|
export const decodeFromImage = async (imageBase64: string): Promise<DecodeResult | null> => {
|
||||||
const canvas = await image2canvas(imageBase64);
|
const canvas = await image2canvas(imageBase64);
|
||||||
|
|
||||||
const readerOptions: ReaderOptions = {
|
const readerOptions: ReaderOptions = {
|
||||||
@@ -353,18 +284,18 @@ export const decodeFromImage = async (imageBase64:string): Promise<DecodeResult|
|
|||||||
|
|
||||||
const hub3aText = results[0].text;
|
const hub3aText = results[0].text;
|
||||||
|
|
||||||
return({
|
return ({
|
||||||
hub3aText,
|
hub3aText,
|
||||||
billInfo: parseHubText(hub3aText)
|
billInfo: parseHubText(hub3aText)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Finds PDF417 code within a file and decodes it */
|
/** Finds PDF417 code within a file and decodes it */
|
||||||
const decodeFromFile = async (file:File): Promise<DecodeResult[]|null> => {
|
const decodeFromFile = async (file: File): Promise<DecodeResult[] | null> => {
|
||||||
switch(file.type) {
|
switch (file.type) {
|
||||||
case 'image/png':
|
case 'image/png':
|
||||||
case 'image/jpeg':
|
case 'image/jpeg':
|
||||||
return(await decodeFromCanvas( await file2canvas(file) ));
|
return (await decodeFromCanvas(await file2canvas(file)));
|
||||||
case 'application/pdf':
|
case 'application/pdf':
|
||||||
const pageCanvas = await pdf2canvas(file);
|
const pageCanvas = await pdf2canvas(file);
|
||||||
// go through each page of the PDF and decode the PDF417 codes
|
// go through each page of the PDF and decode the PDF417 codes
|
||||||
@@ -379,7 +310,7 @@ const decodeFromFile = async (file:File): Promise<DecodeResult[]|null> => {
|
|||||||
// flatten the array of arrays into a single array
|
// flatten the array of arrays into a single array
|
||||||
.flat() as DecodeResult[];
|
.flat() as DecodeResult[];
|
||||||
|
|
||||||
return(results);
|
return (results);
|
||||||
default:
|
default:
|
||||||
console.error(file.name, 'is not a .pdf file.');
|
console.error(file.name, 'is not a .pdf file.');
|
||||||
return null;
|
return null;
|
||||||
@@ -391,13 +322,13 @@ const decodeFromFile = async (file:File): Promise<DecodeResult[]|null> => {
|
|||||||
* @param {Event} event - The change event from an HTMLInputElement.
|
* @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.
|
* @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> {
|
export async function findDecodePdf417(event: React.ChangeEvent<HTMLInputElement>): Promise<Array<DecodeResult> | null> {
|
||||||
const file = (event.target as HTMLInputElement).files?.[0];
|
const file = (event.target as HTMLInputElement).files?.[0];
|
||||||
|
|
||||||
if(!file) {
|
if (!file) {
|
||||||
console.error('No file was selected.');
|
console.error('No file was selected.');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return(await decodeFromFile(file));
|
return (await decodeFromFile(file));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user