Merge branch 'release/2.16.0'
This commit is contained in:
@@ -16,7 +16,11 @@
|
|||||||
"Bash(npm run build:*)",
|
"Bash(npm run build:*)",
|
||||||
"Bash(openssl rand:*)",
|
"Bash(openssl rand:*)",
|
||||||
"Bash(ls:*)",
|
"Bash(ls:*)",
|
||||||
"Bash(find:*)"
|
"Bash(find:*)",
|
||||||
|
"mcp__context7__resolve-library-id",
|
||||||
|
"mcp__context7__get-library-docs",
|
||||||
|
"mcp__serena__create_text_file",
|
||||||
|
"Bash(curl:*)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"enableAllProjectMcpServers": true,
|
"enableAllProjectMcpServers": true,
|
||||||
|
|||||||
@@ -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<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Ä\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<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} image - 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,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 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<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);
|
|
||||||
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<DecodeResult|null> => {
|
|
||||||
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<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));
|
|
||||||
}
|
|
||||||
334
app/lib/pdf/barcodeDecoderWasm.ts
Normal file
334
app/lib/pdf/barcodeDecoderWasm.ts
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,51 +0,0 @@
|
|||||||
import { BarcodeArray } from './pdf417';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a PDF417 barcode matrix to a canvas and returns it as a data URL.
|
|
||||||
*
|
|
||||||
* This function creates an HTML canvas element, draws the barcode by iterating through
|
|
||||||
* the barcode matrix, and converts the canvas to a base64-encoded PNG data URL that
|
|
||||||
* can be used as an image source.
|
|
||||||
*
|
|
||||||
* @param barcodeMatrix - The barcode array generated by the PDF417 encoder containing
|
|
||||||
* the barcode matrix data with dimensions and binary code values
|
|
||||||
* @param blockWidth - The width in pixels of each individual barcode module (bar/space unit)
|
|
||||||
* @param blockHeight - The height in pixels of each individual barcode module (bar/space unit)
|
|
||||||
*
|
|
||||||
* @returns A data URL string (base64-encoded PNG) representing the rendered barcode image,
|
|
||||||
* suitable for use in an HTML img src attribute
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const pdf417 = createPDF417();
|
|
||||||
* pdf417.init("Hello World", 2, 2);
|
|
||||||
* const barcodeArray = pdf417.getBarcodeArray();
|
|
||||||
* const dataUrl = renderBarcode(barcodeArray, 2, 4);
|
|
||||||
* // dataUrl can now be used: <img src={dataUrl} />
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function renderBarcode(barcodeMatrix: BarcodeArray, blockWidth: number, blockHeight: number) {
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = barcodeMatrix.num_cols * blockWidth;
|
|
||||||
canvas.height = barcodeMatrix.num_rows * blockHeight;
|
|
||||||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
|
||||||
|
|
||||||
let positionY = 0;
|
|
||||||
for (let row = 0; row < barcodeMatrix.num_rows; row += 1) {
|
|
||||||
let positionX = 0;
|
|
||||||
|
|
||||||
for (let col = 0; col < barcodeMatrix.num_cols; col += 1) {
|
|
||||||
if (barcodeMatrix.bcode[row][col] === 1) {
|
|
||||||
ctx.fillStyle = '#000';
|
|
||||||
} else {
|
|
||||||
ctx.fillStyle = '#FFF';
|
|
||||||
}
|
|
||||||
ctx.fillRect(positionX, positionY, blockWidth, blockHeight);
|
|
||||||
positionX += blockWidth;
|
|
||||||
}
|
|
||||||
positionY += blockHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
return canvas.toDataURL();
|
|
||||||
}
|
|
||||||
@@ -7,10 +7,10 @@ import { useFormState } from "react-dom";
|
|||||||
import { updateOrAddBill } from "../lib/actions/billActions";
|
import { updateOrAddBill } from "../lib/actions/billActions";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { formatYearMonth } from "../lib/format";
|
import { formatYearMonth } from "../lib/format";
|
||||||
import { decodeFromImage, DecodeResult, findDecodePdf417 } from "../lib/pdf/barcodeDecoder";
|
import { DecodeResult, findDecodePdf417 } from "../lib/pdf/barcodeDecoderWasm";
|
||||||
import { useLocale, useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import { Pdf417Barcode } from "./Pdf417Barcode";
|
|
||||||
import { InfoBox } from "./InfoBox";
|
import { InfoBox } from "./InfoBox";
|
||||||
|
import { Pdf417BarcodeWasm } from "./Pdf417BarcodeWasm";
|
||||||
|
|
||||||
// Next.js does not encode an utf-8 file name correctly when sending a form with a file attachment
|
// 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
|
// This is a workaround for that
|
||||||
@@ -50,22 +50,8 @@ export const BillEditForm: FC<BillEditFormProps> = ({ location, bill }) => {
|
|||||||
const [barcodeResults, setBarcodeResults] = React.useState<Array<DecodeResult> | null>(null);
|
const [barcodeResults, setBarcodeResults] = React.useState<Array<DecodeResult> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// migrating the legacy `barcodeImage` field to `hub3aText`
|
console.log("[BillEditForm] hub3a text from DB:", bill?.hub3aText);
|
||||||
// by converting it to `hub3aText`
|
}, [bill?.hub3aText]);
|
||||||
if (!hub3aText && bill?.barcodeImage) {
|
|
||||||
decodeFromImage(bill.barcodeImage).then(results => {
|
|
||||||
if (results) {
|
|
||||||
const {
|
|
||||||
hub3aText: decodedHub3aText,
|
|
||||||
} = results;
|
|
||||||
|
|
||||||
setHub3aText(decodedHub3aText);
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
|
||||||
console.error('Failed to migrate barcodeImage to hub3aText:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [bill?.barcodeImage, hub3aText]);
|
|
||||||
|
|
||||||
|
|
||||||
const billedTo_handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
const billedTo_handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
@@ -98,11 +84,15 @@ export const BillEditForm: FC<BillEditFormProps> = ({ location, bill }) => {
|
|||||||
|
|
||||||
setPayedAmount(`${billInfo.amount / 100}`);
|
setPayedAmount(`${billInfo.amount / 100}`);
|
||||||
setHub3aText(hub3aText);
|
setHub3aText(hub3aText);
|
||||||
|
console.log("[BillEditForm] Single barcode result found:", hub3aText);
|
||||||
} else {
|
} else {
|
||||||
|
console.log("[BillEditForm] Multiple barcode results found:", results);
|
||||||
setPayedAmount("");
|
setPayedAmount("");
|
||||||
setBarcodeResults(results);
|
setBarcodeResults(results);
|
||||||
setHub3aText(undefined);
|
setHub3aText(undefined);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.log("[BillEditForm] No barcode results found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsScanningPDF(false);
|
setIsScanningPDF(false);
|
||||||
@@ -213,7 +203,7 @@ export const BillEditForm: FC<BillEditFormProps> = ({ location, bill }) => {
|
|||||||
hub3aText ?
|
hub3aText ?
|
||||||
<div className="form-control p-1">
|
<div className="form-control p-1">
|
||||||
<label className="label p-2 grow bg-white border border-gray-300 rounded-box justify-center">
|
<label className="label p-2 grow bg-white border border-gray-300 rounded-box justify-center">
|
||||||
<Pdf417Barcode hub3aText={hub3aText} />
|
<Pdf417BarcodeWasm hub3aText={hub3aText} />
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs my-1">{t.rich('barcode-disclaimer', { br: () => <br /> })}</p>
|
<p className="text-xs my-1">{t.rich('barcode-disclaimer', { br: () => <br /> })}</p>
|
||||||
</div> : null
|
</div> : null
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useEffect, FC } from 'react';
|
|
||||||
import { generateBarcode } from '../lib/pdf/pdf417';
|
|
||||||
import { renderBarcode } from '../lib/pdf/renderBarcode';
|
|
||||||
|
|
||||||
export const Pdf417Barcode:FC<{hub3aText:string, className?: string}> = ({hub3aText: hub3a_text, className}) => {
|
|
||||||
const [bitmapData, setBitmapData] = useState<string | undefined>(undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const barcodeMatrix = generateBarcode(hub3a_text);
|
|
||||||
const bitmap = renderBarcode(barcodeMatrix, 2, 2);
|
|
||||||
setBitmapData(bitmap);
|
|
||||||
}, [hub3a_text]);
|
|
||||||
|
|
||||||
// Don't render until bitmap is generated (prevents hydration mismatch)
|
|
||||||
if (!bitmapData) {
|
|
||||||
return (
|
|
||||||
<div style={{ width: "350px", height: "92px" }} className="flex items-center justify-center">
|
|
||||||
<span className="loading loading-spinner loading-lg"></span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img src={bitmapData} alt="PDF417 Barcode" className={className} style={className ? undefined : { width: "350px", height: "92px" }} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
76
app/ui/Pdf417BarcodeWasm.tsx
Normal file
76
app/ui/Pdf417BarcodeWasm.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, FC } from 'react';
|
||||||
|
import { writeBarcode, prepareZXingModule, type WriterOptions } from 'zxing-wasm/writer';
|
||||||
|
|
||||||
|
// Configure WASM file location for writer
|
||||||
|
prepareZXingModule({
|
||||||
|
overrides: {
|
||||||
|
locateFile: (path, prefix) => {
|
||||||
|
if (path.endsWith('.wasm')) {
|
||||||
|
return window.location.origin + '/zxing_writer.wasm';
|
||||||
|
}
|
||||||
|
return prefix + path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Pdf417BarcodeWasm: FC<{ hub3aText: string, className?: string }> = ({ hub3aText, className }) => {
|
||||||
|
const [barcodeDataUrl, setBarcodeDataUrl] = useState<string | undefined>(undefined);
|
||||||
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const generateBarcode = async () => {
|
||||||
|
try {
|
||||||
|
setError(undefined);
|
||||||
|
setBarcodeDataUrl(undefined);
|
||||||
|
|
||||||
|
const writerOptions: WriterOptions = {
|
||||||
|
format: 'PDF417',
|
||||||
|
ecLevel: "5",
|
||||||
|
scale: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await writeBarcode(hub3aText, writerOptions);
|
||||||
|
|
||||||
|
// Convert PNG blob to data URL
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
setBarcodeDataUrl(reader.result as string);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(result.image as Blob);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to generate PDF417 barcode:', err);
|
||||||
|
setError('Failed to generate barcode');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (hub3aText) {
|
||||||
|
generateBarcode();
|
||||||
|
}
|
||||||
|
}, [hub3aText]);
|
||||||
|
|
||||||
|
// Show error state
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div style={{ width: "350px", height: "92px" }} className="flex items-center justify-center">
|
||||||
|
<span className="text-error text-sm">{error}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't render until barcode is generated (prevents hydration mismatch)
|
||||||
|
if (!barcodeDataUrl) {
|
||||||
|
return (
|
||||||
|
<div style={{ width: "350px", height: "92px" }} className="flex items-center justify-center">
|
||||||
|
<span className="loading loading-spinner loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img src={barcodeDataUrl} alt="PDF417 Barcode" className={className} />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { PrintBarcodeData } from '../lib/actions/printActions';
|
import { PrintBarcodeData } from '../lib/actions/printActions';
|
||||||
import { Pdf417Barcode } from './Pdf417Barcode';
|
import { Pdf417BarcodeWasm } from './Pdf417BarcodeWasm';
|
||||||
|
|
||||||
export interface PrintPreviewProps {
|
export interface PrintPreviewProps {
|
||||||
data: PrintBarcodeData[];
|
data: PrintBarcodeData[];
|
||||||
@@ -132,21 +132,8 @@ export const PrintPreview: React.FC<PrintPreviewProps> = ({ data, year, month, t
|
|||||||
<td className="border-2 border-gray-800 px-3 py-1.5 text-center">
|
<td className="border-2 border-gray-800 px-3 py-1.5 text-center">
|
||||||
<div className="flex justify-center items-center">
|
<div className="flex justify-center items-center">
|
||||||
{
|
{
|
||||||
item.hub3aText ?
|
item.hub3aText ? <Pdf417BarcodeWasm hub3aText={item.hub3aText} className="print:m-[5em_auto]" /> : null
|
||||||
<Pdf417Barcode hub3aText={item.hub3aText} className="max-h-28 w-auto max-w-[270px] print:m-[5em_auto] print:h-[auto] print:max-h-[85px] print:w-[69.6mm] print:max-w-[69.6mm]" />
|
|
||||||
: (
|
|
||||||
// LEGACY SUPPORT ... untill all bills have been migrated
|
|
||||||
|
|
||||||
item.barcodeImage ?
|
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img
|
|
||||||
src={item.barcodeImage.startsWith('data:') ? item.barcodeImage : `data:image/png;base64,${item.barcodeImage}`}
|
|
||||||
alt={`Barcode for ${item.billName}`}
|
|
||||||
className="max-h-28 w-auto max-w-[270px] print:m-[5em_auto] print:h-[auto] print:max-h-[85px] print:w-[69.6mm] print:max-w-[69.6mm]"
|
|
||||||
/> : null
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { formatYearMonth } from "../lib/format";
|
import { formatYearMonth } from "../lib/format";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Pdf417Barcode } from "./Pdf417Barcode";
|
|
||||||
import { uploadProofOfPayment } from "../lib/actions/billActions";
|
import { uploadProofOfPayment } from "../lib/actions/billActions";
|
||||||
|
import { Pdf417BarcodeWasm } from "./Pdf417BarcodeWasm";
|
||||||
|
|
||||||
export interface ViewBillCardProps {
|
export interface ViewBillCardProps {
|
||||||
location: BillingLocation;
|
location: BillingLocation;
|
||||||
@@ -111,7 +111,7 @@ export const ViewBillCard: FC<ViewBillCardProps> = ({ location, bill, shareId })
|
|||||||
hub3aText ?
|
hub3aText ?
|
||||||
<div className="form-control p-1">
|
<div className="form-control p-1">
|
||||||
<label className="label p-2 grow bg-white border border-gray-300 rounded-box justify-center">
|
<label className="label p-2 grow bg-white border border-gray-300 rounded-box justify-center">
|
||||||
<Pdf417Barcode hub3aText={hub3aText} />
|
<Pdf417BarcodeWasm hub3aText={hub3aText} />
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs my-1">{t.rich('barcode-disclaimer', { br: () => <br /> })}</p>
|
<p className="text-xs my-1">{t.rich('barcode-disclaimer', { br: () => <br /> })}</p>
|
||||||
</div> : null
|
</div> : null
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import { formatCurrency, formatIban } from "../lib/formatStrings";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { ViewBillBadge } from "./ViewBillBadge";
|
import { ViewBillBadge } from "./ViewBillBadge";
|
||||||
import { Pdf417Barcode } from "./Pdf417Barcode";
|
|
||||||
import { EncodePayment, PaymentParams } from "hub-3a-payment-encoder";
|
import { EncodePayment, PaymentParams } from "hub-3a-payment-encoder";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { LinkIcon } from "@heroicons/react/24/outline";
|
import { LinkIcon } from "@heroicons/react/24/outline";
|
||||||
import { uploadUtilBillsProofOfPayment } from "../lib/actions/locationActions";
|
import { uploadUtilBillsProofOfPayment } from "../lib/actions/locationActions";
|
||||||
import QRCode from "react-qr-code";
|
import QRCode from "react-qr-code";
|
||||||
import { TicketIcon } from "@heroicons/react/24/solid";
|
import { TicketIcon } from "@heroicons/react/24/solid";
|
||||||
|
import { Pdf417BarcodeWasm } from "./Pdf417BarcodeWasm";
|
||||||
|
|
||||||
export interface ViewLocationCardProps {
|
export interface ViewLocationCardProps {
|
||||||
location: BillingLocation;
|
location: BillingLocation;
|
||||||
@@ -153,7 +153,7 @@ export const ViewLocationCard: FC<ViewLocationCardProps> = ({ location, userSett
|
|||||||
<li><strong>{t("payment-reference-label")}</strong><pre className="inline pl-1">{paymentParams.PozivNaBroj}</pre></li>
|
<li><strong>{t("payment-reference-label")}</strong><pre className="inline pl-1">{paymentParams.PozivNaBroj}</pre></li>
|
||||||
</ul>
|
</ul>
|
||||||
<label className="label p-2 grow bg-white border border-gray-300 rounded-box justify-center">
|
<label className="label p-2 grow bg-white border border-gray-300 rounded-box justify-center">
|
||||||
<Pdf417Barcode hub3aText={hub3aText} />
|
<Pdf417BarcodeWasm hub3aText={hub3aText} />
|
||||||
</label>
|
</label>
|
||||||
</>
|
</>
|
||||||
: null
|
: null
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ export default async function middleware(req: NextRequest) {
|
|||||||
export const config = {
|
export const config = {
|
||||||
// for these paths middleware will not be called
|
// for these paths middleware will not be called
|
||||||
// `pdf.worker.min.mjs` is a web worker code used by pdf.js
|
// `pdf.worker.min.mjs` is a web worker code used by pdf.js
|
||||||
|
// `*.wasm` files are WebAssembly modules used by zxing-wasm
|
||||||
matcher: [
|
matcher: [
|
||||||
'/((?!api|_next/static|_next/image|.*\\.png$|pdf.worker.min.mjs$|.*\\.webm$).*)',
|
'/((?!api|_next/static|_next/image|.*\\.png$|pdf.worker.min.mjs$|.*\\.wasm$|.*\\.webm$).*)',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
95
package-lock.json
generated
95
package-lock.json
generated
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "evidencija-rezija",
|
"name": "evidencija-rezija",
|
||||||
"version": "2.15.0",
|
"version": "2.16.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"version": "2.15.0",
|
"version": "2.16.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
@@ -15,8 +15,6 @@
|
|||||||
"@tailwindcss/typography": "^0.5.10",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
"@types/iban": "^0.0.35",
|
"@types/iban": "^0.0.35",
|
||||||
"@types/node": "20.5.7",
|
"@types/node": "20.5.7",
|
||||||
"@zxing/browser": "^0.1.4",
|
|
||||||
"@zxing/library": "^0.20.0",
|
|
||||||
"autoprefixer": "10.4.15",
|
"autoprefixer": "10.4.15",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
@@ -39,7 +37,8 @@
|
|||||||
"tailwindcss": "^3.4.0",
|
"tailwindcss": "^3.4.0",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"use-debounce": "^10.0.0",
|
"use-debounce": "^10.0.0",
|
||||||
"zod": "^3.22.2"
|
"zod": "^3.22.2",
|
||||||
|
"zxing-wasm": "^2.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcrypt": "^5.0.1",
|
"@types/bcrypt": "^5.0.1",
|
||||||
@@ -1749,6 +1748,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/emscripten": {
|
||||||
|
"version": "1.41.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz",
|
||||||
|
"integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/iban": {
|
"node_modules/@types/iban": {
|
||||||
"version": "0.0.35",
|
"version": "0.0.35",
|
||||||
"resolved": "https://registry.npmjs.org/@types/iban/-/iban-0.0.35.tgz",
|
"resolved": "https://registry.npmjs.org/@types/iban/-/iban-0.0.35.tgz",
|
||||||
@@ -2212,38 +2217,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@zxing/browser": {
|
|
||||||
"version": "0.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@zxing/browser/-/browser-0.1.4.tgz",
|
|
||||||
"integrity": "sha512-WYjaav7St4sj/u/Km2llE4NU2Pq3JFIWnczr0tmyCC1KUlp08rV3qpu7iiEB4kOx/CgcCzrSebNnSmFt5B3IFg==",
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@zxing/text-encoding": "^0.9.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@zxing/library": "^0.20.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@zxing/library": {
|
|
||||||
"version": "0.20.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.20.0.tgz",
|
|
||||||
"integrity": "sha512-6Ev6rcqVjMakZFIDvbUf0dtpPGeZMTfyxYg4HkVWioWeN7cRcnUWT3bU6sdohc82O1nPXcjq6WiGfXX2Pnit6A==",
|
|
||||||
"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": {
|
"node_modules/abbrev": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||||
@@ -8016,6 +7989,18 @@
|
|||||||
"url": "https://opencollective.com/unts"
|
"url": "https://opencollective.com/unts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tagged-tag": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.1",
|
"version": "3.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz",
|
||||||
@@ -8172,14 +8157,6 @@
|
|||||||
"typescript": ">=4.2.0"
|
"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": {
|
"node_modules/ts-interface-checker": {
|
||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||||
@@ -8725,6 +8702,34 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zxing-wasm": {
|
||||||
|
"version": "2.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-2.2.4.tgz",
|
||||||
|
"integrity": "sha512-1gq5zs4wuNTs5umWLypzNNeuJoluFvwmvjiiT3L9z/TMlVveeJRWy7h90xyUqCe+Qq0zL0w7o5zkdDMWDr9aZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/emscripten": "^1.41.5",
|
||||||
|
"type-fest": "^5.2.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/emscripten": ">=1.39.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zxing-wasm/node_modules/type-fest": {
|
||||||
|
"version": "5.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.1.tgz",
|
||||||
|
"integrity": "sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==",
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"dependencies": {
|
||||||
|
"tagged-tag": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,6 @@
|
|||||||
"@tailwindcss/typography": "^0.5.10",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
"@types/iban": "^0.0.35",
|
"@types/iban": "^0.0.35",
|
||||||
"@types/node": "20.5.7",
|
"@types/node": "20.5.7",
|
||||||
"@zxing/browser": "^0.1.4",
|
|
||||||
"@zxing/library": "^0.20.0",
|
|
||||||
"autoprefixer": "10.4.15",
|
"autoprefixer": "10.4.15",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
@@ -41,7 +39,8 @@
|
|||||||
"tailwindcss": "^3.4.0",
|
"tailwindcss": "^3.4.0",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"use-debounce": "^10.0.0",
|
"use-debounce": "^10.0.0",
|
||||||
"zod": "^3.22.2"
|
"zod": "^3.22.2",
|
||||||
|
"zxing-wasm": "^2.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcrypt": "^5.0.1",
|
"@types/bcrypt": "^5.0.1",
|
||||||
@@ -59,5 +58,5 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.17.0"
|
"node": ">=18.17.0"
|
||||||
},
|
},
|
||||||
"version": "2.15.0"
|
"version": "2.16.0"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/zxing_reader.wasm
Normal file
BIN
public/zxing_reader.wasm
Normal file
Binary file not shown.
BIN
public/zxing_writer.wasm
Normal file
BIN
public/zxing_writer.wasm
Normal file
Binary file not shown.
Reference in New Issue
Block a user