diff --git a/.claude/settings.local.json b/.claude/settings.local.json index afef7dc..3f9cead 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -16,7 +16,11 @@ "Bash(npm run build:*)", "Bash(openssl rand:*)", "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, diff --git a/app/lib/pdf/barcodeDecoder.ts b/app/lib/pdf/barcodeDecoder.ts deleted file mode 100644 index 2110bcc..0000000 --- a/app/lib/pdf/barcodeDecoder.ts +++ /dev/null @@ -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((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} the canvas with the image rendered onto it - */ -const file2canvas = async function (imageFile:File): Promise { - - const reader = new FileReader(); - - const canvas = await new Promise((resolve, reject) => { - reader.onload = (progressEvent:ProgressEvent) => { - 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} the canvas with the image rendered onto it - */ -const image2canvas = async function (imageBase64:string): Promise { - - const canvas = await new Promise((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} the canvas with the first page of the PDF - */ -const pdf2canvas = async function (pdfFile:File): Promise> { - - const reader = new FileReader(); - const data = await new Promise((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 = []; - - 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 | null>} - an array of decoded results - * */ -const decodeFromCanvas = async (canvas:HTMLCanvasElement): Promise | 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|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 = []; - - // 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 => { - 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 => { - 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} 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): Promise|null> { - const file = (event.target as HTMLInputElement).files?.[0]; - - if(!file) { - console.error('No file was selected.'); - return null; - } - - return(await decodeFromFile(file)); -} \ No newline at end of file diff --git a/app/lib/pdf/barcodeDecoderWasm.ts b/app/lib/pdf/barcodeDecoderWasm.ts new file mode 100644 index 0000000..464879b --- /dev/null +++ b/app/lib/pdf/barcodeDecoderWasm.ts @@ -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((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} the canvas with the image rendered onto it + */ +const file2canvas = async function (imageFile: File): Promise { + + const reader = new FileReader(); + + const canvas = await new Promise((resolve, reject) => { + reader.onload = (progressEvent: ProgressEvent) => { + 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} the canvas with the image rendered onto it + */ +const image2canvas = async function (imageBase64: string): Promise { + + const canvas = await new Promise((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} the canvas with the first page of the PDF + */ +const pdf2canvas = async function (pdfFile: File): Promise> { + + const reader = new FileReader(); + const data = await new Promise((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 = []; + + 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 | null>} - an array of decoded results + * */ +const decodeFromCanvas = async (canvas: HTMLCanvasElement): Promise | 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 = 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 => { + 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 => { + 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} 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): Promise | null> { + const file = (event.target as HTMLInputElement).files?.[0]; + + if (!file) { + console.error('No file was selected.'); + return null; + } + + return (await decodeFromFile(file)); +} diff --git a/app/lib/pdf/pdf417.ts b/app/lib/pdf/pdf417.ts deleted file mode 100644 index 95f5d40..0000000 --- a/app/lib/pdf/pdf417.ts +++ /dev/null @@ -1,1053 +0,0 @@ -import { bcadd, bcdiv, bcmul } from "./bcmath"; -import { - textsubmodes, - textlatch, - clusters, - rsfactors, - TextLatchKeys -} from "./pdf417LookupTables"; - -/** - * PDF417 - 2D Barcode generator (LGPLv3) - * - * Ported from PHP - PDF417 class, version 1.0.005, from TCPDF library (http://www.tcpdf.org/) - */ - -export interface BarcodeArray { - num_rows: number; - num_cols: number; - bcode: number[][]; -} - -const ROWHEIGHT = 4; -const QUIETH = 2; -const QUIETV = 2; -const start_pattern = "11111111010101000"; -const stop_pattern = "111111101000101001"; - -function getInputSequences(code: string): [number, string][] { - var sequence_array: [number, string][] = []; // array to be returned - var numseq: [string, number][] = []; - // get numeric sequences - var numseqMatch = code.match(/([0-9]{13,44})/g); - if (numseqMatch == null) { - numseq = []; - } else { - // add offset to each matched line - for (var n = 0, offset = 0; n < numseqMatch.length; n++) { - offset = code.indexOf(numseqMatch[n], offset); - numseq.push([numseqMatch[n], offset]); - offset += numseqMatch[n].length; - } - } - numseq.push(["", code.length]); - var offset = 0; - for (var i = 0; i < numseq.length; i++) { - var seq = numseq[i]; - var seqlen = seq[0].length; - if (seq[1] > 0) { - // extract text sequence before the number sequence - var prevseq = code.substr(offset, seq[1] - offset); - var textseq: [string, number][] = []; - // get text sequences - var textseqMatch = prevseq.match(/([\x09\x0a\x0d\x20-\x7e]{5,})/g); - if (textseqMatch == null) { - textseq = []; - } else { - // add offset to each matched line - for (var n = 0; n < textseqMatch.length; n++) { - var txtOffset = prevseq.indexOf(textseqMatch[n]); - textseq.push([textseqMatch[n], txtOffset]); - } - } - textseq.push(["", prevseq.length]); - var txtoffset = 0; - for (var j = 0; j < textseq.length; j++) { - var txtseq = textseq[j]; - var txtseqlen = txtseq[0].length; - if (txtseq[1] > 0) { - // extract byte sequence before the text sequence - var prevtxtseq = prevseq.substr(txtoffset, txtseq[1] - txtoffset); - if (prevtxtseq.length > 0) { - // add BYTE sequence - if ( - prevtxtseq.length == 1 && - sequence_array.length > 0 && - sequence_array[sequence_array.length - 1][0] == 900 - ) { - sequence_array.push([913, prevtxtseq]); - } else if (prevtxtseq.length % 6 == 0) { - sequence_array.push([924, prevtxtseq]); - } else { - sequence_array.push([901, prevtxtseq]); - } - } - } - if (txtseqlen > 0) { - // add numeric sequence - sequence_array.push([900, txtseq[0]]); - } - txtoffset = txtseq[1] + txtseqlen; - } - } - if (seqlen > 0) { - // add numeric sequence - sequence_array.push([902, seq[0]]); - } - offset = seq[1] + seqlen; - } - return sequence_array; -}; - -function getCompaction(mode: number, code: string, addmode?: boolean): number[] { - addmode = addmode ?? true; - var cw: number[] = []; // array of codewords to return - switch (mode) { - case 900: { - // Text Compaction mode latch - var submode = 0; // default Alpha sub-mode - var txtarr: number[] = []; // array of characters and sub-mode switching characters - var codelen = code.length; - for (var i = 0; i < codelen; ++i) { - var chval = _ord(code.charAt(i)); - var k: number | string | false; - if ( - (k = _array_search(chval, textsubmodes[submode])) !== - false - ) { - // we are on the same sub-mode - txtarr.push(k as number); - } else { - // the sub-mode is changed - for (var s = 0; s < 4; ++s) { - // search new sub-mode - if ( - s != submode && - (k = _array_search(chval, textsubmodes[s])) !== - false - ) { - // s is the new submode - if ( - (i + 1 == codelen || - (i + 1 < codelen && - _array_search( - _ord(code.charAt(i + 1)), - textsubmodes[submode] - ) !== false)) && - (s == 3 || (s == 0 && submode == 1)) - ) { - // shift (temporary change only for this char) - if (s == 3) { - // shift to puntuaction - txtarr.push(29); - } else { - // shift from lower to alpha - txtarr.push(27); - } - } else { - // latch - txtarr = txtarr.concat(textlatch[("" + submode + s) as TextLatchKeys]); - // set new submode - submode = s; - } - // add characted code to array - txtarr.push(k as number); - break; - } - } - } - } - var txtarrlen = txtarr.length; - if (txtarrlen % 2 != 0) { - // add padding - txtarr.push(29); - ++txtarrlen; - } - // calculate codewords - for (var i = 0; i < txtarrlen; i += 2) { - cw.push(30 * parseInt(String(txtarr[i])) + parseInt(String(txtarr[i + 1]))); - } - break; - } - case 901: - case 924: { - // Byte Compaction mode latch - var rest: string; - var sublen: number; - var codelen: number; - while ((codelen = code.length) > 0) { - if (codelen > 6) { - rest = code.substring(6); - code = code.substring(0, 6); - sublen = 6; - } else { - rest = ""; - sublen = code.length; - } - if (sublen == 6) { - var t = bcmul("" + _ord(code.charAt(0)), "1099511627776"); - t = bcadd(t, bcmul("" + _ord(code.charAt(1)), "4294967296")); - t = bcadd(t, bcmul("" + _ord(code.charAt(2)), "16777216")); - t = bcadd(t, bcmul("" + _ord(code.charAt(3)), "65536")); - t = bcadd(t, bcmul("" + _ord(code.charAt(4)), "256")); - t = bcadd(t, "" + _ord(code.charAt(5))); - // tmp array for the 6 bytes block - var cw6: number[] = []; - do { - var d = _my_bcmod(t, "900"); - t = bcdiv(t, "900"); - // prepend the value to the beginning of the array - cw6.unshift(d); - } while (t != "0"); - // append the result array at the end - cw = cw.concat(cw6); - } else { - for (var i = 0; i < sublen; ++i) { - cw.push(_ord(code.charAt(i))); - } - } - code = rest; - } - break; - } - case 902: { - // Numeric Compaction mode latch - var rest: string; - var codelen: number; - while ((codelen = code.length) > 0) { - if (codelen > 44) { - rest = code.substring(44); - code = code.substring(0, 44); - } else { - rest = ""; - } - var t = "1" + code; - do { - var d = _my_bcmod(t, "900"); - t = bcdiv(t, "900"); - cw.unshift(d); - } while (t != "0"); - code = rest; - } - break; - } - case 913: { - // Byte Compaction mode shift - cw.push(_ord(code)); - break; - } - } - if (addmode) { - // add the compaction mode codeword at the beginning - cw.unshift(mode); - } - return cw; -}; - -function getErrorCorrectionLevel(ecl: number, numcw: number): number { - // get maximum correction level - var maxecl = 8; // starting error level - var maxerrsize = 928 - numcw; // available codewords for error - while (maxecl > 0) { - var errsize = 2 << ecl; - if (maxerrsize >= errsize) { - break; - } - --maxecl; - } - // check for automatic levels - if (ecl < 0 || ecl > 8) { - if (numcw < 41) { - ecl = 2; - } else if (numcw < 161) { - ecl = 3; - } else if (numcw < 321) { - ecl = 4; - } else if (numcw < 864) { - ecl = 5; - } else { - ecl = maxecl; - } - } - if (ecl > maxecl) { - ecl = maxecl; - } - return ecl; -}; - -function getErrorCorrection(cw: number[], ecl: number): number[] { - // get error correction coefficients - var ecc = rsfactors[ecl]; - // number of error correction factors - var eclsize = 2 << ecl; - // maximum index for rsfactors[ecl] - var eclmaxid = eclsize - 1; - // initialize array of error correction codewords - var ecw = _array_fill(0, eclsize, 0) as number[]; - // for each data codeword - for (var k = 0; k < cw.length; k++) { - var t1 = (cw[k] + ecw[eclmaxid]) % 929; - for (var j = eclmaxid; j > 0; --j) { - var t2 = t1 * ecc[j] % 929; - var t3 = 929 - t2; - ecw[j] = (ecw[j - 1] + t3) % 929; - } - t2 = t1 * ecc[0] % 929; - t3 = 929 - t2; - ecw[0] = t3 % 929; - } - for (var j = 0; j < ecw.length; j++) { - if (ecw[j] != 0) { - ecw[j] = 929 - ecw[j]; - } - } - ecw = ecw.reverse(); - return ecw; -}; - -/** - * - * Functions from phpjs.org - * - */ -function _array_fill(start_index: number, num: number, mixed_val: number): number[] | Record { - var key: number, - tmp_arr: Record = {}; - - if (start_index == 0) { - var tmpArray: number[] = []; - for (var i = 0; i < num; i++) { - tmpArray.push(mixed_val); - } - return tmpArray; - } - - if (!isNaN(start_index) && !isNaN(num)) { - for (key = 0; key < num; key++) { - tmp_arr[key + start_index] = mixed_val; - } - } - - return tmp_arr; -}; - -function _str_repeat(input: string, multiplier: number): string { - // http://kevin.vanzonneveld.net - // + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + improved by: Jonas Raoni Soares Silva (http://www.jsfromhell.com) - // + improved by: Ian Carter (http://euona.com/) - // * example 1: str_repeat('-=', 10); - // * returns 1: '-=-=-=-=-=-=-=-=-=-=' - var y = ""; - while (true) { - if (multiplier & 1) { - y += input; - } - multiplier >>= 1; - if (multiplier) { - input += input; - } else { - break; - } - } - return y; -}; - -function _intval(mixed_var: any, base?: number): number { - // http://kevin.vanzonneveld.net - // + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + improved by: stensi - // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + input by: Matteo - // + bugfixed by: Brett Zamir (http://brett-zamir.me) - // + bugfixed by: Rafał Kukawski (http://kukawski.pl) - // * example 1: intval('Kevin van Zonneveld'); - // * returns 1: 0 - // * example 2: intval(4.2); - // * returns 2: 4 - // * example 3: intval(42, 8); - // * returns 3: 42 - // * example 4: intval('09'); - // * returns 4: 9 - // * example 5: intval('1e', 16); - // * returns 5: 30 - var tmp: number; - - var type = typeof mixed_var; - - if (type === "boolean") { - return +mixed_var; - } else if (type === "string") { - tmp = parseInt(mixed_var, base || 10); - return isNaN(tmp) || !isFinite(tmp) ? 0 : tmp; - } else if (type === "number" && isFinite(mixed_var)) { - return mixed_var | 0; - } else { - return 0; - } -}; - -function _sprintf(format: string, ...args: any[]): string { - // http://kevin.vanzonneveld.net - // + original by: Ash Searle (http://hexmen.com/blog/) - // + namespaced by: Michael White (http://getsprink.com) - // + tweaked by: Jack - // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + input by: Paulo Freitas - // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + input by: Brett Zamir (http://brett-zamir.me) - // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + improved by: Dj - // + improved by: Allidylls - // * example 1: sprintf("%01.2f", 123.1); - // * returns 1: 123.10 - // * example 2: sprintf("[%10s]", 'monkey'); - // * returns 2: '[ monkey]' - // * example 3: sprintf("[%'#10s]", 'monkey'); - // * returns 3: '[####monkey]' - // * example 4: sprintf("%d", 123456789012345); - // * returns 4: '123456789012345' - var regex = /%%|%(\d+\$)?([-+\'#0 ]*)(\*\d+\$|\*|\d+)?(\.(\*\d+\$|\*|\d+))?([scboxXuideEfFgG])/g; - var a = [format, ...args], - i = 0; - format = a[i++]; - - // pad() - var pad = function (str: string, len: number, chr?: string, leftJustify?: boolean): string { - if (!chr) { - chr = " "; - } - var padding = str.length >= len - ? "" - : Array((1 + len - str.length) >>> 0).join(chr); - return leftJustify ? str + padding : padding + str; - }; - - // justify() - var justify = function ( - value: string, - prefix: string, - leftJustify: boolean, - minWidth: number, - zeroPad: boolean, - customPadChar?: string - ): string { - var diff = minWidth - value.length; - if (diff > 0) { - if (leftJustify || !zeroPad) { - value = pad(value, minWidth, customPadChar, leftJustify); - } else { - value = - value.slice(0, prefix.length) + - pad("", diff, "0", true) + - value.slice(prefix.length); - } - } - return value; - }; - - // formatBaseX() - var formatBaseX = function ( - value: number, - base: number, - prefix: string | boolean, - leftJustify: boolean, - minWidth: number, - precision: number, - zeroPad: boolean - ): string { - // Note: casts negative numbers to positive ones - var number = value >>> 0; - var prefixStr = - (prefix && - number && - ({ - "2": "0b", - "8": "0", - "16": "0x" - } as Record)[String(base)]) || - ""; - value = number; - var valueStr = prefixStr + pad(number.toString(base), precision || 0, "0", false); - return justify(valueStr, prefixStr, leftJustify, minWidth, zeroPad); - }; - - // formatString() - var formatString = function ( - value: string, - leftJustify: boolean, - minWidth: number, - precision: number | null, - zeroPad: boolean, - customPadChar?: string - ): string { - if (precision != null) { - value = value.slice(0, precision); - } - return justify( - value, - "", - leftJustify, - minWidth, - zeroPad, - customPadChar - ); - }; - - // doFormat() - var doFormat = function ( - substring: string, - valueIndex: string | undefined, - flags: string, - minWidth: string, - _: any, - precision: string, - type: string - ): string { - var number: number; - var prefix: string; - var method: string; - var textTransform: string; - var value: any; - - if (substring == "%%") { - return "%"; - } - - // parse flags - var leftJustify = false, - positivePrefix = "", - zeroPad = false, - prefixBaseX: string | boolean = false, - customPadChar = " "; - var flagsl = flags.length; - for (var j = 0; flags && j < flagsl; j++) { - switch (flags.charAt(j)) { - case " ": - positivePrefix = " "; - break; - case "+": - positivePrefix = "+"; - break; - case "-": - leftJustify = true; - break; - case "'": - customPadChar = flags.charAt(j + 1); - break; - case "0": - zeroPad = true; - break; - case "#": - prefixBaseX = true; - break; - } - } - - // parameters may be null, undefined, empty-string or real valued - // we want to ignore null, undefined and empty-string values - var minWidthNum = 0; - if (!minWidth) { - minWidthNum = 0; - } else if (minWidth == "*") { - minWidthNum = +a[i++]; - } else if (minWidth.charAt(0) == "*") { - minWidthNum = +a[parseInt(minWidth.slice(1, -1))]; - } else { - minWidthNum = +minWidth; - } - - // Note: undocumented perl feature: - if (minWidthNum < 0) { - minWidthNum = -minWidthNum; - leftJustify = true; - } - - if (!isFinite(minWidthNum)) { - throw new Error("sprintf: (minimum-)width must be finite"); - } - - var precisionNum: number | undefined; - if (!precision) { - precisionNum = "fFeE".indexOf(type) > -1 - ? 6 - : type == "d" ? 0 : undefined; - } else if (precision == "*") { - precisionNum = +a[i++]; - } else if (precision.charAt(0) == "*") { - precisionNum = +a[parseInt(precision.slice(1, -1))]; - } else { - precisionNum = +precision; - } - - // grab value using valueIndex if required? - value = valueIndex ? a[parseInt(valueIndex.slice(0, -1))] : a[i++]; - - switch (type) { - case "s": - return formatString( - String(value), - leftJustify, - minWidthNum, - precisionNum === undefined ? null : precisionNum, - zeroPad, - customPadChar - ); - case "c": - return formatString( - String.fromCharCode(+value), - leftJustify, - minWidthNum, - precisionNum === undefined ? null : precisionNum, - zeroPad - ); - case "b": - return formatBaseX( - value, - 2, - prefixBaseX, - leftJustify, - minWidthNum, - precisionNum || 0, - zeroPad - ); - case "o": - return formatBaseX( - value, - 8, - prefixBaseX, - leftJustify, - minWidthNum, - precisionNum || 0, - zeroPad - ); - case "x": - return formatBaseX( - value, - 16, - prefixBaseX, - leftJustify, - minWidthNum, - precisionNum || 0, - zeroPad - ); - case "X": - return formatBaseX( - value, - 16, - prefixBaseX, - leftJustify, - minWidthNum, - precisionNum || 0, - zeroPad - ).toUpperCase(); - case "u": - return formatBaseX( - value, - 10, - prefixBaseX, - leftJustify, - minWidthNum, - precisionNum || 0, - zeroPad - ); - case "i": - case "d": - number = +value || 0; - number = Math.round(number - number % 1); // Plain Math.round doesn't just truncate - prefix = number < 0 ? "-" : positivePrefix; - value = - prefix + pad(String(Math.abs(number)), precisionNum || 0, "0", false); - return justify(value, prefix, leftJustify, minWidthNum, zeroPad); - case "e": - case "E": - case "f": // Should handle locales (as per setlocale) - case "F": - case "g": - case "G": - number = +value; - prefix = number < 0 ? "-" : positivePrefix; - method = ["toExponential", "toFixed", "toPrecision"][ - "efg".indexOf(type.toLowerCase()) - ]; - textTransform = ["toString", "toUpperCase"][ - "eEfFgG".indexOf(type) % 2 - ]; - value = prefix + Math.abs(number)[method as 'toExponential' | 'toFixed' | 'toPrecision'](precisionNum); - return justify(value, prefix, leftJustify, minWidthNum, zeroPad)[ - textTransform as 'toString' | 'toUpperCase' - ](); - default: - return substring; - } - }; - - return format.replace(regex, doFormat as any); -}; - -// function _preg_split(pattern: RegExp | string, subject: string, limit?: number, flags?: string | number): (string | [string, number])[] { -// // http://kevin.vanzonneveld.net -// // + original by: Marco Marchiò -// // * example 1: preg_split(/[\s,]+/, 'hypertext language, programming'); -// // * returns 1: ['hypertext', 'language', 'programming'] -// // * example 2: preg_split('//', 'string', -1, 'PREG_SPLIT_NO_EMPTY'); -// // * returns 2: ['s', 't', 'r', 'i', 'n', 'g'] -// // * example 3: var str = 'hypertext language programming'; -// // * example 3: preg_split('/ /', str, -1, 'PREG_SPLIT_OFFSET_CAPTURE'); -// // * returns 3: [['hypertext', 0], ['language', 10], ['programming', 19]] -// // * example 4: preg_split('/( )/', '1 2 3 4 5 6 7 8', 4, 'PREG_SPLIT_DELIM_CAPTURE'); -// // * returns 4: ['1', ' ', '2', ' ', '3', ' ', '4 5 6 7 8'] -// // * example 5: preg_split('/( )/', '1 2 3 4 5 6 7 8', 4, (2 | 4)); -// // * returns 5: [['1', 0], [' ', 1], ['2', 2], [' ', 3], ['3', 4], [' ', 5], ['4 5 6 7 8', 6]] - -// limit = limit || 0; -// flags = flags || ""; // Limit and flags are optional - -// var result: RegExpExecArray | null, -// ret: (string | [string, number])[] = [], -// index = 0, -// i = 0, -// noEmpty = false, -// delim = false, -// offset = false, -// OPTS: Record = {}, -// optTemp = 0; - -// var patternRegex: RegExp; -// if (typeof pattern === 'string') { -// var regexpBody = /^\/(.*)\/\w*$/.exec(pattern)?.[1] || pattern; -// var regexpFlags = /^\/.*\/(\w*)$/.exec(pattern)?.[1] || ''; -// patternRegex = new RegExp(regexpBody, regexpFlags + (regexpFlags.indexOf("g") !== -1 ? "" : "g")); -// } else { -// var regexpBody = pattern.source; -// var regexpFlags = pattern.flags || ''; -// // Non-global regexp causes an infinite loop when executing the while, -// // so if it's not global, copy the regexp and add the "g" modifier. -// patternRegex = pattern.global -// ? pattern -// : new RegExp( -// regexpBody, -// regexpFlags + (regexpFlags.indexOf("g") !== -1 ? "" : "g") -// ); -// } - -// OPTS = { -// PREG_SPLIT_NO_EMPTY: 1, -// PREG_SPLIT_DELIM_CAPTURE: 2, -// PREG_SPLIT_OFFSET_CAPTURE: 4 -// }; -// if (typeof flags !== "number") { -// // Allow for a single string or an array of string flags -// var flagsArr = [].concat(flags as any); -// for (i = 0; i < flagsArr.length; i++) { -// // Resolve string input to bitwise e.g. 'PREG_SPLIT_OFFSET_CAPTURE' becomes 4 -// if (OPTS[flagsArr[i]]) { -// optTemp = optTemp | OPTS[flagsArr[i]]; -// } -// } -// flags = optTemp; -// } -// noEmpty = !!((flags as number) & OPTS.PREG_SPLIT_NO_EMPTY); -// delim = !!((flags as number) & OPTS.PREG_SPLIT_DELIM_CAPTURE); -// offset = !!((flags as number) & OPTS.PREG_SPLIT_OFFSET_CAPTURE); - -// var _filter = function (str: string, strindex: number): void { -// // If the match is empty and the PREG_SPLIT_NO_EMPTY flag is set don't add it -// if (noEmpty && !str.length) { -// return; -// } -// // If the PREG_SPLIT_OFFSET_CAPTURE flag is set -// // transform the match into an array and add the index at position 1 -// if (offset) { -// ret.push([str, strindex]); -// } else { -// ret.push(str); -// } -// }; -// // Special case for empty regexp -// if (!regexpBody) { -// var resultSplit = subject.split(""); -// for (i = 0; i < resultSplit.length; i++) { -// _filter(resultSplit[i], i); -// } -// return ret; -// } -// // Exec the pattern and get the result -// while ((result = patternRegex.exec(subject))) { -// // Stop if the limit is 1 -// if (limit === 1) { -// break; -// } -// // Take the correct portion of the string and filter the match -// _filter(subject.slice(index, result.index), index); -// index = result.index + result[0].length; -// // If the PREG_SPLIT_DELIM_CAPTURE flag is set, every capture match must be included in the results array -// if (delim) { -// // Convert the regexp result into a normal array -// var resarr = Array.prototype.slice.call(result); -// for (i = 1; i < resarr.length; i++) { -// if (result[i] !== undefined) { -// _filter(result[i], result.index + result[0].indexOf(result[i])); -// } -// } -// } -// limit--; -// } -// // Filter last match -// _filter(subject.slice(index, subject.length), index); -// return ret; -// }; - -function _ord(string: string): number { - return string.charCodeAt(0); -}; - -function _array_search(needle: any, haystack: any, argStrict?: boolean): string | number | false { - // http://kevin.vanzonneveld.net - // + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + input by: Brett Zamir (http://brett-zamir.me) - // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // * example 1: array_search('zonneveld', {firstname: 'kevin', middle: 'van', surname: 'zonneveld'}); - // * returns 1: 'surname' - // * example 2: ini_set('phpjs.return_phpjs_arrays', 'on'); - // * example 2: var ordered_arr = array({3:'value'}, {2:'value'}, {'a':'value'}, {'b':'value'}); - // * example 2: var key = array_search(/val/g, ordered_arr); // or var key = ordered_arr.search(/val/g); - // * returns 2: '3' - - var strict = !!argStrict, - key: string | number = ""; - - if ( - haystack && - typeof haystack === "object" && - (haystack as any).change_key_case - ) { - // Duck-type check for our own array()-created PHPJS_Array - return (haystack as any).search(needle, argStrict); - } - if (typeof needle === "object" && (needle as RegExp).exec) { - // Duck-type for RegExp - if (!strict) { - // Let's consider case sensitive searches as strict - var needleRegex = needle as RegExp; - var flags = - "i" + - (needleRegex.global ? "g" : "") + - (needleRegex.multiline ? "m" : "") + - ((needleRegex as any).sticky ? "y" : ""); // sticky is FF only - needle = new RegExp(needleRegex.source, flags); - } - for (key in haystack) { - if ((needle as RegExp).test(haystack[key])) { - return key; - } - } - return false; - } - - for (key in haystack) { - if ( - (strict && haystack[key] === needle) || - (!strict && haystack[key] == needle) - ) { - return key; - } - } - - return false; -}; - -function _my_bcmod(x: string, y: string): number { - // how many numbers to take at once? carefull not to exceed (int) - var take = 5; - var mod = ""; - do { - var a = parseInt(mod + "" + x.substring(0, take)); - x = x.substring(take); - mod = String(a % parseInt(y)); - } while (x.length); - - return parseInt(mod); -} - -/** - * Creates a PDF417 object - * @param code (string) code to represent using PDF417 - * @param ecl (int) error correction level (0-8); default -1 = automatic correction level - * @param aspectratio (float) the width to height of the symbol (excluding quiet zones) - */ -export function generateBarcode(code: string, ecl?: number, aspectratio?: number) { - - code = unescape(encodeURIComponent(code)); // covert UTF-8 to ISO-8859-1 - ecl = ecl || -1; - aspectratio = aspectratio || 2; - if (code === "") { - throw new Error("PDF417 init: empty input code"); - } - // get the input sequence array - let sequence = getInputSequences(code); - let codewords: number[] = []; // array of code-words - for (var i = 0; i < sequence.length; i++) { - var cw = getCompaction(sequence[i][0], sequence[i][1], true); - codewords = codewords.concat(cw); - } - if (codewords[0] == 900) { - // Text Alpha is the default mode, so remove the first code - codewords.shift(); - } - // count number of codewords - var numcw = codewords.length; - if (numcw > 925) { - // reached maximum data codeword capacity - throw new Error("PDF417 init: maximum data codeword capacity exceeded"); - } - - // set error correction level - ecl = getErrorCorrectionLevel(ecl, numcw); - // number of codewords for error correction - var errsize = 2 << ecl; - // calculate number of columns (number of codewords per row) and rows - var nce = numcw + errsize + 1; - var cols = Math.round( - (Math.sqrt(4761 + 68 * aspectratio * ROWHEIGHT * nce) - 69) / 34 - ); - // adjust cols - if (cols < 1) { - cols = 1; - } else if (cols > 30) { - cols = 30; - } - var rows = Math.ceil(nce / cols); - var size = cols * rows; - // adjust rows - if (rows < 3 || rows > 90) { - if (rows < 3) { - rows = 3; - } else if (rows > 90) { - rows = 90; - } - cols = Math.ceil(size / rows); - size = cols * rows; - } - if (size > 928) { - // set dimensions to get maximum capacity - if ( - Math.abs(aspectratio - 17 * 29 / 32) < - Math.abs(aspectratio - 17 * 16 / 58) - ) { - cols = 29; - rows = 32; - } else { - cols = 16; - rows = 58; - } - size = 928; - } - // calculate padding - var pad = size - nce; - if (pad > 0) { - if (size - rows == nce) { - --rows; - size -= rows; - } else { - // add pading - codewords = codewords.concat(_array_fill(0, pad, 900) as number[]); - } - } - - // Symbol Length Descriptor (number of data codewords including Symbol Length Descriptor and pad codewords) - var sld = size - errsize; - // add symbol length description - codewords.unshift(sld); - // calculate error correction - var ecw = getErrorCorrection(codewords, ecl); - // add error correction codewords - codewords = codewords.concat(ecw); - // add horizontal quiet zones to start and stop patterns - var pstart = _str_repeat("0", QUIETH) + start_pattern; - var pstop = stop_pattern + "" + _str_repeat("0", QUIETH); - - const barcode_array: BarcodeArray = { - num_rows: rows * ROWHEIGHT + 2 * QUIETV, - num_cols: (cols + 2) * 17 + 35 + 2 * QUIETH, - bcode: [] - }; - - var empty_row: number[] = []; - // build rows for vertical quiet zone - if (QUIETV > 0) { - empty_row = _array_fill(0, barcode_array.num_cols, 0) as number[]; - for (var i = 0; i < QUIETV; ++i) { - // add vertical quiet rows - barcode_array.bcode.push(empty_row); - } - } - - var L: number = 0; - var k = 0; // codeword index - var cid = 0; // initial cluster - // for each row - for (var r = 0; r < rows; ++r) { - // row start code - var row = pstart; - switch (cid) { - case 0: { - L = 30 * _intval(r / 3) + _intval((rows - 1) / 3); - break; - } - case 1: { - L = 30 * _intval(r / 3) + ecl * 3 + (rows - 1) % 3; - break; - } - case 2: { - L = 30 * _intval(r / 3) + (cols - 1); - break; - } - } - // left row indicator - row += _sprintf("%17b", clusters[cid][L]); - // for each column - for (var c = 0; c < cols; ++c) { - row += _sprintf("%17b", clusters[cid][codewords[k]]); - ++k; - } - switch (cid) { - case 0: { - L = 30 * _intval(r / 3) + (cols - 1); - break; - } - case 1: { - L = 30 * _intval(r / 3) + _intval((rows - 1) / 3); - break; - } - case 2: { - L = 30 * _intval(r / 3) + ecl * 3 + (rows - 1) % 3; - break; - } - } - // right row indicator - row += _sprintf("%17b", clusters[cid][L]); - // row stop code - row += pstop; - // convert the string to array - const arow = row.split(''); - // duplicate row to get the desired height - for (var h = 0; h < ROWHEIGHT; ++h) { - barcode_array.bcode.push(arow.map(x => parseInt(x))); - } - ++cid; - if (cid > 2) { - cid = 0; - } - } - if (QUIETV > 0) { - for (var i = 0; i < QUIETV; ++i) { - // add vertical quiet rows - barcode_array.bcode.push(empty_row); - } - } - - return (barcode_array); -}; - - diff --git a/app/lib/pdf/renderBarcode.ts b/app/lib/pdf/renderBarcode.ts deleted file mode 100644 index f614fd6..0000000 --- a/app/lib/pdf/renderBarcode.ts +++ /dev/null @@ -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: - * ``` - */ -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(); -} \ No newline at end of file diff --git a/app/ui/BillEditForm.tsx b/app/ui/BillEditForm.tsx index 3657ae8..7aa5dcc 100644 --- a/app/ui/BillEditForm.tsx +++ b/app/ui/BillEditForm.tsx @@ -7,10 +7,10 @@ import { useFormState } from "react-dom"; import { updateOrAddBill } from "../lib/actions/billActions"; import Link from "next/link"; 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 { Pdf417Barcode } from "./Pdf417Barcode"; 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 // This is a workaround for that @@ -50,22 +50,8 @@ export const BillEditForm: FC = ({ location, bill }) => { const [barcodeResults, setBarcodeResults] = React.useState | null>(null); useEffect(() => { - // migrating the legacy `barcodeImage` field to `hub3aText` - // by converting it to `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]); + console.log("[BillEditForm] hub3a text from DB:", bill?.hub3aText); + }, [bill?.hub3aText]); const billedTo_handleChange = (event: React.ChangeEvent) => { @@ -98,11 +84,15 @@ export const BillEditForm: FC = ({ location, bill }) => { setPayedAmount(`${billInfo.amount / 100}`); setHub3aText(hub3aText); + console.log("[BillEditForm] Single barcode result found:", hub3aText); } else { + console.log("[BillEditForm] Multiple barcode results found:", results); setPayedAmount(""); setBarcodeResults(results); setHub3aText(undefined); } + } else { + console.log("[BillEditForm] No barcode results found."); } setIsScanningPDF(false); @@ -213,7 +203,7 @@ export const BillEditForm: FC = ({ location, bill }) => { hub3aText ?

{t.rich('barcode-disclaimer', { br: () =>
})}

: null diff --git a/app/ui/Pdf417Barcode.tsx b/app/ui/Pdf417Barcode.tsx deleted file mode 100644 index 9a5d6b4..0000000 --- a/app/ui/Pdf417Barcode.tsx +++ /dev/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(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 ( -
- -
- ); - } - - return ( - // eslint-disable-next-line @next/next/no-img-element - PDF417 Barcode - ); -} \ No newline at end of file diff --git a/app/ui/Pdf417BarcodeWasm.tsx b/app/ui/Pdf417BarcodeWasm.tsx new file mode 100644 index 0000000..5adbb5e --- /dev/null +++ b/app/ui/Pdf417BarcodeWasm.tsx @@ -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(undefined); + const [error, setError] = useState(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 ( +
+ {error} +
+ ); + } + + // Don't render until barcode is generated (prevents hydration mismatch) + if (!barcodeDataUrl) { + return ( +
+ +
+ ); + } + + return ( + // eslint-disable-next-line @next/next/no-img-element + PDF417 Barcode + ); +} diff --git a/app/ui/PrintPreview.tsx b/app/ui/PrintPreview.tsx index 3de1f83..214d6a8 100644 --- a/app/ui/PrintPreview.tsx +++ b/app/ui/PrintPreview.tsx @@ -1,7 +1,7 @@ 'use client'; import { PrintBarcodeData } from '../lib/actions/printActions'; -import { Pdf417Barcode } from './Pdf417Barcode'; +import { Pdf417BarcodeWasm } from './Pdf417BarcodeWasm'; export interface PrintPreviewProps { data: PrintBarcodeData[]; @@ -132,21 +132,8 @@ export const PrintPreview: React.FC = ({ data, year, month, t
{ - item.hub3aText ? - - : ( - // LEGACY SUPPORT ... untill all bills have been migrated - - item.barcodeImage ? - // eslint-disable-next-line @next/next/no-img-element - {`Barcode : null - ) + item.hub3aText ? : null } -
diff --git a/app/ui/ViewBillCard.tsx b/app/ui/ViewBillCard.tsx index 8a6e1ef..8df55d2 100644 --- a/app/ui/ViewBillCard.tsx +++ b/app/ui/ViewBillCard.tsx @@ -7,8 +7,8 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { formatYearMonth } from "../lib/format"; import { useTranslations } from "next-intl"; -import { Pdf417Barcode } from "./Pdf417Barcode"; import { uploadProofOfPayment } from "../lib/actions/billActions"; +import { Pdf417BarcodeWasm } from "./Pdf417BarcodeWasm"; export interface ViewBillCardProps { location: BillingLocation; @@ -111,7 +111,7 @@ export const ViewBillCard: FC = ({ location, bill, shareId }) hub3aText ?

{t.rich('barcode-disclaimer', { br: () =>
})}

: null diff --git a/app/ui/ViewLocationCard.tsx b/app/ui/ViewLocationCard.tsx index e0fd0b0..21a12fc 100644 --- a/app/ui/ViewLocationCard.tsx +++ b/app/ui/ViewLocationCard.tsx @@ -7,13 +7,13 @@ import { formatCurrency, formatIban } from "../lib/formatStrings"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { ViewBillBadge } from "./ViewBillBadge"; -import { Pdf417Barcode } from "./Pdf417Barcode"; import { EncodePayment, PaymentParams } from "hub-3a-payment-encoder"; import Link from "next/link"; import { LinkIcon } from "@heroicons/react/24/outline"; import { uploadUtilBillsProofOfPayment } from "../lib/actions/locationActions"; import QRCode from "react-qr-code"; import { TicketIcon } from "@heroicons/react/24/solid"; +import { Pdf417BarcodeWasm } from "./Pdf417BarcodeWasm"; export interface ViewLocationCardProps { location: BillingLocation; @@ -153,7 +153,7 @@ export const ViewLocationCard: FC = ({ location, userSett
  • {t("payment-reference-label")}
    {paymentParams.PozivNaBroj}
  • : null diff --git a/middleware.ts b/middleware.ts index ec2ffdf..4fdfc6d 100644 --- a/middleware.ts +++ b/middleware.ts @@ -44,7 +44,8 @@ export default async function middleware(req: NextRequest) { export const config = { // for these paths middleware will not be called // `pdf.worker.min.mjs` is a web worker code used by pdf.js + // `*.wasm` files are WebAssembly modules used by zxing-wasm matcher: [ - '/((?!api|_next/static|_next/image|.*\\.png$|pdf.worker.min.mjs$|.*\\.webm$).*)', + '/((?!api|_next/static|_next/image|.*\\.png$|pdf.worker.min.mjs$|.*\\.wasm$|.*\\.webm$).*)', ], }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e6d5596..34b90a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "evidencija-rezija", - "version": "2.15.0", + "version": "2.16.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "version": "2.15.0", + "version": "2.16.0", "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", @@ -15,8 +15,6 @@ "@tailwindcss/typography": "^0.5.10", "@types/iban": "^0.0.35", "@types/node": "20.5.7", - "@zxing/browser": "^0.1.4", - "@zxing/library": "^0.20.0", "autoprefixer": "10.4.15", "bcrypt": "^5.1.1", "clsx": "^2.0.0", @@ -39,7 +37,8 @@ "tailwindcss": "^3.4.0", "typescript": "5.2.2", "use-debounce": "^10.0.0", - "zod": "^3.22.2" + "zod": "^3.22.2", + "zxing-wasm": "^2.2.4" }, "devDependencies": { "@types/bcrypt": "^5.0.1", @@ -1749,6 +1748,12 @@ "@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": { "version": "0.0.35", "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": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -8016,6 +7989,18 @@ "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": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", @@ -8172,14 +8157,6 @@ "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": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -8725,6 +8702,34 @@ "funding": { "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" + } } } } diff --git a/package.json b/package.json index 8c83e82..7bfddf7 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,6 @@ "@tailwindcss/typography": "^0.5.10", "@types/iban": "^0.0.35", "@types/node": "20.5.7", - "@zxing/browser": "^0.1.4", - "@zxing/library": "^0.20.0", "autoprefixer": "10.4.15", "bcrypt": "^5.1.1", "clsx": "^2.0.0", @@ -41,7 +39,8 @@ "tailwindcss": "^3.4.0", "typescript": "5.2.2", "use-debounce": "^10.0.0", - "zod": "^3.22.2" + "zod": "^3.22.2", + "zxing-wasm": "^2.2.4" }, "devDependencies": { "@types/bcrypt": "^5.0.1", @@ -59,5 +58,5 @@ "engines": { "node": ">=18.17.0" }, - "version": "2.15.0" + "version": "2.16.0" } diff --git a/public/zxing_reader.wasm b/public/zxing_reader.wasm new file mode 100644 index 0000000..112f8ca Binary files /dev/null and b/public/zxing_reader.wasm differ diff --git a/public/zxing_writer.wasm b/public/zxing_writer.wasm new file mode 100644 index 0000000..523e694 Binary files /dev/null and b/public/zxing_writer.wasm differ