Merge branch 'release/1.35.0'

This commit is contained in:
2025-07-23 13:13:43 +02:00
5 changed files with 209 additions and 40 deletions

View File

@@ -95,6 +95,14 @@ The MongoDB server v > 5.0 will not run on and old machine such as Acer Revo due
This issue was solved by using an older Mongo DB Version 4.4.27
## Decoding Barcode
Barcode decoding is slow and can lead to locking of the main thread.
This heavy lifting could be moved to background thread.
The new solution could be based on the following code: https://github.com/pocesar/react-use-qrcode
That solution uses video to decode the QR code. This could be modified so that it uses data stored in canvas.
# OAuth verification video transcript
This is Rezije app demonstration for Google OAuth login process for the users of our app.

View File

@@ -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) => {
@@ -145,37 +151,126 @@ const pdf2canvas = async function (pdfFile:File): Promise<HTMLCanvasElement> {
const pdf = await pdfJS.getDocument(data).promise;
const page: PDFPageProxy = await pdf.getPage(1);
const canvases: Array<HTMLCanvasElement> = [];
const scale = 4;
const viewport = page.getViewport({ scale });
for(let i = 0; i< pdf.numPages; i++) {
const page: PDFPageProxy = await pdf.getPage(i+1);
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
const scale = 4;
const viewport = page.getViewport({ scale });
await page.render({ canvasContext: context as CanvasRenderingContext2D, viewport }).promise;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
return(canvas);
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,7 +361,7 @@ 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) {

View File

@@ -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 { findDecodePdf417 } from "../lib/pdf/barcodeDecoder";
import { DecodeResult, findDecodePdf417 } from "../lib/pdf/barcodeDecoder";
import { useLocale, useTranslations } from "next-intl";
// Next.js does not encode an utf-8 file name correctly when sending a form with a file attachment
@@ -37,10 +37,12 @@ export const BillEditForm:FC<BillEditFormProps> = ({ location, bill }) => {
const handleAction = updateOrAddBillMiddleware.bind(null, locationID, billID, billYear, billMonth);
const [ isScanningPDF, setIsScanningPDF ] = React.useState<boolean>(false);
const [ state, dispatch ] = useFormState(handleAction, initialState);
const [ isPaid, setIsPaid ] = React.useState<boolean>(paid);
const [ payedAmount, setPayedAmount ] = React.useState<string>(initialPayedAmount ? `${initialPayedAmount/100}` : "" );
const [ barcodeImage, setBarcodeImage ] = React.useState<string | undefined>(initialBarcodeImage);
const [ barcodeResults, setBarcodeResults ] = React.useState<Array<DecodeResult> | null>(null);
const billPaid_handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setIsPaid(event.target.checked);
@@ -50,19 +52,39 @@ export const BillEditForm:FC<BillEditFormProps> = ({ location, bill }) => {
setPayedAmount(event.target.value);
}
const billAttachment_handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
findDecodePdf417(event)
.then(result => {
if(result) {
const {
barcodeImage,
billInfo
} = result;
const billAttachment_handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
setIsScanningPDF(true);
setPayedAmount(`${billInfo.amount/100}`);
setBarcodeImage(barcodeImage);
}
});
setPayedAmount("");
setBarcodeImage(undefined);
setBarcodeResults(null);
const results = await findDecodePdf417(event);
if(results && results.length > 0) {
if(results.length === 1) {
const {
barcodeImage,
billInfo
} = results[0];
setPayedAmount(`${billInfo.amount/100}`);
setBarcodeImage(barcodeImage);
} else {
setPayedAmount("");
setBarcodeImage(undefined);
setBarcodeResults(results);
}
}
setIsScanningPDF(false);
}
const handleBarcodeSelectClick = (result: DecodeResult) => {
setPayedAmount(`${result.billInfo.amount/100}`);
setBarcodeImage(result.barcodeImage);
setBarcodeResults(null);
}
return(
@@ -98,8 +120,36 @@ export const BillEditForm:FC<BillEditFormProps> = ({ location, bill }) => {
: null
}
<div className="flex">
<input id="billAttachment" name="billAttachment" type="file" className="file-input file-input-bordered grow file-input-xs my-2 block" onChange={billAttachment_handleChange} />
<input id="billAttachment" name="billAttachment" type="file" className="file-input file-input-bordered grow file-input-s my-2 block max-w-[17em] md:max-w-[80em] break-words" onChange={billAttachment_handleChange} />
</div>
{
isScanningPDF &&
<div className="flex flex-row items-center w-full mt-2">
<div className="loading loading-spinner loading-m mr-2"></div>
<span>{t("scanning-pdf")}</span>
</div>
}
{
// if multiple results are found, show them as a list
// and notify the user to select the correct one
barcodeResults && barcodeResults.length > 0 &&
<>
<div className="flex mt-2 ml-2">
<label className="label-text max-w-xs break-words">{t("multiple-barcode-results-notification")}</label>
</div>
<div className="flex">
<label className="label grow pt-0 ml-3">
<ul className="list-none">
{barcodeResults.map((result, index) => (
<li key={index} className="cursor-pointer mt-3" onClick={() => handleBarcodeSelectClick(result)}>
👉 { result.billInfo.description }
</li>
))}
</ul>
</label>
</div>
</>
}
<div id="status-error" aria-live="polite" aria-atomic="true">
{state.errors?.billAttachment &&
state.errors.billAttachment.map((error: string) => (
@@ -141,7 +191,6 @@ export const BillEditForm:FC<BillEditFormProps> = ({ location, bill }) => {
<p className="text-xs my-1">{t.rich('barcode-disclaimer', { br: () => <br /> })}</p>
</div> : null
}
<textarea id="billNotes" name="billNotes" className="textarea textarea-bordered my-2 max-w-lg w-full block" placeholder={t("notes-placeholder")} defaultValue={notes ?? ''}></textarea>
<div id="status-error" aria-live="polite" aria-atomic="true">
{state.errors?.billNotes &&

View File

@@ -69,6 +69,8 @@
"bill-edit-form": {
"bill-name-placeholder": "Bill name",
"paid-checkbox": "Paid",
"scanning-pdf": "🕵️‍♂️ Scanning document for 2D codes ...",
"multiple-barcode-results-notification": "✅ Multiple 2D barcodes found. Pick the one which you want to use:",
"payed-amount": "Amount",
"barcode-disclaimer": "After scanning the code make sure the information is correct.<br></br>We are not liable in case of an incorrect payment.",
"notes-placeholder": "Notes",

View File

@@ -69,6 +69,8 @@
"bill-edit-form": {
"bill-name-placeholder": "Ime računa",
"paid-checkbox": "Plaćeno",
"scanning-pdf": "🕵️‍♂️ Tražim 2D barkodove unutar dokumenta...",
"multiple-barcode-results-notification": "✅ Pronađeno je više 2D barkodova. Molimo odaberi onaj koji se odnosi na zadani mjesec:",
"payed-amount": "Iznos",
"barcode-disclaimer": "Nakon skeniranja bar koda obavezni provjeri jesu li svi podaci ispravni.<br></br>Ne snosimo odgovornost za slučaj pogrešno provedene uplate.",
"notes-placeholder": "Bilješke",