From e8563a1392fea32d5509c4313d67abfeb139bda9 Mon Sep 17 00:00:00 2001 From: alam00000 Date: Sun, 1 Mar 2026 19:48:34 +0530 Subject: [PATCH] feat: add barcode field support in form creator and support to edit existing form fields --- package-lock.json | 14 +- package.json | 1 + src/js/logic/form-creator.ts | 414 +++++++++++++++++++++++++++++- src/js/types/form-creator-type.ts | 92 ++++--- src/pages/form-creator.html | 10 + 5 files changed, 487 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8aa2221..4b539ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bento-pdf", - "version": "2.2.1", + "version": "2.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bento-pdf", - "version": "2.2.1", + "version": "2.3.3", "license": "AGPL-3.0-only", "dependencies": { "@fontsource/cedarville-cursive": "^5.2.7", @@ -28,6 +28,7 @@ "@types/papaparse": "^5.5.2", "archiver": "^7.0.1", "blob-stream": "^0.1.3", + "bwip-js": "^4.8.0", "cropperjs": "^1.6.1", "embedpdf-snippet": "file:vendor/embedpdf/embedpdf-snippet-2.3.0.tgz", "heic2any": "^0.0.4", @@ -5150,6 +5151,15 @@ "dev": true, "license": "MIT" }, + "node_modules/bwip-js": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/bwip-js/-/bwip-js-4.8.0.tgz", + "integrity": "sha512-gUDkDHSTv8/DJhomSIbO0fX/Dx0MO/sgllLxJyJfu4WixCQe9nfGJzmHm64ZCbxo+gUYQEsQcRmqcwcwPRwUkg==", + "license": "MIT", + "bin": { + "bwip-js": "bin/bwip-js.js" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", diff --git a/package.json b/package.json index de4c831..3ba7584 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "@types/papaparse": "^5.5.2", "archiver": "^7.0.1", "blob-stream": "^0.1.3", + "bwip-js": "^4.8.0", "cropperjs": "^1.6.1", "embedpdf-snippet": "file:vendor/embedpdf/embedpdf-snippet-2.3.0.tgz", "heic2any": "^0.0.4", diff --git a/src/js/logic/form-creator.ts b/src/js/logic/form-creator.ts index b914b69..cd9c64e 100644 --- a/src/js/logic/form-creator.ts +++ b/src/js/logic/form-creator.ts @@ -10,11 +10,18 @@ import { PDFDict, PDFArray, PDFRadioGroup, + PDFTextField, + PDFCheckBox, + PDFDropdown, + PDFOptionList, + PDFButton, + PDFSignature, } from 'pdf-lib'; import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'; import { downloadFile, hexToRgb, getPDFDocument } from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; import * as pdfjsLib from 'pdfjs-dist'; +import * as bwipjs from 'bwip-js/browser'; import 'pdfjs-dist/web/pdf_viewer.css'; // Initialize PDF.js worker @@ -33,6 +40,7 @@ const existingRadioGroups: Set = new Set(); let draggedElement: HTMLElement | null = null; let offsetX = 0; let offsetY = 0; +let pendingFieldExtraction = false; let pages: PageData[] = []; let currentPageIndex = 0; @@ -384,8 +392,18 @@ function createField(type: FormField['type'], x: number, y: number): void { type: type, x: Math.max(0, Math.min(x, 816 - 150)), y: Math.max(0, Math.min(y, 1056 - 30)), - width: type === 'checkbox' || type === 'radio' ? 30 : 150, - height: type === 'checkbox' || type === 'radio' ? 30 : 30, + width: + type === 'checkbox' || type === 'radio' + ? 30 + : type === 'barcode' + ? 150 + : 150, + height: + type === 'checkbox' || type === 'radio' + ? 30 + : type === 'barcode' + ? 150 + : 30, name: `${type.charAt(0).toUpperCase() + type.slice(1)}_${fieldCounter}`, defaultValue: '', fontSize: 12, @@ -417,6 +435,8 @@ function createField(type: FormField['type'], x: number, y: number): void { multiline: type === 'text' ? false : undefined, borderColor: '#000000', hideBorder: false, + barcodeFormat: type === 'barcode' ? 'qrcode' : undefined, + barcodeValue: type === 'barcode' ? 'https://example.com' : undefined, }; fields.push(field); @@ -575,6 +595,32 @@ function renderField(field: FormField): void { 'w-full h-full flex items-center justify-center bg-gray-100 text-gray-500 border border-gray-300'; contentEl.innerHTML = `
${field.label || 'Click to Upload Image'}
`; setTimeout(() => (window as any).lucide?.createIcons(), 0); + } else if (field.type === 'barcode') { + contentEl.className = + 'w-full h-full flex items-center justify-center bg-white'; + if (field.barcodeValue) { + try { + const offscreen = document.createElement('canvas'); + bwipjs.toCanvas(offscreen, { + bcid: field.barcodeFormat || 'qrcode', + text: field.barcodeValue, + scale: 2, + includetext: + field.barcodeFormat !== 'qrcode' && + field.barcodeFormat !== 'datamatrix', + }); + const img = document.createElement('img'); + img.src = offscreen.toDataURL('image/png'); + img.className = 'max-w-full max-h-full object-contain'; + contentEl.appendChild(img); + } catch { + contentEl.innerHTML = `
Invalid data
`; + setTimeout(() => (window as any).lucide?.createIcons(), 0); + } + } else { + contentEl.innerHTML = `
Barcode
`; + setTimeout(() => (window as any).lucide?.createIcons(), 0); + } } fieldContainer.appendChild(contentEl); @@ -1114,6 +1160,26 @@ function showProperties(field: FormField): void { Clicking this field in the PDF will open a file picker to upload an image. `; + } else if (field.type === 'barcode') { + specificProps = ` +
+ + +
+
+ + +
+
+ `; } propertiesPanel.innerHTML = ` @@ -1795,6 +1861,56 @@ function showProperties(field: FormField): void { field.label = (e.target as HTMLInputElement).value; renderField(field); }); + } else if (field.type === 'barcode') { + const propBarcodeFormat = document.getElementById( + 'propBarcodeFormat' + ) as HTMLSelectElement; + const propBarcodeValue = document.getElementById( + 'propBarcodeValue' + ) as HTMLInputElement; + + const barcodeSampleValues: Record = { + qrcode: 'https://example.com', + code128: 'ABC-123', + code39: 'ABC123', + ean13: '590123412345', + upca: '01234567890', + datamatrix: 'https://example.com', + pdf417: 'https://example.com', + }; + + const barcodeFormatHints: Record = { + qrcode: 'Any text, URL, or data', + code128: 'ASCII characters (letters, numbers, symbols)', + code39: 'Uppercase A-Z, digits 0-9, and - . $ / + % SPACE', + ean13: 'Exactly 12 or 13 digits', + upca: 'Exactly 11 or 12 digits', + datamatrix: 'Any text, URL, or data', + pdf417: 'Any text, URL, or data', + }; + + const hintEl = document.getElementById('barcodeFormatHint'); + if (hintEl) + hintEl.textContent = + barcodeFormatHints[field.barcodeFormat || 'qrcode'] || ''; + + if (propBarcodeFormat) { + propBarcodeFormat.addEventListener('change', (e) => { + const newFormat = (e.target as HTMLSelectElement).value; + field.barcodeFormat = newFormat; + field.barcodeValue = barcodeSampleValues[newFormat] || 'hello'; + if (propBarcodeValue) propBarcodeValue.value = field.barcodeValue; + if (hintEl) hintEl.textContent = barcodeFormatHints[newFormat] || ''; + renderField(field); + }); + } + + if (propBarcodeValue) { + propBarcodeValue.addEventListener('input', (e) => { + field.barcodeValue = (e.target as HTMLInputElement).value; + renderField(field); + }); + } } } @@ -1911,6 +2027,19 @@ downloadBtn.addEventListener('click', async () => { const form = pdfDoc.getForm(); + if (extractedFieldNames.size > 0) { + for (const fieldName of extractedFieldNames) { + try { + const existingField = form.getFieldMaybe(fieldName); + if (existingField) { + form.removeField(existingField); + } + } catch (e) { + console.warn(`Failed to remove existing field "${fieldName}":`, e); + } + } + } + const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica); // Set document metadata for accessibility @@ -2332,6 +2461,32 @@ downloadBtn.addEventListener('click', async () => { if (field.tooltip) { widgetDict.set(PDFName.of('TU'), PDFString.of(field.tooltip)); } + } else if (field.type === 'barcode') { + if (field.barcodeValue) { + try { + const offscreen = document.createElement('canvas'); + bwipjs.toCanvas(offscreen, { + bcid: field.barcodeFormat || 'qrcode', + text: field.barcodeValue, + scale: 3, + includetext: + field.barcodeFormat !== 'qrcode' && + field.barcodeFormat !== 'datamatrix', + }); + const dataUrl = offscreen.toDataURL('image/png'); + const base64 = dataUrl.split(',')[1]; + const pngBytes = Uint8Array.from(atob(base64), (c) => + c.charCodeAt(0) + ); + const pngImage = await pdfDoc.embedPng(pngBytes); + pdfPage.drawImage(pngImage, { x, y, width, height }); + } catch (e) { + console.warn( + `Failed to generate barcode for field "${field.name}":`, + e + ); + } + } } } @@ -2429,6 +2584,8 @@ function resetToInitial(): void { currentPageIndex = 0; uploadedPdfDoc = null; selectedField = null; + extractedFieldNames.clear(); + pendingFieldExtraction = false; canvas.innerHTML = ''; @@ -2611,6 +2768,27 @@ async function renderCanvas(): Promise { height: currentPage.height, }, }); + + if (pendingFieldExtraction && uploadedPdfDoc) { + pendingFieldExtraction = false; + extractExistingFields(uploadedPdfDoc); + extractedFieldNames.forEach((name) => + existingFieldNames.delete(name) + ); + + try { + const form = uploadedPdfDoc.getForm(); + for (const name of extractedFieldNames) { + try { + const f = form.getFieldMaybe(name); + if (f) form.removeField(f); + } catch {} + } + } catch {} + + renderCanvas(); + updateFieldCount(); + } }, 50); } } @@ -2701,6 +2879,235 @@ confirmBlankBtn.addEventListener('click', () => { setTimeout(() => createIcons({ icons }), 100); }); +const extractedFieldNames: Set = new Set(); + +function extractExistingFields(pdfDoc: PDFDocument): void { + try { + const form = pdfDoc.getForm(); + const pdfFields = form.getFields(); + const pdfPages = pdfDoc.getPages(); + + const pageRefToIndex = new Map(); + pdfPages.forEach((page, index) => { + pageRefToIndex.set(page.ref, index); + }); + + for (const pdfField of pdfFields) { + const name = pdfField.getName(); + const widgets = pdfField.acroField.getWidgets(); + + if (widgets.length === 0) continue; + + let fieldType: FormField['type']; + if (pdfField instanceof PDFTextField) { + fieldType = 'text'; + } else if (pdfField instanceof PDFCheckBox) { + fieldType = 'checkbox'; + } else if (pdfField instanceof PDFRadioGroup) { + fieldType = 'radio'; + } else if (pdfField instanceof PDFDropdown) { + fieldType = 'dropdown'; + } else if (pdfField instanceof PDFOptionList) { + fieldType = 'optionlist'; + } else if (pdfField instanceof PDFButton) { + fieldType = 'button'; + } else if (pdfField instanceof PDFSignature) { + fieldType = 'signature'; + } else { + continue; + } + + if (fieldType === 'radio') { + const radioField = pdfField as PDFRadioGroup; + const options = radioField.getOptions(); + + for (let wi = 0; wi < widgets.length; wi++) { + const widget = widgets[wi]; + const rect = widget.getRectangle(); + + const pageRef = widget.dict.get(PDFName.of('P')); + let pageIndex = 0; + if (pageRef) { + for (let pi = 0; pi < pdfPages.length; pi++) { + if (pdfPages[pi].ref === pageRef) { + pageIndex = pi; + break; + } + } + } + + const page = pdfPages[pageIndex]; + const { height: pageHeight } = page.getSize(); + + const canvasX = rect.x * pdfViewerScale + pdfViewerOffset.x; + const canvasY = + (pageHeight - rect.y - rect.height) * pdfViewerScale + + pdfViewerOffset.y; + const canvasWidth = rect.width * pdfViewerScale; + const canvasHeight = rect.height * pdfViewerScale; + + fieldCounter++; + const exportValue = options[wi] || 'Yes'; + + let tooltip = ''; + try { + const tu = widget.dict.get(PDFName.of('TU')); + if (tu instanceof PDFString) { + tooltip = tu.decodeText(); + } + } catch {} + + const formField: FormField = { + id: `field_${fieldCounter}`, + type: 'radio', + x: canvasX, + y: canvasY, + width: canvasWidth, + height: canvasHeight, + name: name, + defaultValue: '', + fontSize: 12, + alignment: 'left', + textColor: '#000000', + required: radioField.isRequired(), + readOnly: radioField.isReadOnly(), + tooltip: tooltip, + combCells: 0, + maxLength: 0, + checked: false, + exportValue: exportValue, + groupName: name, + pageIndex: pageIndex, + borderColor: '#000000', + hideBorder: false, + }; + + fields.push(formField); + } + + extractedFieldNames.add(name); + continue; + } + + const widget = widgets[0]; + const rect = widget.getRectangle(); + + const pageRef = widget.dict.get(PDFName.of('P')); + let pageIndex = 0; + if (pageRef) { + for (let pi = 0; pi < pdfPages.length; pi++) { + if (pdfPages[pi].ref === pageRef) { + pageIndex = pi; + break; + } + } + } + + const page = pdfPages[pageIndex]; + const { height: pageHeight } = page.getSize(); + + const canvasX = rect.x * pdfViewerScale + pdfViewerOffset.x; + const canvasY = + (pageHeight - rect.y - rect.height) * pdfViewerScale + + pdfViewerOffset.y; + const canvasWidth = rect.width * pdfViewerScale; + const canvasHeight = rect.height * pdfViewerScale; + + let tooltip = ''; + try { + const tu = widget.dict.get(PDFName.of('TU')); + if (tu instanceof PDFString) { + tooltip = tu.decodeText(); + } + } catch {} + + fieldCounter++; + + const formField: FormField = { + id: `field_${fieldCounter}`, + type: fieldType, + x: canvasX, + y: canvasY, + width: canvasWidth, + height: canvasHeight, + name: name, + defaultValue: '', + fontSize: 12, + alignment: 'left', + textColor: '#000000', + required: pdfField.isRequired(), + readOnly: pdfField.isReadOnly(), + tooltip: tooltip, + combCells: 0, + maxLength: 0, + pageIndex: pageIndex, + borderColor: '#000000', + hideBorder: false, + }; + + if (pdfField instanceof PDFTextField) { + try { + formField.defaultValue = pdfField.getText() || ''; + } catch {} + try { + formField.multiline = pdfField.isMultiline(); + } catch {} + try { + const maxLen = pdfField.getMaxLength(); + if (maxLen !== undefined) { + if (pdfField.isCombed()) { + formField.combCells = maxLen; + } else { + formField.maxLength = maxLen; + } + } + } catch {} + try { + const alignment = pdfField.getAlignment(); + if (alignment === TextAlignment.Center) + formField.alignment = 'center'; + else if (alignment === TextAlignment.Right) + formField.alignment = 'right'; + else formField.alignment = 'left'; + } catch {} + } else if (pdfField instanceof PDFCheckBox) { + try { + formField.checked = pdfField.isChecked(); + } catch {} + formField.exportValue = 'Yes'; + } else if (pdfField instanceof PDFDropdown) { + try { + formField.options = pdfField.getOptions(); + } catch {} + try { + const selected = pdfField.getSelected(); + if (selected.length > 0) formField.defaultValue = selected[0]; + } catch {} + } else if (pdfField instanceof PDFOptionList) { + try { + formField.options = pdfField.getOptions(); + } catch {} + try { + const selected = pdfField.getSelected(); + if (selected.length > 0) formField.defaultValue = selected[0]; + } catch {} + } else if (pdfField instanceof PDFButton) { + formField.label = 'Button'; + formField.action = 'none'; + } + + fields.push(formField); + extractedFieldNames.add(name); + } + + console.log( + `Extracted ${extractedFieldNames.size} existing fields for editing` + ); + } catch (e) { + console.warn('Error extracting existing fields:', e); + } +} + async function handlePdfUpload(file: File) { try { const arrayBuffer = await file.arrayBuffer(); @@ -2759,6 +3166,9 @@ async function handlePdfUpload(file: File) { } currentPageIndex = 0; + + pendingFieldExtraction = true; + renderCanvas(); updatePageNavigation(); diff --git a/src/js/types/form-creator-type.ts b/src/js/types/form-creator-type.ts index f0f7c5b..58460a1 100644 --- a/src/js/types/form-creator-type.ts +++ b/src/js/types/form-creator-type.ts @@ -1,40 +1,52 @@ -export interface FormField { - id: string - type: 'text' | 'checkbox' | 'radio' | 'dropdown' | 'optionlist' | 'button' | 'signature' | 'date' | 'image' - x: number - y: number - width: number - height: number - name: string - defaultValue: string - fontSize: number - alignment: 'left' | 'center' | 'right' - textColor: string - required: boolean - readOnly: boolean - tooltip: string - combCells: number - maxLength: number - options?: string[] - checked?: boolean - exportValue?: string - groupName?: string - label?: string - pageIndex: number - action?: 'none' | 'reset' | 'print' | 'url' | 'js' | 'showHide' - actionUrl?: string - jsScript?: string - targetFieldName?: string - visibilityAction?: 'show' | 'hide' | 'toggle' - dateFormat?: string - multiline?: boolean - borderColor?: string - hideBorder?: boolean -} - -export interface PageData { - index: number - width: number - height: number - pdfPageData?: string -} +export interface FormField { + id: string; + type: + | 'text' + | 'checkbox' + | 'radio' + | 'dropdown' + | 'optionlist' + | 'button' + | 'signature' + | 'date' + | 'image' + | 'barcode'; + x: number; + y: number; + width: number; + height: number; + name: string; + defaultValue: string; + fontSize: number; + alignment: 'left' | 'center' | 'right'; + textColor: string; + required: boolean; + readOnly: boolean; + tooltip: string; + combCells: number; + maxLength: number; + options?: string[]; + checked?: boolean; + exportValue?: string; + groupName?: string; + label?: string; + pageIndex: number; + action?: 'none' | 'reset' | 'print' | 'url' | 'js' | 'showHide'; + actionUrl?: string; + jsScript?: string; + targetFieldName?: string; + visibilityAction?: 'show' | 'hide' | 'toggle'; + dateFormat?: string; + multiline?: boolean; + borderColor?: string; + hideBorder?: boolean; + barcodeFormat?: string; + barcodeValue?: string; +} + +export interface PageData { + index: number; + width: number; + height: number; + pdfPageData?: string; +} diff --git a/src/pages/form-creator.html b/src/pages/form-creator.html index c6caf69..4e05bf4 100644 --- a/src/pages/form-creator.html +++ b/src/pages/form-creator.html @@ -504,6 +504,16 @@ Image + +
+ + Barcode +