From 649aec046d2967c7b3332146ee312281f76456a5 Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Wed, 3 Dec 2025 23:13:14 +0530 Subject: [PATCH 01/21] feat(ocr,form-creator): Add comprehensive font support and TypeScript type definitions - Add @pdf-lib/fontkit dependency for enhanced font rendering capabilities - Create font-mappings.ts configuration with language-to-font-family mappings for 100+ languages - Implement font-loader.ts utility for dynamic font loading from CDN sources - Add TypeScript type definitions for form-creator, OCR, and general application types - Create types/index.ts as centralized type exports - Remove hidden-on-touch CSS class and update shortcuts button styling for better accessibility - Update OCR text layer rendering to support multilingual font families - Enhance form-creator with improved font handling for international text - Update txt-to-pdf with font support for diverse character sets - Migrate fileHandler to support new font loading workflow - Update main.ts and ui.ts to integrate new type system and font utilities - Update form-creator.html page with enhanced font configuration UI --- index.html | 2 +- package-lock.json | 14 +- package.json | 1 + src/css/styles.css | 14 -- src/js/config/font-mappings.ts | 189 ++++++++++++++++ src/js/handlers/fileHandler.ts | 8 +- src/js/logic/form-creator.ts | 235 +++++++++++++++----- src/js/logic/ocr-pdf.ts | 148 +++++++++---- src/js/logic/txt-to-pdf.ts | 388 ++++++++++++++++++++++++--------- src/js/main.ts | 51 +++-- src/js/types/form-creator.ts | 40 ++++ src/js/types/index.ts | 2 + src/js/types/ocr.ts | 10 + src/js/ui.ts | 57 ++++- src/js/utils/font-loader.ts | 281 ++++++++++++++++++++++++ src/pages/form-creator.html | 21 ++ 16 files changed, 1220 insertions(+), 241 deletions(-) create mode 100644 src/js/config/font-mappings.ts create mode 100644 src/js/types/form-creator.ts create mode 100644 src/js/types/index.ts create mode 100644 src/js/types/ocr.ts create mode 100644 src/js/utils/font-loader.ts diff --git a/index.html b/index.html index f63bbc8..3dcd988 100644 --- a/index.html +++ b/index.html @@ -280,7 +280,7 @@ diff --git a/package-lock.json b/package-lock.json index ac62519..5721625 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bento-pdf", - "version": "1.7.3", + "version": "1.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bento-pdf", - "version": "1.7.3", + "version": "1.10.0", "license": "Apache-2.0", "dependencies": { "@fontsource/cedarville-cursive": "^5.2.7", @@ -17,6 +17,7 @@ "@fontsource/lato": "^5.2.7", "@fontsource/merriweather": "^5.2.11", "@neslinesli93/qpdf-wasm": "^0.3.0", + "@pdf-lib/fontkit": "^1.1.1", "@tailwindcss/vite": "^4.1.15", "archiver": "^7.0.1", "blob-stream": "^0.1.3", @@ -3088,6 +3089,15 @@ "node": ">= 8" } }, + "node_modules/@pdf-lib/fontkit": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/fontkit/-/fontkit-1.1.1.tgz", + "integrity": "sha512-KjMd7grNapIWS/Dm0gvfHEilSyAmeLvrEGVcqLGi0VYebuqqzTbgF29efCx7tvx+IEbG3zQciRSWl3GkUSvjZg==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, "node_modules/@pdf-lib/standard-fonts": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", diff --git a/package.json b/package.json index 37e6cd1..89bbb90 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@fontsource/lato": "^5.2.7", "@fontsource/merriweather": "^5.2.11", "@neslinesli93/qpdf-wasm": "^0.3.0", + "@pdf-lib/fontkit": "^1.1.1", "@tailwindcss/vite": "^4.1.15", "archiver": "^7.0.1", "blob-stream": "^0.1.3", diff --git a/src/css/styles.css b/src/css/styles.css index f8de956..cb8ee6b 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -568,20 +568,6 @@ button:disabled, background: #6b7280; } -/* Hide elements on touch devices */ -@media (pointer: coarse) { - .hidden-on-touch { - display: none !important; - } -} - -/* Also hide on very small screens just in case */ -@media (max-width: 640px) { - .hidden-on-touch { - display: none !important; - } -} - /* Scroll to Top Button Visibility */ #scroll-to-top-btn.visible { opacity: 1; diff --git a/src/js/config/font-mappings.ts b/src/js/config/font-mappings.ts new file mode 100644 index 0000000..c6c0c31 --- /dev/null +++ b/src/js/config/font-mappings.ts @@ -0,0 +1,189 @@ +/** + * Font mappings for OCR text layer rendering + * Maps Tesseract language codes to appropriate Noto Sans font families and their CDN URLs + */ + +export const languageToFontFamily: Record = { + // CJK Languages + jpn: 'Noto Sans JP', + chi_sim: 'Noto Sans SC', + chi_tra: 'Noto Sans TC', + kor: 'Noto Sans KR', + + // Arabic Script + ara: 'Noto Sans Arabic', + fas: 'Noto Sans Arabic', + urd: 'Noto Sans Arabic', + pus: 'Noto Sans Arabic', + kur: 'Noto Sans Arabic', + + // Devanagari Script + hin: 'Noto Sans Devanagari', + mar: 'Noto Sans Devanagari', + san: 'Noto Sans Devanagari', + nep: 'Noto Sans Devanagari', + + // Bengali Script + ben: 'Noto Sans Bengali', + asm: 'Noto Sans Bengali', + + // Tamil Script + tam: 'Noto Sans Tamil', + + // Telugu Script + tel: 'Noto Sans Telugu', + + // Kannada Script + kan: 'Noto Sans Kannada', + + // Malayalam Script + mal: 'Noto Sans Malayalam', + + // Gujarati Script + guj: 'Noto Sans Gujarati', + + // Gurmukhi Script (Punjabi) + pan: 'Noto Sans Gurmukhi', + + // Oriya Script + ori: 'Noto Sans Oriya', + + // Sinhala Script + sin: 'Noto Sans Sinhala', + + // Thai Script + tha: 'Noto Sans Thai', + + // Lao Script + lao: 'Noto Sans Lao', + + // Khmer Script + khm: 'Noto Sans Khmer', + + // Myanmar Script + mya: 'Noto Sans Myanmar', + + // Tibetan Script + bod: 'Noto Serif Tibetan', + + // Georgian Script + kat: 'Noto Sans Georgian', + kat_old: 'Noto Sans Georgian', + + // Armenian Script + hye: 'Noto Sans Armenian', + + // Hebrew Script + heb: 'Noto Sans Hebrew', + yid: 'Noto Sans Hebrew', + + // Ethiopic Script + amh: 'Noto Sans Ethiopic', + tir: 'Noto Sans Ethiopic', + + // Cherokee Script + chr: 'Noto Sans Cherokee', + + // Syriac Script + syr: 'Noto Sans Syriac', + + // Cyrillic Script (Noto Sans includes Cyrillic) + bel: 'Noto Sans', + bul: 'Noto Sans', + mkd: 'Noto Sans', + rus: 'Noto Sans', + srp: 'Noto Sans', + srp_latn: 'Noto Sans', + ukr: 'Noto Sans', + kaz: 'Noto Sans', + kir: 'Noto Sans', + tgk: 'Noto Sans', + uzb: 'Noto Sans', + uzb_cyrl: 'Noto Sans', + aze_cyrl: 'Noto Sans', + + // Latin Script (covered by base Noto Sans) + afr: 'Noto Sans', + aze: 'Noto Sans', + bos: 'Noto Sans', + cat: 'Noto Sans', + ceb: 'Noto Sans', + ces: 'Noto Sans', + cym: 'Noto Sans', + dan: 'Noto Sans', + deu: 'Noto Sans', + ell: 'Noto Sans', + eng: 'Noto Sans', + enm: 'Noto Sans', + epo: 'Noto Sans', + est: 'Noto Sans', + eus: 'Noto Sans', + fin: 'Noto Sans', + fra: 'Noto Sans', + frk: 'Noto Sans', + frm: 'Noto Sans', + gle: 'Noto Sans', + glg: 'Noto Sans', + grc: 'Noto Sans', + hat: 'Noto Sans', + hrv: 'Noto Sans', + hun: 'Noto Sans', + iku: 'Noto Sans', + ind: 'Noto Sans', + isl: 'Noto Sans', + ita: 'Noto Sans', + ita_old: 'Noto Sans', + jav: 'Noto Sans', + lat: 'Noto Sans', + lav: 'Noto Sans', + lit: 'Noto Sans', + mlt: 'Noto Sans', + msa: 'Noto Sans', + nld: 'Noto Sans', + nor: 'Noto Sans', + pol: 'Noto Sans', + por: 'Noto Sans', + ron: 'Noto Sans', + slk: 'Noto Sans', + slv: 'Noto Sans', + spa: 'Noto Sans', + spa_old: 'Noto Sans', + sqi: 'Noto Sans', + swa: 'Noto Sans', + swe: 'Noto Sans', + tgl: 'Noto Sans', + tur: 'Noto Sans', + vie: 'Noto Sans', + dzo: 'Noto Sans', + uig: 'Noto Sans', +}; + +export const fontFamilyToUrl: Record = { + 'Noto Sans JP': 'https://raw.githack.com/googlefonts/noto-cjk/main/Sans/OTF/Japanese/NotoSansCJKjp-Regular.otf', + 'Noto Sans SC': 'https://raw.githack.com/googlefonts/noto-cjk/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf', + 'Noto Sans TC': 'https://raw.githack.com/googlefonts/noto-cjk/main/Sans/OTF/TraditionalChinese/NotoSansCJKtc-Regular.otf', + 'Noto Sans KR': 'https://raw.githack.com/googlefonts/noto-cjk/main/Sans/OTF/Korean/NotoSansCJKkr-Regular.otf', + 'Noto Sans Arabic': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansArabic/NotoSansArabic-Regular.ttf', + 'Noto Sans Devanagari': 'https://raw.githack.com/googlefonts/noto-fonts/main/unhinted/ttf/NotoSansDevanagari/NotoSansDevanagari-Regular.ttf', + 'Noto Sans Bengali': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansBengali/NotoSansBengali-Regular.ttf', + 'Noto Sans Gujarati': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansGujarati/NotoSansGujarati-Regular.ttf', + 'Noto Sans Kannada': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansKannada/NotoSansKannada-Regular.ttf', + 'Noto Sans Malayalam': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansMalayalam/NotoSansMalayalam-Regular.ttf', + 'Noto Sans Oriya': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansOriya/NotoSansOriya-Regular.ttf', + 'Noto Sans Gurmukhi': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansGurmukhi/NotoSansGurmukhi-Regular.ttf', + 'Noto Sans Tamil': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansTamil/NotoSansTamil-Regular.ttf', + 'Noto Sans Telugu': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansTelugu/NotoSansTelugu-Regular.ttf', + 'Noto Sans Sinhala': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansSinhala/NotoSansSinhala-Regular.ttf', + 'Noto Sans Thai': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansThai/NotoSansThai-Regular.ttf', + 'Noto Sans Khmer': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansKhmer/NotoSansKhmer-Regular.ttf', + 'Noto Sans Lao': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansLao/NotoSansLao-Regular.ttf', + 'Noto Sans Myanmar': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansMyanmar/NotoSansMyanmar-Regular.ttf', + 'Noto Sans Hebrew': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansHebrew/NotoSansHebrew-Regular.ttf', + 'Noto Sans Georgian': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansGeorgian/NotoSansGeorgian-Regular.ttf', + 'Noto Sans Ethiopic': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansEthiopic/NotoSansEthiopic-Regular.ttf', + 'Noto Serif Tibetan': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSerifTibetan/NotoSerifTibetan-Regular.ttf', + 'Noto Sans Cherokee': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansCherokee/NotoSansCherokee-Regular.ttf', + 'Noto Sans Armenian': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansArmenian/NotoSansArmenian-Regular.ttf', + 'Noto Sans Syriac': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansSyriac/NotoSansSyriac-Regular.ttf', + 'Noto Sans': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSans/NotoSans-Regular.ttf', +}; \ No newline at end of file diff --git a/src/js/handlers/fileHandler.ts b/src/js/handlers/fileHandler.ts index 445a1aa..34fef03 100644 --- a/src/js/handlers/fileHandler.ts +++ b/src/js/handlers/fileHandler.ts @@ -473,24 +473,24 @@ async function handleSinglePdfUpload(toolId, file) { addBtn.onclick = () => { const fieldWrapper = document.createElement('div'); - fieldWrapper.className = 'flex items-center gap-2 custom-field-wrapper'; + fieldWrapper.className = 'flex flex-col sm:flex-row items-stretch sm:items-center gap-2 custom-field-wrapper'; const keyInput = document.createElement('input'); keyInput.type = 'text'; keyInput.placeholder = 'Key (e.g., Department)'; keyInput.className = - 'custom-meta-key w-1/3 bg-gray-800 border border-gray-600 text-white rounded-lg p-2'; + 'custom-meta-key w-full sm:w-1/3 bg-gray-800 border border-gray-600 text-white rounded-lg p-2'; const valueInput = document.createElement('input'); valueInput.type = 'text'; valueInput.placeholder = 'Value (e.g., Marketing)'; valueInput.className = - 'custom-meta-value flex-grow bg-gray-800 border border-gray-600 text-white rounded-lg p-2'; + 'custom-meta-value w-full sm:flex-grow bg-gray-800 border border-gray-600 text-white rounded-lg p-2'; const removeBtn = document.createElement('button'); removeBtn.type = 'button'; removeBtn.className = - 'btn p-2 text-red-500 hover:bg-gray-700 rounded-full'; + 'btn p-2 text-red-500 hover:bg-gray-700 rounded-full self-center sm:self-auto'; removeBtn.innerHTML = ''; removeBtn.addEventListener('click', () => fieldWrapper.remove()); diff --git a/src/js/logic/form-creator.ts b/src/js/logic/form-creator.ts index c7d5629..db5808c 100644 --- a/src/js/logic/form-creator.ts +++ b/src/js/logic/form-creator.ts @@ -8,50 +8,14 @@ import 'pdfjs-dist/web/pdf_viewer.css' // Initialize PDF.js worker pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString() -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 -} +import { FormField, PageData } from '../types/index.js' -interface PageData { - index: number - width: number - height: number - pdfPageData?: string -} let fields: FormField[] = [] let selectedField: FormField | null = null let fieldCounter = 0 -let existingFieldNames: Set = new Set() -let existingRadioGroups: Set = new Set() +let existingFieldNames: Set = new Set() +let existingRadioGroups: Set = new Set() let draggedElement: HTMLElement | null = null let offsetX = 0 let offsetY = 0 @@ -59,7 +23,7 @@ let offsetY = 0 let pages: PageData[] = [] let currentPageIndex = 0 let uploadedPdfDoc: PDFDocument | null = null -let uploadedPdfjsDoc: any = null +let uploadedPdfjsDoc: any = null let pageSize: { width: number; height: number } = { width: 612, height: 792 } let currentScale = 1.333 let pdfViewerOffset = { x: 0, y: 0 } @@ -99,6 +63,132 @@ const addPageBtn = document.getElementById('addPageBtn') as HTMLButtonElement const resetBtn = document.getElementById('resetBtn') as HTMLButtonElement const downloadBtn = document.getElementById('downloadBtn') as HTMLButtonElement const backToToolsBtn = document.getElementById('back-to-tools') as HTMLButtonElement | null +const gotoPageInput = document.getElementById('gotoPageInput') as HTMLInputElement +const gotoPageBtn = document.getElementById('gotoPageBtn') as HTMLButtonElement + +const gridVInput = document.getElementById('gridVInput') as HTMLInputElement +const gridHInput = document.getElementById('gridHInput') as HTMLInputElement +const toggleGridBtn = document.getElementById('toggleGridBtn') as HTMLButtonElement +const enableGridCheckbox = document.getElementById('enableGridCheckbox') as HTMLInputElement +let gridV = 2 +let gridH = 2 +let gridAlwaysVisible = false +let gridEnabled = true + +if (gridVInput && gridHInput) { + gridVInput.value = '2' + gridHInput.value = '2' + + const updateGrid = () => { + let v = parseInt(gridVInput.value) || 2 + let h = parseInt(gridHInput.value) || 2 + + if (v < 2) { v = 2; gridVInput.value = '2' } + if (h < 2) { h = 2; gridHInput.value = '2' } + if (v > 14) { v = 14; gridVInput.value = '14' } + if (h > 14) { h = 14; gridHInput.value = '14' } + + gridV = v + gridH = h + + if (gridAlwaysVisible && gridEnabled) { + renderGrid() + } + } + + gridVInput.addEventListener('input', updateGrid) + gridHInput.addEventListener('input', updateGrid) +} + +if (enableGridCheckbox) { + enableGridCheckbox.addEventListener('change', (e) => { + gridEnabled = (e.target as HTMLInputElement).checked + + if (!gridEnabled) { + removeGrid() + if (gridVInput) gridVInput.disabled = true + if (gridHInput) gridHInput.disabled = true + if (toggleGridBtn) toggleGridBtn.disabled = true + } else { + if (gridVInput) gridVInput.disabled = false + if (gridHInput) gridHInput.disabled = false + if (toggleGridBtn) toggleGridBtn.disabled = false + if (gridAlwaysVisible) renderGrid() + } + }) +} + +if (toggleGridBtn) { + toggleGridBtn.addEventListener('click', () => { + gridAlwaysVisible = !gridAlwaysVisible + + if (gridAlwaysVisible) { + toggleGridBtn.classList.add('bg-indigo-600') + toggleGridBtn.classList.remove('bg-gray-600') + if (gridEnabled) renderGrid() + } else { + toggleGridBtn.classList.remove('bg-indigo-600') + toggleGridBtn.classList.add('bg-gray-600') + removeGrid() + } + }) +} + +function renderGrid() { + const existingGrid = document.getElementById('pdfGrid') + if (existingGrid) existingGrid.remove() + + const gridContainer = document.createElement('div') + gridContainer.id = 'pdfGrid' + gridContainer.className = 'absolute inset-0 pointer-events-none' + gridContainer.style.zIndex = '1' + + if (gridV > 0) { + const stepX = canvas.offsetWidth / gridV + for (let i = 0; i <= gridV; i++) { + const line = document.createElement('div') + line.className = 'absolute top-0 bottom-0 border-l-2 border-indigo-500 opacity-60' + line.style.left = (i * stepX) + 'px' + gridContainer.appendChild(line) + } + } + + if (gridH > 0) { + const stepY = canvas.offsetHeight / gridH + for (let i = 0; i <= gridH; i++) { + const line = document.createElement('div') + line.className = 'absolute left-0 right-0 border-t-2 border-indigo-500 opacity-60' + line.style.top = (i * stepY) + 'px' + gridContainer.appendChild(line) + } + } + + canvas.insertBefore(gridContainer, canvas.firstChild) +} + +function removeGrid() { + const existingGrid = document.getElementById('pdfGrid') + if (existingGrid) existingGrid.remove() +} + +if (gotoPageBtn && gotoPageInput) { + gotoPageBtn.addEventListener('click', () => { + const pageNum = parseInt(gotoPageInput.value) + if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= pages.length) { + currentPageIndex = pageNum - 1 + renderCanvas() + updatePageNavigation() + } else { + alert(`Please enter a valid page number between 1 and ${pages.length}`) + } + }) + + gotoPageInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + gotoPageBtn.click() + } + }) +} // Tool item interactions const toolItems = document.querySelectorAll('.tool-item') @@ -109,10 +199,13 @@ toolItems.forEach(item => { e.dataTransfer.effectAllowed = 'copy' const type = (item as HTMLElement).dataset.type || 'text' e.dataTransfer.setData('text/plain', type) + if (gridEnabled) renderGrid() } }) - // Click to select tool for placement + item.addEventListener('dragend', () => { + if (!gridAlwaysVisible && gridEnabled) removeGrid() + }) item.addEventListener('click', () => { const type = (item as HTMLElement).dataset.type || 'text' @@ -191,8 +284,9 @@ canvas.addEventListener('dragover', (e) => { canvas.addEventListener('drop', (e) => { e.preventDefault() + if (!gridAlwaysVisible) removeGrid() const rect = canvas.getBoundingClientRect() - const x = e.clientX - rect.left - 75 // Center the field on drop point + const x = e.clientX - rect.left - 75 const y = e.clientY - rect.top - 15 const type = e.dataTransfer?.getData('text/plain') || 'text' createField(type as any, x, y) @@ -246,7 +340,9 @@ function createField(type: FormField['type'], x: number, y: number): void { visibilityAction: type === 'button' ? 'toggle' : undefined, dateFormat: type === 'date' ? 'mm/dd/yyyy' : undefined, pageIndex: currentPageIndex, - multiline: type === 'text' ? false : undefined + multiline: type === 'text' ? false : undefined, + borderColor: '#000000', + hideBorder: false } fields.push(field) @@ -263,6 +359,7 @@ function renderField(field: FormField): void { fieldWrapper.style.top = field.y + 'px' fieldWrapper.style.width = field.width + 'px' fieldWrapper.style.overflow = 'visible' + fieldWrapper.style.zIndex = '10' // Ensure fields are above grid and PDF // Create label - hidden by default, shown on group hover or selection const label = document.createElement('div') @@ -398,6 +495,7 @@ function renderField(field: FormField): void { offsetX = e.clientX - rect.left - field.x offsetY = e.clientY - rect.top - field.y selectField(field) + if (gridEnabled) renderGrid() e.preventDefault() }) @@ -559,9 +657,9 @@ document.addEventListener('mouseup', () => { draggedElement = null resizing = false resizeField = null + if (!gridAlwaysVisible) removeGrid() }) -// Touch move for dragging and resizing document.addEventListener('touchmove', (e) => { const touch = e.touches[0] if (resizing && resizeField) { @@ -866,6 +964,14 @@ function showProperties(field: FormField): void { +
+ + +
+
+ + +
@@ -963,6 +1069,17 @@ function showProperties(field: FormField): void { field.readOnly = (e.target as HTMLInputElement).checked }) + const propBorderColor = document.getElementById('propBorderColor') as HTMLInputElement + const propHideBorder = document.getElementById('propHideBorder') as HTMLInputElement + + propBorderColor.addEventListener('input', (e) => { + field.borderColor = (e.target as HTMLInputElement).value + }) + + propHideBorder.addEventListener('change', (e) => { + field.hideBorder = (e.target as HTMLInputElement).checked + }) + deleteBtn.addEventListener('click', () => { deleteField(field) }) @@ -1424,14 +1541,15 @@ downloadBtn.addEventListener('click', async () => { if (field.type === 'text') { const textField = form.createTextField(field.name) const rgbColor = hexToRgb(field.textColor) + const borderRgb = hexToRgb(field.borderColor || '#000000') textField.addToPage(pdfPage, { x: x, y: y, width: width, height: height, - borderWidth: 1, - borderColor: rgb(0, 0, 0), + borderWidth: field.hideBorder ? 0 : 1, + borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), backgroundColor: rgb(1, 1, 1), textColor: rgb(rgbColor.r, rgbColor.g, rgbColor.b), }) @@ -1474,13 +1592,14 @@ downloadBtn.addEventListener('click', async () => { } else if (field.type === 'checkbox') { const checkBox = form.createCheckBox(field.name) + const borderRgb = hexToRgb(field.borderColor || '#000000') checkBox.addToPage(pdfPage, { x: x, y: y, width: width, height: height, - borderWidth: 1, - borderColor: rgb(0, 0, 0), + borderWidth: field.hideBorder ? 0 : 1, + borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), backgroundColor: rgb(1, 1, 1), }) if (field.checked) checkBox.check() @@ -1512,13 +1631,14 @@ downloadBtn.addEventListener('click', async () => { } } + const borderRgb = hexToRgb(field.borderColor || '#000000') radioGroup.addOptionToPage(field.exportValue || 'Yes', pdfPage as any, { x: x, y: y, width: width, height: height, - borderWidth: 1, - borderColor: rgb(0, 0, 0), + borderWidth: field.hideBorder ? 0 : 1, + borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), backgroundColor: rgb(1, 1, 1), }) if (field.checked) radioGroup.select(field.exportValue || 'Yes') @@ -1532,13 +1652,14 @@ downloadBtn.addEventListener('click', async () => { } else if (field.type === 'dropdown') { const dropdown = form.createDropdown(field.name) + const borderRgb = hexToRgb(field.borderColor || '#000000') dropdown.addToPage(pdfPage, { x: x, y: y, width: width, height: height, - borderWidth: 1, - borderColor: rgb(0, 0, 0), + borderWidth: field.hideBorder ? 0 : 1, + borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), backgroundColor: rgb(1, 1, 1), // Light blue not supported in standard PDF appearance easily without streams }) if (field.options) dropdown.setOptions(field.options) @@ -1561,13 +1682,14 @@ downloadBtn.addEventListener('click', async () => { } else if (field.type === 'optionlist') { const optionList = form.createOptionList(field.name) + const borderRgb = hexToRgb(field.borderColor || '#000000') optionList.addToPage(pdfPage, { x: x, y: y, width: width, height: height, - borderWidth: 1, - borderColor: rgb(0, 0, 0), + borderWidth: field.hideBorder ? 0 : 1, + borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), backgroundColor: rgb(1, 1, 1), }) if (field.options) optionList.setOptions(field.options) @@ -1590,13 +1712,14 @@ downloadBtn.addEventListener('click', async () => { } else if (field.type === 'button') { const button = form.createButton(field.name) + const borderRgb = hexToRgb(field.borderColor || '#000000') button.addToPage(field.label || 'Button', pdfPage, { x: x, y: y, width: width, height: height, - borderWidth: 1, - borderColor: rgb(0, 0, 0), + borderWidth: field.hideBorder ? 0 : 1, + borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), backgroundColor: rgb(0.8, 0.8, 0.8), // Light gray }) diff --git a/src/js/logic/ocr-pdf.ts b/src/js/logic/ocr-pdf.ts index 303b09f..d77fcbd 100644 --- a/src/js/logic/ocr-pdf.ts +++ b/src/js/logic/ocr-pdf.ts @@ -4,20 +4,25 @@ import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/he import { state } from '../state.js'; import Tesseract from 'tesseract.js'; import { PDFDocument as PDFLibDocument, StandardFonts, rgb } from 'pdf-lib'; +import fontkit from '@pdf-lib/fontkit'; import { icons, createIcons } from 'lucide'; import * as pdfjsLib from 'pdfjs-dist'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); +import type { Word } from '../types/index.js'; -let searchablePdfBytes: any = null; +let searchablePdfBytes: Uint8Array | null = null; -function sanitizeTextForWinAnsi(text: string): string { - // Remove invisible Unicode control characters (like Left-to-Right Mark U+200E) - return text - .replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '') - .replace(/[^\u0020-\u007E\u00A0-\u00FF]/g, ''); -} +import { getFontForLanguage } from '../utils/font-loader.js'; + + +// function sanitizeTextForWinAnsi(text: string): string { +// // Remove invisible Unicode control characters (like Left-to-Right Mark U+200E) +// return text +// .replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '') +// .replace(/[^\u0020-\u007E\u00A0-\u00FF]/g, ''); +// } function parseHOCR(hocrText: string) { const parser = new DOMParser(); @@ -55,7 +60,7 @@ function parseHOCR(hocrText: string) { return words; } -function binarizeCanvas(ctx: any) { +function binarizeCanvas(ctx: CanvasRenderingContext2D) { const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); const data = imageData.data; for (let i = 0; i < data.length; i += 4) { @@ -68,7 +73,7 @@ function binarizeCanvas(ctx: any) { ctx.putImageData(imageData, 0, 0); } -function updateProgress(status: any, progress: any) { +function updateProgress(status: string, progress: number) { const progressBar = document.getElementById('progress-bar'); const progressStatus = document.getElementById('progress-status'); const progressLog = document.getElementById('progress-log'); @@ -88,12 +93,13 @@ async function runOCR() { const selectedLangs = Array.from( document.querySelectorAll('.lang-checkbox:checked') ).map((cb) => (cb as HTMLInputElement).value); - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const scale = parseFloat(document.getElementById('ocr-resolution').value); - // @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message - const binarize = document.getElementById('ocr-binarize').checked; - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const whitelist = document.getElementById('ocr-whitelist').value; + const scale = parseFloat( + (document.getElementById('ocr-resolution') as HTMLSelectElement).value + ); + const binarize = (document.getElementById('ocr-binarize') as HTMLInputElement) + .checked; + const whitelist = (document.getElementById('ocr-whitelist') as HTMLInputElement) + .value; if (selectedLangs.length === 0) { showAlert( @@ -109,12 +115,13 @@ async function runOCR() { try { const worker = await Tesseract.createWorker(langString, 1, { - logger: (m: any) => updateProgress(m.status, m.progress || 0), + logger: (m: { status: string; progress: number }) => + updateProgress(m.status, m.progress || 0), }); - // Enable hOCR output await worker.setParameters({ tessjs_create_hocr: '1', + tessedit_pageseg_mode: Tesseract.PSM.AUTO, }); await worker.setParameters({ @@ -125,7 +132,48 @@ async function runOCR() { await readFileAsArrayBuffer(state.files[0]) ).promise; const newPdfDoc = await PDFLibDocument.create(); - const font = await newPdfDoc.embedFont(StandardFonts.Helvetica); + + newPdfDoc.registerFontkit(fontkit); + + updateProgress('Loading fonts...', 0); + + // Prioritize non-Latin languages for font selection if multiple are selected + const cjkLangs = ['jpn', 'chi_sim', 'chi_tra', 'kor']; + const indicLangs = ['hin', 'ben', 'guj', 'kan', 'mal', 'ori', 'pan', 'tam', 'tel', 'sin']; + const priorityLangs = [...cjkLangs, ...indicLangs, 'ara', 'rus', 'ukr']; + + const primaryLang = selectedLangs.find(l => priorityLangs.includes(l)) || selectedLangs[0] || 'eng'; + + const hasCJK = selectedLangs.some(l => cjkLangs.includes(l)); + const hasIndic = selectedLangs.some(l => indicLangs.includes(l)); + const hasLatin = selectedLangs.some(l => !priorityLangs.includes(l)) || selectedLangs.includes('eng'); + const isIndicPlusLatin = hasIndic && hasLatin && !hasCJK; + + let primaryFont; + let latinFont; + + try { + let fontBytes; + if (isIndicPlusLatin) { + const [scriptFontBytes, latinFontBytes] = await Promise.all([ + getFontForLanguage(primaryLang), + getFontForLanguage('eng') + ]); + primaryFont = await newPdfDoc.embedFont(scriptFontBytes, { subset: false }); + latinFont = await newPdfDoc.embedFont(latinFontBytes, { subset: false }); + } else { + // For CJK or single-script, use one font + fontBytes = await getFontForLanguage(primaryLang); + primaryFont = await newPdfDoc.embedFont(fontBytes, { subset: false }); + latinFont = primaryFont; + } + } catch (e) { + console.error('Font loading failed, falling back to Helvetica', e); + primaryFont = await newPdfDoc.embedFont(StandardFonts.Helvetica); + latinFont = primaryFont; + showAlert('Font Warning', 'Could not load the specific font for this language. Some characters may not appear correctly.'); + } + let fullText = ''; for (let i = 1; i <= pdf.numPages; i++) { @@ -135,10 +183,12 @@ async function runOCR() { ); const page = await pdf.getPage(i); const viewport = page.getViewport({ scale }); + const canvas = document.createElement('canvas'); canvas.width = viewport.width; canvas.height = viewport.height; const context = canvas.getContext('2d'); + await page.render({ canvasContext: context, viewport, canvas }).promise; if (binarize) { @@ -155,8 +205,8 @@ async function runOCR() { const pngImageBytes = await new Promise((resolve) => canvas.toBlob((blob) => { const reader = new FileReader(); - // @ts-expect-error TS(2769) FIXME: No overload matches this call. - reader.onload = () => resolve(new Uint8Array(reader.result)); + reader.onload = () => + resolve(new Uint8Array(reader.result as ArrayBuffer)); reader.readAsArrayBuffer(blob); }, 'image/png') ); @@ -172,22 +222,37 @@ async function runOCR() { if (data.hocr) { const words = parseHOCR(data.hocr); - words.forEach((word: any) => { + words.forEach((word: Word) => { const { x0, y0, x1, y1 } = word.bbox; - // Sanitize the text to remove characters WinAnsi cannot encode - const text = sanitizeTextForWinAnsi(word.text); + const text = word.text.replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, ''); - // Skip words that become empty after sanitization if (!text.trim()) return; + const hasNonLatin = /[^\u0000-\u007F]/.test(text); + const font = hasNonLatin ? primaryFont : latinFont; + + if (!font) { + console.warn(`Font not available for text: "${text}"`); + return; + } + const bboxWidth = x1 - x0; const bboxHeight = y1 - y0; + if (bboxWidth <= 0 || bboxHeight <= 0) { + return; + } + let fontSize = bboxHeight * 0.9; - let textWidth = font.widthOfTextAtSize(text, fontSize); - while (textWidth > bboxWidth && fontSize > 1) { - fontSize -= 0.5; - textWidth = font.widthOfTextAtSize(text, fontSize); + try { + let textWidth = font.widthOfTextAtSize(text, fontSize); + while (textWidth > bboxWidth && fontSize > 1) { + fontSize -= 0.5; + textWidth = font.widthOfTextAtSize(text, fontSize); + } + } catch (error) { + console.warn(`Could not calculate text width for "${text}":`, error); + return; } try { @@ -200,12 +265,12 @@ async function runOCR() { opacity: 0, }); } catch (error) { - // If drawing fails despite sanitization, log and skip this word console.warn(`Could not draw text "${text}":`, error); } }); } + fullText += data.text + '\n\n'; } @@ -216,23 +281,27 @@ async function runOCR() { document.getElementById('ocr-results').classList.remove('hidden'); createIcons({ icons }); - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - document.getElementById('ocr-text-output').value = fullText.trim(); + ( + document.getElementById('ocr-text-output') as HTMLTextAreaElement + ).value = fullText.trim(); document .getElementById('download-searchable-pdf') .addEventListener('click', () => { - downloadFile( - new Blob([searchablePdfBytes], { type: 'application/pdf' }), - 'searchable.pdf' - ); + if (searchablePdfBytes) { + downloadFile( + new Blob([searchablePdfBytes as BlobPart], { type: 'application/pdf' }), + 'searchable.pdf' + ); + } }); // CHANGE: The copy button logic is updated to be safer. document.getElementById('copy-text-btn').addEventListener('click', (e) => { const button = e.currentTarget as HTMLButtonElement; - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... - const textToCopy = document.getElementById('ocr-text-output').value; + const textToCopy = ( + document.getElementById('ocr-text-output') as HTMLTextAreaElement + ).value; navigator.clipboard.writeText(textToCopy).then(() => { button.textContent = ''; // Clear the button safely @@ -259,8 +328,9 @@ async function runOCR() { document .getElementById('download-txt-btn') .addEventListener('click', () => { - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const textToSave = document.getElementById('ocr-text-output').value; + const textToSave = ( + document.getElementById('ocr-text-output') as HTMLTextAreaElement + ).value; const blob = new Blob([textToSave], { type: 'text/plain' }); downloadFile(blob, 'ocr-text.txt'); }); diff --git a/src/js/logic/txt-to-pdf.ts b/src/js/logic/txt-to-pdf.ts index 5245335..299cc55 100644 --- a/src/js/logic/txt-to-pdf.ts +++ b/src/js/logic/txt-to-pdf.ts @@ -2,6 +2,9 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, hexToRgb } from '../utils/helpers.js'; import { state } from '../state.js'; import JSZip from 'jszip'; +import { getFontForLanguage, getLanguageForChar } from '../utils/font-loader.js'; +import { languageToFontFamily } from '../config/font-mappings.js'; +import fontkit from '@pdf-lib/fontkit'; import { PDFDocument as PDFLibDocument, @@ -10,69 +13,46 @@ import { PageSizes, } from 'pdf-lib'; -function sanitizeTextForPdf(text: string): string { - return text - .split('') - .map((char) => { - const code = char.charCodeAt(0); - - if (code === 0x20 || code === 0x09 || code === 0x0A) { - return char; - } - - if ((code >= 0x00 && code <= 0x1F) || (code >= 0x7F && code <= 0x9F)) { - return ' '; - } - - if (code < 0x20 || (code > 0x7E && code < 0xA0)) { - return ' '; - } - - const replacements: { [key: number]: string } = { - 0x2018: "'", - 0x2019: "'", - 0x201C: '"', - 0x201D: '"', - 0x2013: '-', - 0x2014: '--', - 0x2026: '...', - 0x00A0: ' ', - }; - - if (replacements[code]) { - return replacements[code]; - } - - try { - if (code <= 0xFF) { - return char; - } - return '?'; - } catch { - return '?'; - } - }) - .join('') - .replace(/[ \t]+/g, ' ') - .replace(/\r\n/g, '\n') - .replace(/\r/g, '\n') - .split('\n') - .map((line) => line.trimEnd()) - .join('\n'); -} - async function createPdfFromText( text: string, - fontFamilyKey: string, + selectedLanguages: string[], fontSize: number, pageSizeKey: string, - colorHex: string + colorHex: string, + orientation: string, + customWidth?: number, + customHeight?: number ): Promise { - const sanitizedText = sanitizeTextForPdf(text); - const pdfDoc = await PDFLibDocument.create(); - const font = await pdfDoc.embedFont(StandardFonts[fontFamilyKey]); - const pageSize = PageSizes[pageSizeKey]; + pdfDoc.registerFontkit(fontkit); + + console.log(`User selected languages: ${selectedLanguages.join(', ')}`); + + const fontMap = new Map(); + const fallbackFont = await pdfDoc.embedFont(StandardFonts.Helvetica); + + if (!selectedLanguages.includes('eng')) { + selectedLanguages.push('eng'); + } + + for (const lang of selectedLanguages) { + try { + const fontBytes = await getFontForLanguage(lang); + const font = await pdfDoc.embedFont(fontBytes, { subset: false }); + fontMap.set(lang, font); + } catch (e) { + console.warn(`Failed to load font for ${lang}, using fallback`, e); + fontMap.set(lang, fallbackFont); + } + } + + let pageSize = pageSizeKey === 'Custom' + ? [customWidth || 595, customHeight || 842] as [number, number] + : PageSizes[pageSizeKey]; + + if (orientation === 'landscape') { + pageSize = [pageSize[1], pageSize[0]] as [number, number]; + } const margin = 72; const textColor = hexToRgb(colorHex); @@ -82,43 +62,130 @@ async function createPdfFromText( const lineHeight = fontSize * 1.3; let y = height - margin; - const paragraphs = sanitizedText.split('\n'); + const paragraphs = text.split('\n'); + for (const paragraph of paragraphs) { + if (paragraph.trim() === '') { + y -= lineHeight; + if (y < margin) { + page = pdfDoc.addPage(pageSize); + y = page.getHeight() - margin; + } + continue; + } + const words = paragraph.split(' '); - let currentLine = ''; + let currentLineWords: { text: string; font: any }[] = []; + let currentLineWidth = 0; + for (const word of words) { - const testLine = - currentLine.length > 0 ? `${currentLine} ${word}` : word; - if (font.widthOfTextAtSize(testLine, fontSize) <= textWidth) { - currentLine = testLine; + let wordLang = 'eng'; + + for (const char of word) { + const charLang = getLanguageForChar(char); + + if (charLang === 'chi_sim') { + if (selectedLanguages.includes('jpn')) wordLang = 'jpn'; + else if (selectedLanguages.includes('kor')) wordLang = 'kor'; + else if (selectedLanguages.includes('chi_tra')) wordLang = 'chi_tra'; + else if (selectedLanguages.includes('chi_sim')) wordLang = 'chi_sim'; + } else if (selectedLanguages.includes(charLang)) { + wordLang = charLang; + } + + if (wordLang !== 'eng') break; + } + + const font = fontMap.get(wordLang) || fontMap.get('eng') || fallbackFont; + + let wordWidth = 0; + try { + wordWidth = font.widthOfTextAtSize(word, fontSize); + } catch (e) { + console.warn(`Width calculation failed for "${word}"`, e); + wordWidth = word.length * fontSize * 0.5; + } + + let spaceWidth = 0; + if (currentLineWords.length > 0) { + try { + spaceWidth = font.widthOfTextAtSize(' ', fontSize); + } catch { + spaceWidth = fontSize * 0.25; + } + } + + if (currentLineWidth + spaceWidth + wordWidth <= textWidth) { + currentLineWords.push({ text: word, font }); + currentLineWidth += spaceWidth + wordWidth; } else { + // Draw current line if (y < margin + lineHeight) { page = pdfDoc.addPage(pageSize); y = page.getHeight() - margin; } - page.drawText(currentLine, { - x: margin, - y, - font, - size: fontSize, - color: rgb(textColor.r, textColor.g, textColor.b), - }); + + let currentX = margin; + for (let i = 0; i < currentLineWords.length; i++) { + const w = currentLineWords[i]; + try { + page.drawText(w.text, { + x: currentX, + y, + font: w.font, + size: fontSize, + color: rgb(textColor.r, textColor.g, textColor.b), + }); + + const wWidth = w.font.widthOfTextAtSize(w.text, fontSize); + currentX += wWidth; + + if (i < currentLineWords.length - 1) { + const sWidth = w.font.widthOfTextAtSize(' ', fontSize); + currentX += sWidth; + } + } catch (e) { + console.warn(`Failed to draw word: "${w.text}"`, e); + } + } + y -= lineHeight; - currentLine = word; + + currentLineWords = [{ text: word, font }]; + currentLineWidth = wordWidth; } } - if (currentLine.length > 0) { + + if (currentLineWords.length > 0) { if (y < margin + lineHeight) { page = pdfDoc.addPage(pageSize); y = page.getHeight() - margin; } - page.drawText(currentLine, { - x: margin, - y, - font, - size: fontSize, - color: rgb(textColor.r, textColor.g, textColor.b), - }); + + let currentX = margin; + for (let i = 0; i < currentLineWords.length; i++) { + const w = currentLineWords[i]; + try { + page.drawText(w.text, { + x: currentX, + y, + font: w.font, + size: fontSize, + color: rgb(textColor.r, textColor.g, textColor.b), + }); + + const wWidth = w.font.widthOfTextAtSize(w.text, fontSize); + currentX += wWidth; + + if (i < currentLineWords.length - 1) { + const sWidth = w.font.widthOfTextAtSize(' ', fontSize); + currentX += sWidth; + } + } catch (e) { + console.warn(`Failed to draw word: "${w.text}"`, e); + } + } + y -= lineHeight; } } @@ -134,6 +201,99 @@ export async function setupTxtToPdfTool() { if (!uploadBtn || !textBtn || !uploadPanel || !textPanel) return; + const langContainer = document.getElementById('language-list-container'); + const dropdownBtn = document.getElementById('lang-dropdown-btn'); + const dropdownContent = document.getElementById('lang-dropdown-content'); + const dropdownText = document.getElementById('lang-dropdown-text'); + const searchInput = document.getElementById('lang-search'); + + if (langContainer && langContainer.children.length === 0) { + const allLanguages = Object.keys(languageToFontFamily).sort().map(code => { + let name = code; + try { + const displayNames = new Intl.DisplayNames(['en'], { type: 'language' }); + name = displayNames.of(code) || code; + } catch (e) { + console.warn(`Failed to get language name for ${code}`, e); + } + return { code, name: `${name} (${code})` }; + }); + + const renderLanguages = (filter = '') => { + langContainer.innerHTML = ''; + const lowerFilter = filter.toLowerCase(); + + allLanguages.forEach(lang => { + if (lang.name.toLowerCase().includes(lowerFilter) || lang.code.toLowerCase().includes(lowerFilter)) { + const wrapper = document.createElement('div'); + wrapper.className = 'flex items-center hover:bg-gray-700 p-1 rounded'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.value = lang.code; + checkbox.id = `lang-${lang.code}`; + checkbox.className = 'w-4 h-4 text-indigo-600 bg-gray-600 border-gray-500 rounded focus:ring-indigo-500 ring-offset-gray-800'; + if (lang.code === 'eng') checkbox.checked = true; + + const label = document.createElement('label'); + label.htmlFor = `lang-${lang.code}`; + label.className = 'ml-2 text-sm font-medium text-gray-300 w-full cursor-pointer'; + label.textContent = lang.name; + + checkbox.addEventListener('change', updateButtonText); + + wrapper.appendChild(checkbox); + wrapper.appendChild(label); + langContainer.appendChild(wrapper); + } + }); + }; + + renderLanguages(); + + if (searchInput) { + searchInput.addEventListener('input', (e) => { + const filter = (e.target as HTMLInputElement).value.toLowerCase(); + const items = langContainer.children; + for (let i = 0; i < items.length; i++) { + const item = items[i] as HTMLElement; + const text = item.textContent?.toLowerCase() || ''; + if (text.includes(filter)) { + item.classList.remove('hidden'); + } else { + item.classList.add('hidden'); + } + } + }); + } + + if (dropdownBtn && dropdownContent) { + dropdownBtn.addEventListener('click', (e) => { + e.stopPropagation(); + dropdownContent.classList.toggle('hidden'); + }); + + document.addEventListener('click', (e) => { + if (!dropdownBtn.contains(e.target as Node) && !dropdownContent.contains(e.target as Node)) { + dropdownContent.classList.add('hidden'); + } + }); + } + + function updateButtonText() { + const checkboxes = langContainer?.querySelectorAll('input[type="checkbox"]:checked'); + const count = checkboxes?.length || 0; + if (count === 0) { + if (dropdownText) dropdownText.textContent = 'Select Languages'; + } else if (count === 1) { + const text = checkboxes[0].nextElementSibling.textContent; + if (dropdownText) dropdownText.textContent = text || '1 Language Selected'; + } else { + if (dropdownText) dropdownText.textContent = `${count} Languages Selected`; + } + } + } + const switchToUpload = () => { uploadPanel.classList.remove('hidden'); textPanel.classList.add('hidden'); @@ -155,6 +315,19 @@ export async function setupTxtToPdfTool() { uploadBtn.addEventListener('click', switchToUpload); textBtn.addEventListener('click', switchToText); + const pageSizeSelect = document.getElementById('page-size') as HTMLSelectElement; + const customSizeContainer = document.getElementById('custom-size-container'); + + if (pageSizeSelect && customSizeContainer) { + pageSizeSelect.addEventListener('change', () => { + if (pageSizeSelect.value === 'Custom') { + customSizeContainer.classList.remove('hidden'); + } else { + customSizeContainer.classList.add('hidden'); + } + }); + } + const processBtn = document.getElementById('process-btn'); if (processBtn) { processBtn.onclick = txtToPdf; @@ -167,25 +340,41 @@ export async function txtToPdf() { showLoader('Creating PDF...'); try { - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const fontFamilyKey = document.getElementById('font-family').value; - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const fontSize = parseInt(document.getElementById('font-size').value) || 12; - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const pageSizeKey = document.getElementById('page-size').value; - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const colorHex = document.getElementById('text-color').value; + const selectedLanguages: string[] = []; + const langContainer = document.getElementById('language-list-container'); + if (langContainer) { + const checkboxes = langContainer.querySelectorAll('input[type="checkbox"]:checked'); + checkboxes.forEach((cb) => { + selectedLanguages.push((cb as HTMLInputElement).value); + }); + } + if (selectedLanguages.length === 0) selectedLanguages.push('eng'); // Fallback + + const fontSize = parseInt((document.getElementById('font-size') as HTMLInputElement)?.value) || 12; + const pageSizeKey = (document.getElementById('page-size') as HTMLSelectElement)?.value; + const orientation = (document.getElementById('page-orientation') as HTMLSelectElement)?.value || 'portrait'; + const colorHex = (document.getElementById('text-color') as HTMLInputElement)?.value; + + let customWidth: number | undefined; + let customHeight: number | undefined; + if (pageSizeKey === 'Custom') { + customWidth = parseInt((document.getElementById('custom-width') as HTMLInputElement)?.value) || 595; + customHeight = parseInt((document.getElementById('custom-height') as HTMLInputElement)?.value) || 842; + } if (isUploadMode && state.files.length > 0) { if (state.files.length === 1) { const file = state.files[0]; - const text = await file.text(); + const text = (await file.text()).normalize('NFC'); const pdfBytes = await createPdfFromText( text, - fontFamilyKey, + selectedLanguages, fontSize, pageSizeKey, - colorHex + colorHex, + orientation, + customWidth, + customHeight ); const baseName = file.name.replace(/\.txt$/i, ''); downloadFile( @@ -197,13 +386,16 @@ export async function txtToPdf() { const zip = new JSZip(); for (const file of state.files) { - const text = await file.text(); + const text = (await file.text()).normalize('NFC'); const pdfBytes = await createPdfFromText( text, - fontFamilyKey, + selectedLanguages, fontSize, pageSizeKey, - colorHex + colorHex, + orientation, + customWidth, + customHeight ); const baseName = file.name.replace(/\.txt$/i, ''); zip.file(`${baseName}.pdf`, pdfBytes); @@ -213,8 +405,7 @@ export async function txtToPdf() { downloadFile(zipBlob, 'text-to-pdf.zip'); } } else { - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const text = document.getElementById('text-input').value; + const text = ((document.getElementById('text-input') as HTMLTextAreaElement)?.value || '').normalize('NFC'); if (!text.trim()) { showAlert('Input Required', 'Please enter some text to convert.'); hideLoader(); @@ -223,10 +414,13 @@ export async function txtToPdf() { const pdfBytes = await createPdfFromText( text, - fontFamilyKey, + selectedLanguages, fontSize, pageSizeKey, - colorHex + colorHex, + orientation, + customWidth, + customHeight ); downloadFile( new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), diff --git a/src/js/main.ts b/src/js/main.ts index 97a0faa..fe54d03 100644 --- a/src/js/main.ts +++ b/src/js/main.ts @@ -140,15 +140,20 @@ const init = () => { hideBrandingSections(); } - // Hide shortcuts button on touch devices - const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; - if (isTouchDevice) { - const shortcutsBtn = document.getElementById('open-shortcuts-btn'); - if (shortcutsBtn) { - shortcutsBtn.style.display = 'none'; - } - } + // Hide shortcuts buttons on mobile devices (Android/iOS) + // exclude iPad -> users can connect keyboard and use shortcuts + const isMobile = /Android|iPhone|iPod/i.test(navigator.userAgent); + const keyboardShortcutBtn = document.getElementById('shortcut'); + const shortcutSettingsBtn = document.getElementById('open-shortcuts-btn'); + if (isMobile) { + keyboardShortcutBtn.style.display = 'none'; + shortcutSettingsBtn.style.display = 'none'; + } else { + keyboardShortcutBtn.textContent = navigator.userAgent.toUpperCase().includes('MAC') + ? '⌘ + K' + : 'Ctrl + K'; + } dom.toolGrid.textContent = ''; @@ -205,6 +210,22 @@ const init = () => { const searchBar = document.getElementById('search-bar'); const categoryGroups = dom.toolGrid.querySelectorAll('.category-group'); + + const fuzzyMatch = (searchTerm: string, targetText: string): boolean => { + if (!searchTerm) return true; + + let searchIndex = 0; + let targetIndex = 0; + + while (searchIndex < searchTerm.length && targetIndex < targetText.length) { + if (searchTerm[searchIndex] === targetText[targetIndex]) { + searchIndex++; + } + targetIndex++; + } + + return searchIndex === searchTerm.length; + }; searchBar.addEventListener('input', () => { // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message @@ -218,8 +239,9 @@ const init = () => { const toolName = card.querySelector('h3').textContent.toLowerCase(); const toolSubtitle = card.querySelector('p')?.textContent.toLowerCase() || ''; + const isMatch = - toolName.includes(searchTerm) || toolSubtitle.includes(searchTerm); + fuzzyMatch(searchTerm, toolName) || fuzzyMatch(searchTerm, toolSubtitle); card.classList.toggle('hidden', !isMatch); if (isMatch) { @@ -243,17 +265,6 @@ const init = () => { } }); - const shortcutK = document.getElementById('shortcut'); - const isIosOrAndroid = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent); - - if (isIosOrAndroid) { - shortcutK.style.display = 'none'; - } else { - shortcutK.textContent = navigator.userAgent.toUpperCase().includes('MAC') - ? '⌘ + K' - : 'Ctrl + K'; - } - dom.toolGrid.addEventListener('click', (e) => { // @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message const card = e.target.closest('.tool-card'); diff --git a/src/js/types/form-creator.ts b/src/js/types/form-creator.ts new file mode 100644 index 0000000..f0f7c5b --- /dev/null +++ b/src/js/types/form-creator.ts @@ -0,0 +1,40 @@ +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 +} diff --git a/src/js/types/index.ts b/src/js/types/index.ts new file mode 100644 index 0000000..1e88b7b --- /dev/null +++ b/src/js/types/index.ts @@ -0,0 +1,2 @@ +export * from './ocr.js'; +export * from './form-creator.js'; diff --git a/src/js/types/ocr.ts b/src/js/types/ocr.ts new file mode 100644 index 0000000..84e7d53 --- /dev/null +++ b/src/js/types/ocr.ts @@ -0,0 +1,10 @@ +export interface Word { + text: string; + bbox: { + x0: number; + y0: number; + x1: number; + y1: number; + }; + confidence: number; +} diff --git a/src/js/ui.ts b/src/js/ui.ts index e85d265..f929b49 100644 --- a/src/js/ui.ts +++ b/src/js/ui.ts @@ -1309,12 +1309,21 @@ export const toolTemplates = {
- - + +
+ + +
@@ -1323,10 +1332,42 @@ export const toolTemplates = {
+
+ + +
+
diff --git a/src/js/utils/font-loader.ts b/src/js/utils/font-loader.ts new file mode 100644 index 0000000..7d2bc83 --- /dev/null +++ b/src/js/utils/font-loader.ts @@ -0,0 +1,281 @@ +import { languageToFontFamily, fontFamilyToUrl } from '../config/font-mappings.js'; + +const fontCache: Map = new Map(); + +const DB_NAME = 'bentopdf-fonts'; +const DB_VERSION = 1; +const STORE_NAME = 'fonts'; + +async function openFontDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME); + } + }; + }); +} + +async function getCachedFontFromDB(fontFamily: string): Promise { + try { + const db = await openFontDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction(STORE_NAME, 'readonly'); + const store = transaction.objectStore(STORE_NAME); + const request = store.get(fontFamily); + + request.onsuccess = () => resolve(request.result || null); + request.onerror = () => reject(request.error); + }); + } catch (error) { + console.warn('IndexedDB read failed:', error); + return null; + } +} + +async function saveFontToDB(fontFamily: string, fontBuffer: ArrayBuffer): Promise { + try { + const db = await openFontDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction(STORE_NAME, 'readwrite'); + const store = transaction.objectStore(STORE_NAME); + const request = store.put(fontBuffer, fontFamily); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } catch (error) { + console.warn('IndexedDB write failed:', error); + } +} + +export async function getFontForLanguage(lang: string): Promise { + const fontFamily = languageToFontFamily[lang] || 'Noto Sans'; + + if (fontCache.has(fontFamily)) { + return fontCache.get(fontFamily)!; + } + const cachedFont = await getCachedFontFromDB(fontFamily); + if (cachedFont) { + fontCache.set(fontFamily, cachedFont); + return cachedFont; + } + + try { + const fontUrl = fontFamilyToUrl[fontFamily] || fontFamilyToUrl['Noto Sans']; + + const fontResponse = await fetch(fontUrl); + + if (!fontResponse.ok) { + throw new Error(`Failed to fetch font file: ${fontResponse.statusText}`); + } + + const fontBuffer = await fontResponse.arrayBuffer(); + + fontCache.set(fontFamily, fontBuffer); + await saveFontToDB(fontFamily, fontBuffer); + + return fontBuffer; + } catch (error) { + console.warn(`Failed to fetch font for ${lang} (${fontFamily}), falling back to default.`, error); + + if (fontFamily !== 'Noto Sans') { + return await getFontForLanguage('eng'); + } + + throw error; + } +} + +export function detectScripts(text: string): string[] { + const scripts = new Set(); + + // Japanese: Hiragana (\u3040-\u309F) & Katakana (\u30A0-\u30FF) + if (/[\u3040-\u309F\u30A0-\u30FF]/.test(text)) { + scripts.add('jpn'); + } + + // Korean: Hangul Syllables (\uAC00-\uD7A3) & Jamo (\u1100-\u11FF) + if (/[\uAC00-\uD7A3\u1100-\u11FF]/.test(text)) { + scripts.add('kor'); + } + + // Chinese: CJK Unified Ideographs (\u4E00-\u9FFF) & Ext A (\u3400-\u4DBF) + if (/[\u4E00-\u9FFF\u3400-\u4DBF]/.test(text)) { + scripts.add('chi_sim'); + } + + // Check for Arabic + if (/[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]/.test(text)) { + scripts.add('ara'); + } + + // Check for Devanagari (Hindi, Marathi, etc.) + if (/[\u0900-\u097F]/.test(text)) scripts.add('hin'); + + // Check for Bengali + if (/[\u0980-\u09FF]/.test(text)) scripts.add('ben'); + + // Check for Tamil + if (/[\u0B80-\u0BFF]/.test(text)) scripts.add('tam'); + + // Check for Telugu + if (/[\u0C00-\u0C7F]/.test(text)) scripts.add('tel'); + + // Check for Kannada + if (/[\u0C80-\u0CFF]/.test(text)) scripts.add('kan'); + + // Check for Malayalam + if (/[\u0D00-\u0D7F]/.test(text)) scripts.add('mal'); + + // Check for Gujarati + if (/[\u0A80-\u0AFF]/.test(text)) scripts.add('guj'); + + // Check for Punjabi (Gurmukhi) + if (/[\u0A00-\u0A7F]/.test(text)) scripts.add('pan'); + + // Check for Oriya + if (/[\u0B00-\u0B7F]/.test(text)) scripts.add('ori'); + + // Check for Sinhala + if (/[\u0D80-\u0DFF]/.test(text)) scripts.add('sin'); + + // Check for Thai + if (/[\u0E00-\u0E7F]/.test(text)) scripts.add('tha'); + + // Check for Lao + if (/[\u0E80-\u0EFF]/.test(text)) scripts.add('lao'); + + // Check for Khmer + if (/[\u1780-\u17FF]/.test(text)) scripts.add('khm'); + + // Check for Myanmar + if (/[\u1000-\u109F]/.test(text)) scripts.add('mya'); + + // Check for Tibetan + if (/[\u0F00-\u0FFF]/.test(text)) scripts.add('bod'); + + // Check for Georgian + if (/[\u10A0-\u10FF]/.test(text)) scripts.add('kat'); + + // Check for Armenian + if (/[\u0530-\u058F]/.test(text)) scripts.add('hye'); + + // Check for Hebrew + if (/[\u0590-\u05FF]/.test(text)) scripts.add('heb'); + + // Check for Ethiopic + if (/[\u1200-\u137F]/.test(text)) scripts.add('amh'); + + // Check for Cherokee + if (/[\u13A0-\u13FF]/.test(text)) scripts.add('chr'); + + // Check for Syriac + if (/[\u0700-\u074F]/.test(text)) scripts.add('syr'); + + if (scripts.size === 0 || /[a-zA-Z]/.test(text)) { + scripts.add('eng'); + } + + return Array.from(scripts); +} + +export function getLanguageForChar(char: string): string { + const code = char.charCodeAt(0); + + // Latin (Basic + Supplement + Extended) + if (code <= 0x024F) return 'eng'; + + // Japanese: Hiragana & Katakana + if ( + (code >= 0x3040 && code <= 0x309F) || // Hiragana + (code >= 0x30A0 && code <= 0x30FF) // Katakana + ) return 'jpn'; + + // Korean: Hangul Syllables & Jamo + if ( + (code >= 0xAC00 && code <= 0xD7A3) || // Hangul Syllables + (code >= 0x1100 && code <= 0x11FF) // Hangul Jamo + ) return 'kor'; + + // Chinese: CJK Unified Ideographs (Han) + if ( + (code >= 0x4E00 && code <= 0x9FFF) || // CJK Unified + (code >= 0x3400 && code <= 0x4DBF) // CJK Ext A + ) return 'chi_sim'; + + // Arabic + if ((code >= 0x0600 && code <= 0x06FF) || (code >= 0x0750 && code <= 0x077F) || (code >= 0x08A0 && code <= 0x08FF)) return 'ara'; + + // Devanagari + if (code >= 0x0900 && code <= 0x097F) return 'hin'; + + // Bengali + if (code >= 0x0980 && code <= 0x09FF) return 'ben'; + + // Tamil + if (code >= 0x0B80 && code <= 0x0BFF) return 'tam'; + + // Telugu + if (code >= 0x0C00 && code <= 0x0C7F) return 'tel'; + + // Kannada + if (code >= 0x0C80 && code <= 0x0CFF) return 'kan'; + + // Malayalam + if (code >= 0x0D00 && code <= 0x0D7F) return 'mal'; + + // Gujarati + if (code >= 0x0A80 && code <= 0x0AFF) return 'guj'; + + // Punjabi (Gurmukhi) + if (code >= 0x0A00 && code <= 0x0A7F) return 'pan'; + + // Oriya + if (code >= 0x0B00 && code <= 0x0B7F) return 'ori'; + + // Sinhala + if (code >= 0x0D80 && code <= 0x0DFF) return 'sin'; + + // Thai + if (code >= 0x0E00 && code <= 0x0E7F) return 'tha'; + + // Lao + if (code >= 0x0E80 && code <= 0x0EFF) return 'lao'; + + // Khmer + if (code >= 0x1780 && code <= 0x17FF) return 'khm'; + + // Myanmar + if (code >= 0x1000 && code <= 0x109F) return 'mya'; + + // Tibetan + if (code >= 0x0F00 && code <= 0x0FFF) return 'bod'; + + // Georgian + if (code >= 0x10A0 && code <= 0x10FF) return 'kat'; + + // Armenian + if (code >= 0x0530 && code <= 0x058F) return 'hye'; + + // Hebrew + if (code >= 0x0590 && code <= 0x05FF) return 'heb'; + + // Ethiopic + if (code >= 0x1200 && code <= 0x137F) return 'amh'; + + // Cherokee + if (code >= 0x13A0 && code <= 0x13FF) return 'chr'; + + // Syriac + if (code >= 0x0700 && code <= 0x074F) return 'syr'; + + // Default to English (Latin) + return 'eng'; +} diff --git a/src/pages/form-creator.html b/src/pages/form-creator.html index 3206a6f..e0deb9b 100644 --- a/src/pages/form-creator.html +++ b/src/pages/form-creator.html @@ -205,6 +205,27 @@ disabled> +
+ Go to: + + +
+
+ + + + x + + +
- -
- -
-
-

How it works:

-
    -
  • Click and drag the icon to change the order of the files.
  • -
  • In the "Pages" box for each file, you can specify ranges (e.g., "1-3, 5") to merge only those pages.
  • -
  • Leave the "Pages" box blank to include all pages from that file.
  • -
-
-
    -
    - - - - -
    -`, split: () => `

    Split PDF

    diff --git a/src/pages/merge-pdf.html b/src/pages/merge-pdf.html new file mode 100644 index 0000000..f5f4ec7 --- /dev/null +++ b/src/pages/merge-pdf.html @@ -0,0 +1,260 @@ + + + + + + + Merge PDF - BentoPDF + + + + + + + + +
    +
    + + +

    Merge PDFs

    +

    + Combine whole files, or select specific pages to merge into a new document. +

    + + +
    +
    + +

    Click to select a file or drag and + drop

    +

    PDFs or Images

    +

    Your files never leave your device.

    +
    + +
    + + + + + + +
    +
    + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 159e558..d1bef63 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -62,6 +62,7 @@ export default defineConfig(({ mode }) => ({ 'add-stamps': resolve(__dirname, 'src/pages/add-stamps.html'), 'form-creator': resolve(__dirname, 'src/pages/form-creator.html'), 'repair-pdf': resolve(__dirname, 'src/pages/repair-pdf.html'), + 'merge-pdf': resolve(__dirname, 'src/pages/merge-pdf.html'), }, }, }, From 6676fe9f8915fe08029efa5513d9b1518fbc0bbb Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Thu, 4 Dec 2025 14:02:20 +0530 Subject: [PATCH 04/21] feat: Reimplement PDF splitting functionality on a new dedicated page. --- src/js/config/tools.ts | 2 +- src/js/logic/index.ts | 3 +- src/js/logic/merge-pdf-page.ts | 82 +- src/js/logic/split-pdf-page.ts | 547 +++++ src/js/logic/split.ts | 361 ---- src/js/ui.ts | 3473 ++++++++++++++++---------------- src/pages/merge-pdf.html | 2 +- src/pages/split-pdf.html | 334 +++ vite.config.ts | 1 + 9 files changed, 2634 insertions(+), 2171 deletions(-) create mode 100644 src/js/logic/split-pdf-page.ts delete mode 100644 src/js/logic/split.ts create mode 100644 src/pages/split-pdf.html diff --git a/src/js/config/tools.ts b/src/js/config/tools.ts index 700d25d..f73abee 100644 --- a/src/js/config/tools.ts +++ b/src/js/config/tools.ts @@ -16,7 +16,7 @@ export const categories = [ subtitle: 'Combine multiple PDFs into one file. Preserves Bookmarks.', }, { - id: 'split', + href: '/src/pages/split-pdf.html', name: 'Split PDF', icon: 'scissors', subtitle: 'Extract a range of pages into a new PDF.', diff --git a/src/js/logic/index.ts b/src/js/logic/index.ts index 6434a18..033d1be 100644 --- a/src/js/logic/index.ts +++ b/src/js/logic/index.ts @@ -1,5 +1,5 @@ -import { setupSplitTool, split } from './split.js'; + import { encrypt } from './encrypt.js'; import { decrypt } from './decrypt.js'; import { organize } from './organize.js'; @@ -71,7 +71,6 @@ import { repairPdf } from './repair-pdf.js'; export const toolLogic = { - split: { process: split, setup: setupSplitTool }, encrypt, decrypt, 'remove-restrictions': removeRestrictions, diff --git a/src/js/logic/merge-pdf-page.ts b/src/js/logic/merge-pdf-page.ts index 3b99ab4..25712f2 100644 --- a/src/js/logic/merge-pdf-page.ts +++ b/src/js/logic/merge-pdf-page.ts @@ -201,6 +201,59 @@ async function renderPageMergeThumbnails() { } } +const updateUI = async () => { + const fileControls = document.getElementById('file-controls'); + const mergeOptions = document.getElementById('merge-options'); + + if (state.files.length > 0) { + if (fileControls) fileControls.classList.remove('hidden'); + if (mergeOptions) mergeOptions.classList.remove('hidden'); + await refreshMergeUI(); + } else { + if (fileControls) fileControls.classList.add('hidden'); + if (mergeOptions) mergeOptions.classList.add('hidden'); + // Clear file list UI + const fileList = document.getElementById('file-list'); + if (fileList) fileList.innerHTML = ''; + } +}; + +const resetState = async () => { + state.files = []; + state.pdfDoc = null; + + mergeState.pdfDocs = {}; + mergeState.pdfBytes = {}; + mergeState.activeMode = 'file'; + mergeState.cachedThumbnails = null; + mergeState.lastFileHash = null; + mergeState.mergeSuccess = false; + + const fileList = document.getElementById('file-list'); + if (fileList) fileList.innerHTML = ''; + + const pageMergePreview = document.getElementById('page-merge-preview'); + if (pageMergePreview) pageMergePreview.innerHTML = ''; + + const fileModeBtn = document.getElementById('file-mode-btn'); + const pageModeBtn = document.getElementById('page-mode-btn'); + const filePanel = document.getElementById('file-mode-panel'); + const pagePanel = document.getElementById('page-mode-panel'); + + if (fileModeBtn && pageModeBtn && filePanel && pagePanel) { + fileModeBtn.classList.add('bg-indigo-600', 'text-white'); + fileModeBtn.classList.remove('bg-gray-700', 'text-gray-300'); + pageModeBtn.classList.remove('bg-indigo-600', 'text-white'); + pageModeBtn.classList.add('bg-gray-700', 'text-gray-300'); + + filePanel.classList.remove('hidden'); + pagePanel.classList.add('hidden'); + } + + await updateUI(); +}; + + export async function merge() { showLoader('Merging PDFs...'); try { @@ -320,7 +373,9 @@ export async function merge() { const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' }); downloadFile(blob, 'merged.pdf'); mergeState.mergeSuccess = true; - showAlert('Success', 'PDFs merged successfully!'); + showAlert('Success', 'PDFs merged successfully!', 'success', async () => { + await resetState(); + }); } else { console.error('Worker merge error:', e.data.message); showAlert('Error', e.data.message || 'Failed to merge PDFs.'); @@ -494,19 +549,7 @@ document.addEventListener('DOMContentLoaded', () => { }); } - const updateUI = async () => { - if (state.files.length > 0) { - if (fileControls) fileControls.classList.remove('hidden'); - if (mergeOptions) mergeOptions.classList.remove('hidden'); - await refreshMergeUI(); - } else { - if (fileControls) fileControls.classList.add('hidden'); - if (mergeOptions) mergeOptions.classList.add('hidden'); - // Clear file list UI - const fileList = document.getElementById('file-list'); - if (fileList) fileList.innerHTML = ''; - } - }; + if (fileInput && dropZone) { fileInput.addEventListener('change', async (e) => { @@ -565,14 +608,5 @@ document.addEventListener('DOMContentLoaded', () => { }); } - const alertOkBtn = document.getElementById('alert-ok-btn'); - if (alertOkBtn) { - alertOkBtn.addEventListener('click', async () => { - if (mergeState.mergeSuccess) { - state.files = []; - mergeState.mergeSuccess = false; - await updateUI(); - } - }); - } + }); diff --git a/src/js/logic/split-pdf-page.ts b/src/js/logic/split-pdf-page.ts new file mode 100644 index 0000000..5cf7f4e --- /dev/null +++ b/src/js/logic/split-pdf-page.ts @@ -0,0 +1,547 @@ +import { showLoader, hideLoader, showAlert } from '../ui.js'; +import { createIcons, icons } from 'lucide'; +import * as pdfjsLib from 'pdfjs-dist'; +import { downloadFile, getPDFDocument, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js'; +import { state } from '../state.js'; +import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js'; +import JSZip from 'jszip'; +import { PDFDocument as PDFLibDocument } from 'pdf-lib'; + +// @ts-ignore +pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); + +document.addEventListener('DOMContentLoaded', () => { + let visualSelectorRendered = false; + + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const processBtn = document.getElementById('process-btn'); + const fileDisplayArea = document.getElementById('file-display-area'); + const splitOptions = document.getElementById('split-options'); + const backBtn = document.getElementById('back-to-tools'); + + // Split Mode Elements + const splitModeSelect = document.getElementById('split-mode') as HTMLSelectElement; + const rangePanel = document.getElementById('range-panel'); + const visualPanel = document.getElementById('visual-select-panel'); + const evenOddPanel = document.getElementById('even-odd-panel'); + const zipOptionWrapper = document.getElementById('zip-option-wrapper'); + const allPagesPanel = document.getElementById('all-pages-panel'); + const bookmarksPanel = document.getElementById('bookmarks-panel'); + const nTimesPanel = document.getElementById('n-times-panel'); + const nTimesWarning = document.getElementById('n-times-warning'); + + if (backBtn) { + backBtn.addEventListener('click', () => { + window.location.href = '/'; + }); + } + + const updateUI = async () => { + if (state.files.length > 0) { + const file = state.files[0]; + if (fileDisplayArea) { + fileDisplayArea.innerHTML = ''; + const fileDiv = document.createElement('div'); + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col overflow-hidden'; + + const nameSizeContainer = document.createElement('div'); + nameSizeContainer.className = 'flex items-center gap-2'; + + const nameSpan = document.createElement('span'); + nameSpan.className = 'truncate font-medium text-gray-200'; + nameSpan.textContent = file.name; + + const sizeSpan = document.createElement('span'); + sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs'; + sizeSpan.textContent = `(${formatBytes(file.size)})`; + + nameSizeContainer.append(nameSpan, sizeSpan); + + const pagesSpan = document.createElement('span'); + pagesSpan.className = 'text-xs text-gray-500 mt-0.5'; + pagesSpan.textContent = 'Loading pages...'; // Placeholder + + infoContainer.append(nameSizeContainer, pagesSpan); + + // Add remove button + const removeBtn = document.createElement('button'); + removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.innerHTML = ''; + removeBtn.onclick = () => { + state.files = []; + state.pdfDoc = null; + updateUI(); + }; + + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); + + // Load PDF Document + try { + if (!state.pdfDoc) { + showLoader('Loading PDF...'); + const arrayBuffer = await readFileAsArrayBuffer(file) as ArrayBuffer; + state.pdfDoc = await PDFLibDocument.load(arrayBuffer); + hideLoader(); + } + // Update page count + pagesSpan.textContent = `${state.pdfDoc.getPageCount()} Pages`; + } catch (error) { + console.error('Error loading PDF:', error); + showAlert('Error', 'Failed to load PDF file.'); + state.files = []; + updateUI(); + return; + } + } + + if (splitOptions) splitOptions.classList.remove('hidden'); + + } else { + if (fileDisplayArea) fileDisplayArea.innerHTML = ''; + if (splitOptions) splitOptions.classList.add('hidden'); + state.pdfDoc = null; + } + }; + + const renderVisualSelector = async () => { + if (visualSelectorRendered) return; + + const container = document.getElementById('page-selector-grid'); + if (!container) return; + + visualSelectorRendered = true; + container.textContent = ''; + + // Cleanup any previous lazy loading observers + cleanupLazyRendering(); + + showLoader('Rendering page previews...'); + + try { + if (!state.pdfDoc) { + // If pdfDoc is not loaded yet (e.g. page refresh), try to load it from the first file + if (state.files.length > 0) { + const file = state.files[0]; + const arrayBuffer = await readFileAsArrayBuffer(file) as ArrayBuffer; + state.pdfDoc = await PDFLibDocument.load(arrayBuffer); + } else { + throw new Error('No PDF document loaded'); + } + } + + const pdfData = await state.pdfDoc.save(); + const pdf = await getPDFDocument({ data: pdfData }).promise; + + // Function to create wrapper element for each page + const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => { + const wrapper = document.createElement('div'); + wrapper.className = + 'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500 relative'; + wrapper.dataset.pageIndex = (pageNumber - 1).toString(); + + const img = document.createElement('img'); + img.src = canvas.toDataURL(); + img.className = 'rounded-md w-full h-auto'; + + const p = document.createElement('p'); + p.className = 'text-center text-xs mt-1 text-gray-300'; + p.textContent = `Page ${pageNumber}`; + + wrapper.append(img, p); + + const handleSelection = (e: any) => { + e.preventDefault(); + e.stopPropagation(); + + const isSelected = wrapper.classList.contains('selected'); + + if (isSelected) { + wrapper.classList.remove('selected', 'border-indigo-500'); + wrapper.classList.add('border-transparent'); + } else { + wrapper.classList.add('selected', 'border-indigo-500'); + wrapper.classList.remove('border-transparent'); + } + }; + + wrapper.addEventListener('click', handleSelection); + wrapper.addEventListener('touchend', handleSelection); + + wrapper.addEventListener('touchstart', (e) => { + e.preventDefault(); + }); + + return wrapper; + }; + + // Render pages progressively with lazy loading + await renderPagesProgressively( + pdf, + container, + createWrapper, + { + batchSize: 8, + useLazyLoading: true, + lazyLoadMargin: '400px', + onProgress: (current, total) => { + showLoader(`Rendering page previews: ${current}/${total}`); + }, + onBatchComplete: () => { + createIcons({ icons }); + } + } + ); + } catch (error) { + console.error('Error rendering visual selector:', error); + showAlert('Error', 'Failed to render page previews.'); + // Reset the flag on error so the user can try again. + visualSelectorRendered = false; + } finally { + hideLoader(); + } + }; + + const resetState = () => { + state.files = []; + state.pdfDoc = null; + + // Reset visual selection + document.querySelectorAll('.page-thumbnail-wrapper.selected').forEach(el => { + el.classList.remove('selected', 'border-indigo-500'); + el.classList.add('border-transparent'); + }); + visualSelectorRendered = false; + const container = document.getElementById('page-selector-grid'); + if (container) container.innerHTML = ''; + + // Reset inputs + const pageRangeInput = document.getElementById('page-range') as HTMLInputElement; + if (pageRangeInput) pageRangeInput.value = ''; + + const nValueInput = document.getElementById('split-n-value') as HTMLInputElement; + if (nValueInput) nValueInput.value = '5'; + + // Reset radio buttons to default (range) + const rangeRadio = document.querySelector('input[name="split-mode"][value="range"]') as HTMLInputElement; + if (rangeRadio) { + rangeRadio.checked = true; + rangeRadio.dispatchEvent(new Event('change')); + } + + // Reset split mode select + if (splitModeSelect) { + splitModeSelect.value = 'range'; + splitModeSelect.dispatchEvent(new Event('change')); + } + + updateUI(); + }; + + const split = async () => { + const splitMode = splitModeSelect.value; + const downloadAsZip = + (document.getElementById('download-as-zip') as HTMLInputElement)?.checked || + false; + + showLoader('Splitting PDF...'); + + try { + if (!state.pdfDoc) throw new Error('No PDF document loaded.'); + + const totalPages = state.pdfDoc.getPageCount(); + let indicesToExtract: number[] = []; + + switch (splitMode) { + case 'range': + const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value; + if (!pageRangeInput) throw new Error('Choose a valid page range.'); + const ranges = pageRangeInput.split(','); + for (const range of ranges) { + const trimmedRange = range.trim(); + if (trimmedRange.includes('-')) { + const [start, end] = trimmedRange.split('-').map(Number); + if ( + isNaN(start) || + isNaN(end) || + start < 1 || + end > totalPages || + start > end + ) + continue; + for (let i = start; i <= end; i++) indicesToExtract.push(i - 1); + } else { + const pageNum = Number(trimmedRange); + if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue; + indicesToExtract.push(pageNum - 1); + } + } + break; + + case 'even-odd': + const choiceElement = document.querySelector( + 'input[name="even-odd-choice"]:checked' + ) as HTMLInputElement; + if (!choiceElement) throw new Error('Please select even or odd pages.'); + const choice = choiceElement.value; + for (let i = 0; i < totalPages; i++) { + if (choice === 'even' && (i + 1) % 2 === 0) indicesToExtract.push(i); + if (choice === 'odd' && (i + 1) % 2 !== 0) indicesToExtract.push(i); + } + break; + case 'all': + indicesToExtract = Array.from({ length: totalPages }, (_, i) => i); + break; + case 'visual': + indicesToExtract = Array.from( + document.querySelectorAll('.page-thumbnail-wrapper.selected') + ) + .map((el) => parseInt((el as HTMLElement).dataset.pageIndex || '0')); + break; + case 'bookmarks': + const { getCpdf } = await import('../utils/cpdf-helper.js'); + const cpdf = await getCpdf(); + const pdfBytes = await state.pdfDoc.save(); + const pdf = cpdf.fromMemory(new Uint8Array(pdfBytes), ''); + + cpdf.startGetBookmarkInfo(pdf); + const bookmarkCount = cpdf.numberBookmarks(); + const bookmarkLevel = (document.getElementById('bookmark-level') as HTMLSelectElement)?.value; + + const splitPages: number[] = []; + for (let i = 0; i < bookmarkCount; i++) { + const level = cpdf.getBookmarkLevel(i); + const page = cpdf.getBookmarkPage(pdf, i); + + if (bookmarkLevel === 'all' || level === parseInt(bookmarkLevel)) { + if (page > 1 && !splitPages.includes(page - 1)) { + splitPages.push(page - 1); // Convert to 0-based index + } + } + } + cpdf.endGetBookmarkInfo(); + cpdf.deletePdf(pdf); + + if (splitPages.length === 0) { + throw new Error('No bookmarks found at the selected level.'); + } + + splitPages.sort((a, b) => a - b); + const zip = new JSZip(); + + for (let i = 0; i < splitPages.length; i++) { + const startPage = i === 0 ? 0 : splitPages[i]; + const endPage = i < splitPages.length - 1 ? splitPages[i + 1] - 1 : totalPages - 1; + + const newPdf = await PDFLibDocument.create(); + const pageIndices = Array.from({ length: endPage - startPage + 1 }, (_, idx) => startPage + idx); + const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices); + copiedPages.forEach((page: any) => newPdf.addPage(page)); + const pdfBytes2 = await newPdf.save(); + zip.file(`split-${i + 1}.pdf`, pdfBytes2); + } + + const zipBlob = await zip.generateAsync({ type: 'blob' }); + downloadFile(zipBlob, 'split-by-bookmarks.zip'); + hideLoader(); + showAlert('Success', 'PDF split successfully!', 'success', () => { + resetState(); + }); + return; + + case 'n-times': + const nValue = parseInt((document.getElementById('split-n-value') as HTMLInputElement)?.value || '5'); + if (nValue < 1) throw new Error('N must be at least 1.'); + + const zip2 = new JSZip(); + const numSplits = Math.ceil(totalPages / nValue); + + for (let i = 0; i < numSplits; i++) { + const startPage = i * nValue; + const endPage = Math.min(startPage + nValue - 1, totalPages - 1); + const pageIndices = Array.from({ length: endPage - startPage + 1 }, (_, idx) => startPage + idx); + + const newPdf = await PDFLibDocument.create(); + const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices); + copiedPages.forEach((page: any) => newPdf.addPage(page)); + const pdfBytes3 = await newPdf.save(); + zip2.file(`split-${i + 1}.pdf`, pdfBytes3); + } + + const zipBlob2 = await zip2.generateAsync({ type: 'blob' }); + downloadFile(zipBlob2, 'split-n-times.zip'); + hideLoader(); + showAlert('Success', 'PDF split successfully!', 'success', () => { + resetState(); + }); + return; + } + + const uniqueIndices = [...new Set(indicesToExtract)]; + if (uniqueIndices.length === 0 && splitMode !== 'bookmarks' && splitMode !== 'n-times') { + throw new Error('No pages were selected for splitting.'); + } + + if ( + splitMode === 'all' || + (['range', 'visual'].includes(splitMode) && downloadAsZip) + ) { + showLoader('Creating ZIP file...'); + const zip = new JSZip(); + for (const index of uniqueIndices) { + const newPdf = await PDFLibDocument.create(); + const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [ + index as number, + ]); + newPdf.addPage(copiedPage); + const pdfBytes = await newPdf.save(); + // @ts-ignore + zip.file(`page-${index + 1}.pdf`, pdfBytes); + } + const zipBlob = await zip.generateAsync({ type: 'blob' }); + downloadFile(zipBlob, 'split-pages.zip'); + } else { + const newPdf = await PDFLibDocument.create(); + const copiedPages = await newPdf.copyPages( + state.pdfDoc, + uniqueIndices as number[] + ); + copiedPages.forEach((page: any) => newPdf.addPage(page)); + const pdfBytes = await newPdf.save(); + downloadFile( + new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), + 'split-document.pdf' + ); + } + + if (splitMode === 'visual') { + visualSelectorRendered = false; + } + + showAlert('Success', 'PDF split successfully!', 'success', () => { + resetState(); + }); + + } catch (e: any) { + console.error(e); + showAlert( + 'Error', + e.message || 'Failed to split PDF. Please check your selection.' + ); + } finally { + hideLoader(); + } + }; + + const handleFileSelect = async (files: FileList | null) => { + if (files && files.length > 0) { + // Split tool only supports one file at a time + state.files = [files[0]]; + await updateUI(); + } + }; + + if (fileInput && dropZone) { + fileInput.addEventListener('change', (e) => { + handleFileSelect((e.target as HTMLInputElement).files); + fileInput.value = ''; + }); + + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); + }); + + dropZone.addEventListener('dragleave', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + const files = e.dataTransfer?.files; + if (files) { + const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')); + if (pdfFiles.length > 0) { + // Take only the first PDF + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(pdfFiles[0]); + handleFileSelect(dataTransfer.files); + } + } + }); + + dropZone.addEventListener('click', () => { + fileInput.click(); + }); + } + + if (splitModeSelect) { + splitModeSelect.addEventListener('change', (e) => { + const mode = (e.target as HTMLSelectElement).value; + + if (mode !== 'visual') { + visualSelectorRendered = false; + const container = document.getElementById('page-selector-grid'); + if (container) container.innerHTML = ''; + } + + rangePanel?.classList.add('hidden'); + visualPanel?.classList.add('hidden'); + evenOddPanel?.classList.add('hidden'); + allPagesPanel?.classList.add('hidden'); + bookmarksPanel?.classList.add('hidden'); + nTimesPanel?.classList.add('hidden'); + zipOptionWrapper?.classList.add('hidden'); + if (nTimesWarning) nTimesWarning.classList.add('hidden'); + + if (mode === 'range') { + rangePanel?.classList.remove('hidden'); + zipOptionWrapper?.classList.remove('hidden'); + } else if (mode === 'visual') { + visualPanel?.classList.remove('hidden'); + zipOptionWrapper?.classList.remove('hidden'); + renderVisualSelector(); + } else if (mode === 'even-odd') { + evenOddPanel?.classList.remove('hidden'); + } else if (mode === 'all') { + allPagesPanel?.classList.remove('hidden'); + } else if (mode === 'bookmarks') { + bookmarksPanel?.classList.remove('hidden'); + zipOptionWrapper?.classList.remove('hidden'); + } else if (mode === 'n-times') { + nTimesPanel?.classList.remove('hidden'); + zipOptionWrapper?.classList.remove('hidden'); + + const updateWarning = () => { + if (!state.pdfDoc) return; + const totalPages = state.pdfDoc.getPageCount(); + const nValue = parseInt((document.getElementById('split-n-value') as HTMLInputElement)?.value || '5'); + const remainder = totalPages % nValue; + if (remainder !== 0 && nTimesWarning) { + nTimesWarning.classList.remove('hidden'); + const warningText = document.getElementById('n-times-warning-text'); + if (warningText) { + warningText.textContent = `The PDF has ${totalPages} pages, which is not evenly divisible by ${nValue}. The last PDF will contain ${remainder} page(s).`; + } + } else if (nTimesWarning) { + nTimesWarning.classList.add('hidden'); + } + }; + + updateWarning(); + document.getElementById('split-n-value')?.addEventListener('input', updateWarning); + } + }); + } + + if (processBtn) { + processBtn.addEventListener('click', split); + } +}); diff --git a/src/js/logic/split.ts b/src/js/logic/split.ts deleted file mode 100644 index 45796a0..0000000 --- a/src/js/logic/split.ts +++ /dev/null @@ -1,361 +0,0 @@ -import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { createIcons, icons } from 'lucide'; -import * as pdfjsLib from 'pdfjs-dist'; -import { downloadFile, getPDFDocument } from '../utils/helpers.js'; - -pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); -import { state } from '../state.js'; -import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js'; -import JSZip from 'jszip'; - -import { PDFDocument as PDFLibDocument } from 'pdf-lib'; - -let visualSelectorRendered = false; - -async function renderVisualSelector() { - if (visualSelectorRendered) return; - - const container = document.getElementById('page-selector-grid'); - if (!container) return; - - visualSelectorRendered = true; - - container.textContent = ''; - - // Cleanup any previous lazy loading observers - cleanupLazyRendering(); - - showLoader('Rendering page previews...'); - - try { - const pdfData = await state.pdfDoc.save(); - const pdf = await getPDFDocument({ data: pdfData }).promise; - - // Function to create wrapper element for each page - const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => { - const wrapper = document.createElement('div'); - wrapper.className = - 'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500'; - // @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'. - wrapper.dataset.pageIndex = pageNumber - 1; - - const img = document.createElement('img'); - img.src = canvas.toDataURL(); - img.className = 'rounded-md w-full h-auto'; - const p = document.createElement('p'); - p.className = 'text-center text-xs mt-1 text-gray-300'; - p.textContent = `Page ${pageNumber}`; - wrapper.append(img, p); - - const handleSelection = (e: any) => { - e.preventDefault(); - e.stopPropagation(); - - const isSelected = wrapper.classList.contains('selected'); - - if (isSelected) { - wrapper.classList.remove('selected', 'border-indigo-500'); - wrapper.classList.add('border-transparent'); - } else { - wrapper.classList.add('selected', 'border-indigo-500'); - wrapper.classList.remove('border-transparent'); - } - }; - - wrapper.addEventListener('click', handleSelection); - wrapper.addEventListener('touchend', handleSelection); - - wrapper.addEventListener('touchstart', (e) => { - e.preventDefault(); - }); - - return wrapper; - }; - - // Render pages progressively with lazy loading - await renderPagesProgressively( - pdf, - container, - createWrapper, - { - batchSize: 8, - useLazyLoading: true, - lazyLoadMargin: '400px', - onProgress: (current, total) => { - showLoader(`Rendering page previews: ${current}/${total}`); - }, - onBatchComplete: () => { - createIcons({ icons }); - } - } - ); - } catch (error) { - console.error('Error rendering visual selector:', error); - showAlert('Error', 'Failed to render page previews.'); - // Reset the flag on error so the user can try again. - visualSelectorRendered = false; - } finally { - hideLoader(); - } -} - -export function setupSplitTool() { - const splitModeSelect = document.getElementById('split-mode'); - const rangePanel = document.getElementById('range-panel'); - const visualPanel = document.getElementById('visual-select-panel'); - const evenOddPanel = document.getElementById('even-odd-panel'); - const zipOptionWrapper = document.getElementById('zip-option-wrapper'); - const allPagesPanel = document.getElementById('all-pages-panel'); - const bookmarksPanel = document.getElementById('bookmarks-panel'); - const nTimesPanel = document.getElementById('n-times-panel'); - const nTimesWarning = document.getElementById('n-times-warning'); - - if (!splitModeSelect) return; - - splitModeSelect.addEventListener('change', (e) => { - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message - const mode = e.target.value; - - if (mode !== 'visual') { - visualSelectorRendered = false; - const container = document.getElementById('page-selector-grid'); - if (container) container.innerHTML = ''; - } - - rangePanel.classList.add('hidden'); - visualPanel.classList.add('hidden'); - evenOddPanel.classList.add('hidden'); - allPagesPanel.classList.add('hidden'); - bookmarksPanel.classList.add('hidden'); - nTimesPanel.classList.add('hidden'); - zipOptionWrapper.classList.add('hidden'); - if (nTimesWarning) nTimesWarning.classList.add('hidden'); - - if (mode === 'range') { - rangePanel.classList.remove('hidden'); - zipOptionWrapper.classList.remove('hidden'); - } else if (mode === 'visual') { - visualPanel.classList.remove('hidden'); - zipOptionWrapper.classList.remove('hidden'); - renderVisualSelector(); - } else if (mode === 'even-odd') { - evenOddPanel.classList.remove('hidden'); - } else if (mode === 'all') { - allPagesPanel.classList.remove('hidden'); - } else if (mode === 'bookmarks') { - bookmarksPanel.classList.remove('hidden'); - zipOptionWrapper.classList.remove('hidden'); - } else if (mode === 'n-times') { - nTimesPanel.classList.remove('hidden'); - zipOptionWrapper.classList.remove('hidden'); - - const updateWarning = () => { - if (!state.pdfDoc) return; - const totalPages = state.pdfDoc.getPageCount(); - const nValue = parseInt((document.getElementById('split-n-value') as HTMLInputElement)?.value || '5'); - const remainder = totalPages % nValue; - if (remainder !== 0 && nTimesWarning) { - nTimesWarning.classList.remove('hidden'); - const warningText = document.getElementById('n-times-warning-text'); - if (warningText) { - warningText.textContent = `The PDF has ${totalPages} pages, which is not evenly divisible by ${nValue}. The last PDF will contain ${remainder} page(s).`; - } - } else if (nTimesWarning) { - nTimesWarning.classList.add('hidden'); - } - }; - - const nValueInput = document.getElementById('split-n-value') as HTMLInputElement; - if (nValueInput) { - nValueInput.addEventListener('input', updateWarning); - updateWarning(); - } - } - }); -} - -export async function split() { - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const splitMode = document.getElementById('split-mode').value; - const downloadAsZip = - (document.getElementById('download-as-zip') as HTMLInputElement)?.checked || - false; - - showLoader('Splitting PDF...'); - - try { - const totalPages = state.pdfDoc.getPageCount(); - let indicesToExtract: any = []; - - switch (splitMode) { - case 'range': - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const pageRangeInput = document.getElementById('page-range').value; - if (!pageRangeInput) throw new Error('Please enter a page range.'); - const ranges = pageRangeInput.split(','); - for (const range of ranges) { - const trimmedRange = range.trim(); - if (trimmedRange.includes('-')) { - const [start, end] = trimmedRange.split('-').map(Number); - if ( - isNaN(start) || - isNaN(end) || - start < 1 || - end > totalPages || - start > end - ) - continue; - for (let i = start; i <= end; i++) indicesToExtract.push(i - 1); - } else { - const pageNum = Number(trimmedRange); - if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue; - indicesToExtract.push(pageNum - 1); - } - } - break; - - case 'even-odd': - const choiceElement = document.querySelector( - 'input[name="even-odd-choice"]:checked' - ); - if (!choiceElement) throw new Error('Please select even or odd pages.'); - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'. - const choice = choiceElement.value; - for (let i = 0; i < totalPages; i++) { - if (choice === 'even' && (i + 1) % 2 === 0) indicesToExtract.push(i); - if (choice === 'odd' && (i + 1) % 2 !== 0) indicesToExtract.push(i); - } - break; - case 'all': - indicesToExtract = Array.from({ length: totalPages }, (_, i) => i); - break; - case 'visual': - indicesToExtract = Array.from( - document.querySelectorAll('.page-thumbnail-wrapper.selected') - ) - // @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message - .map((el) => parseInt(el.dataset.pageIndex)); - break; - case 'bookmarks': - const { getCpdf } = await import('../utils/cpdf-helper.js'); - const cpdf = await getCpdf(); - const pdfBytes = await state.pdfDoc.save(); - const pdf = cpdf.fromMemory(new Uint8Array(pdfBytes), ''); - - cpdf.startGetBookmarkInfo(pdf); - const bookmarkCount = cpdf.numberBookmarks(); - const bookmarkLevel = (document.getElementById('bookmark-level') as HTMLSelectElement)?.value; - - const splitPages: number[] = []; - for (let i = 0; i < bookmarkCount; i++) { - const level = cpdf.getBookmarkLevel(i); - const page = cpdf.getBookmarkPage(pdf, i); - - if (bookmarkLevel === 'all' || level === parseInt(bookmarkLevel)) { - if (page > 1 && !splitPages.includes(page - 1)) { - splitPages.push(page - 1); // Convert to 0-based index - } - } - } - cpdf.endGetBookmarkInfo(); - cpdf.deletePdf(pdf); - - if (splitPages.length === 0) { - throw new Error('No bookmarks found at the selected level.'); - } - - splitPages.sort((a, b) => a - b); - const zip = new JSZip(); - - for (let i = 0; i < splitPages.length; i++) { - const startPage = i === 0 ? 0 : splitPages[i]; - const endPage = i < splitPages.length - 1 ? splitPages[i + 1] - 1 : totalPages - 1; - - const newPdf = await PDFLibDocument.create(); - const pageIndices = Array.from({ length: endPage - startPage + 1 }, (_, idx) => startPage + idx); - const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices); - copiedPages.forEach((page: any) => newPdf.addPage(page)); - const pdfBytes2 = await newPdf.save(); - zip.file(`split-${i + 1}.pdf`, pdfBytes2); - } - - const zipBlob = await zip.generateAsync({ type: 'blob' }); - downloadFile(zipBlob, 'split-by-bookmarks.zip'); - hideLoader(); - return; - - case 'n-times': - const nValue = parseInt((document.getElementById('split-n-value') as HTMLInputElement)?.value || '5'); - if (nValue < 1) throw new Error('N must be at least 1.'); - - const zip2 = new JSZip(); - const numSplits = Math.ceil(totalPages / nValue); - - for (let i = 0; i < numSplits; i++) { - const startPage = i * nValue; - const endPage = Math.min(startPage + nValue - 1, totalPages - 1); - const pageIndices = Array.from({ length: endPage - startPage + 1 }, (_, idx) => startPage + idx); - - const newPdf = await PDFLibDocument.create(); - const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices); - copiedPages.forEach((page: any) => newPdf.addPage(page)); - const pdfBytes3 = await newPdf.save(); - zip2.file(`split-${i + 1}.pdf`, pdfBytes3); - } - - const zipBlob2 = await zip2.generateAsync({ type: 'blob' }); - downloadFile(zipBlob2, 'split-n-times.zip'); - hideLoader(); - return; - } - - const uniqueIndices = [...new Set(indicesToExtract)]; - if (uniqueIndices.length === 0 && splitMode !== 'bookmarks' && splitMode !== 'n-times') { - throw new Error('No pages were selected for splitting.'); - } - - if ( - splitMode === 'all' || - (['range', 'visual'].includes(splitMode) && downloadAsZip) - ) { - showLoader('Creating ZIP file...'); - const zip = new JSZip(); - for (const index of uniqueIndices) { - const newPdf = await PDFLibDocument.create(); - const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [ - index as number, - ]); - newPdf.addPage(copiedPage); - const pdfBytes = await newPdf.save(); - // @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message - zip.file(`page-${index + 1}.pdf`, pdfBytes); - } - const zipBlob = await zip.generateAsync({ type: 'blob' }); - downloadFile(zipBlob, 'split-pages.zip'); - } else { - const newPdf = await PDFLibDocument.create(); - const copiedPages = await newPdf.copyPages( - state.pdfDoc, - uniqueIndices as number[] - ); - copiedPages.forEach((page: any) => newPdf.addPage(page)); - const pdfBytes = await newPdf.save(); - downloadFile( - new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), - 'split-document.pdf' - ); - } - - if (splitMode === 'visual') { - visualSelectorRendered = false; - } - } catch (e) { - console.error(e); - showAlert( - 'Error', - e.message || 'Failed to split PDF. Please check your selection.' - ); - } finally { - hideLoader(); - } -} diff --git a/src/js/ui.ts b/src/js/ui.ts index 140b7eb..4fd847b 100644 --- a/src/js/ui.ts +++ b/src/js/ui.ts @@ -52,10 +52,21 @@ export const hideLoader = () => { if (dom.loaderModal) dom.loaderModal.classList.add('hidden'); }; -export const showAlert = (title: any, message: any) => { +export const showAlert = (title: any, message: any, type: string = 'error', callback?: () => void) => { if (dom.alertTitle) dom.alertTitle.textContent = title; if (dom.alertMessage) dom.alertMessage.textContent = message; if (dom.alertModal) dom.alertModal.classList.remove('hidden'); + + if (dom.alertOkBtn) { + const newOkBtn = dom.alertOkBtn.cloneNode(true) as HTMLElement; + dom.alertOkBtn.replaceWith(newOkBtn); + dom.alertOkBtn = newOkBtn; + + newOkBtn.addEventListener('click', () => { + hideAlert(); + if (callback) callback(); + }); + } }; export const hideAlert = () => { @@ -434,1965 +445,1863 @@ const createFileInputHTML = (options = {}) => { export const toolTemplates = { - split: () => ` -

    Split PDF

    -

    Extract pages from a PDF using various methods.

    - ${createFileInputHTML()} -
    - -`, encrypt: () => ` -

    Encrypt PDF

    -

    Add 256-bit AES password protection to your PDF.

    - ${createFileInputHTML()} -
    - - - + < !--Restriction checkboxes(shown when owner password is entered)-- > + +
    -
    -

    ⚠️ Security Recommendation

    -

    For strong security, set both passwords. Without an owner password, the security restrictions (printing, copying, etc.) can be easily bypassed.

    -
    -
    -

    ✓ High-Quality Encryption

    -

    256-bit AES encryption without quality loss. Text remains selectable and searchable.

    -
    - - -`, + < div class="p-4 bg-yellow-900/20 border border-yellow-500/30 text-yellow-200 rounded-lg" > +

    ⚠️ Security Recommendation

    + < p class="text-sm text-gray-300" > For strong security, set both passwords.Without an owner password, the security restrictions(printing, copying, etc.) can be easily bypassed.

    + + < div class="p-4 bg-green-900/20 border border-green-500/30 text-green-200 rounded-lg" > +

    ✓ High - Quality Encryption

    + < p class="text-sm text-gray-300" > 256 - bit AES encryption without quality loss.Text remains selectable and searchable.

    + + < button id = "process-btn" class="btn-gradient w-full mt-6" > Encrypt & Download + + `, decrypt: () => ` -

    Decrypt PDF

    -

    Upload an encrypted PDF and provide its password to create an unlocked version.

    - ${createFileInputHTML()} -
    - - - `, + < h2 class="text-2xl font-bold text-white mb-4" > Decrypt PDF + < p class="mb-6 text-gray-400" > Upload an encrypted PDF and provide its password to create an unlocked version.

    + ${ createFileInputHTML() } +
    + < div id = "decrypt-options" class="hidden space-y-4 mt-6" > +
    + + < input type = "password" id = "password-input" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder = "Enter the current password" > +
    + < button id = "process-btn" class="btn-gradient w-full mt-6" > Decrypt & Download + + < canvas id = "pdf-canvas" class="hidden" > + `, organize: () => ` -

    Organize PDF

    -

    Reorder, rotate, or delete pages. Drag and drop pages to reorder them.

    - ${createFileInputHTML()} -
    - - - `, + < h2 class="text-2xl font-bold text-white mb-4" > Organize PDF + < p class="mb-6 text-gray-400" > Reorder, rotate, or delete pages.Drag and drop pages to reorder them.

    + ${ createFileInputHTML() } +
    + < div id = "page-organizer" class="hidden grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-4 my-6" > + < button id = "process-btn" class="btn-gradient w-full mt-6" > Save Changes + `, rotate: () => ` -

    Rotate PDF

    -

    Rotate all or specific pages in a PDF document.

    - ${createFileInputHTML()} -
    - - - - - -
    - - -
    -
    - - -
    -
    - - -
    - -
    - - -
    - - - `, + + < div class="relative" > + + < div id = "lang-dropdown-content" class="hidden absolute z-10 w-full bg-gray-800 border border-gray-600 rounded-lg mt-1 max-h-60 overflow-y-auto shadow-lg" > +
    + +
    + < div id = "language-list-container" class="p-2 space-y-1" > + -
    + < div id = "dimensions-results" class="hidden mt-6" > + -
    -
    - - -
    - -
    + + - -
    - - - - - - - - - - - - - - -
    Page #Dimensions (W x H)Standard SizeOrientationAspect RatioAreaRotation
    -
    - - `, + < !--Dimensions Table-- > +
    + + + + + < th class="px-4 py-3 font-medium text-white" > Dimensions(W x H) + < th class="px-4 py-3 font-medium text-white" > Standard Size + < th class="px-4 py-3 font-medium text-white" > Orientation + < th class="px-4 py-3 font-medium text-white" > Aspect Ratio + < th class="px-4 py-3 font-medium text-white" > Area + < th class="px-4 py-3 font-medium text-white" > Rotation + + + < tbody id = "dimensions-table-body" class="divide-y divide-gray-700" > + +
    Page #
    +
    + + `, 'n-up': () => ` -

    N-Up Page Arrangement

    -

    Combine multiple pages from your PDF onto a single sheet. This is great for creating booklets or proof sheets.

    - ${createFileInputHTML()} -
    + < h2 class="text-2xl font-bold text-white mb-4" > N - Up Page Arrangement + < p class="mb-6 text-gray-400" > Combine multiple pages from your PDF onto a single sheet.This is great for creating booklets or proof sheets.

    + ${ createFileInputHTML() } +
    - -
    -
    - - -
    -
    - -
    -
    + < div class="grid grid-cols-1 sm:grid-cols-2 gap-4" > +
    + + < select id = "output-orientation" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" > + + < option value = "portrait" > Portrait + < option value = "landscape" > Landscape + +
    + < div class="flex items-end pb-1" > + + + -
    -
    - -
    - -
    + < div class="border-t border-gray-700 pt-4 grid grid-cols-1 sm:grid-cols-2 gap-4" > +
    + +
    + < div id = "border-color-wrapper" class="hidden" > + + < input type = "color" id = "border-color" value = "#000000" class="w-full h-[42px] bg-gray-700 border border-gray-600 rounded-lg p-1 cursor-pointer" > + + - - - `, + < button id = "process-btn" class="btn-gradient w-full mt-6" > Create N - Up PDF + + `, 'duplicate-organize': () => ` -

    Page Manager

    -

    Drag pages to reorder them. Use the icon to duplicate a page or the icon to delete it.

    - ${createFileInputHTML()} -
    + < h2 class="text-2xl font-bold text-white mb-4" > Page Manager + < p class="mb-6 text-gray-400" > Drag pages to reorder them.Use the < i data - lucide="copy-plus" class="inline-block w-4 h-4 text-green-400" > icon to duplicate a page or the icon to delete it.

    + ${ createFileInputHTML() } +
    - - `, + `, 'combine-single-page': () => ` -

    Combine to a Single Page

    -

    Stitch all pages of your PDF together vertically or horizontally to create one continuous page.

    - ${createFileInputHTML()} -
    + < h2 class="text-2xl font-bold text-white mb-4" > Combine to a Single Page + < p class="mb-6 text-gray-400" > Stitch all pages of your PDF together vertically or horizontally to create one continuous page.

    + ${ createFileInputHTML() } +
    - - `, + < div id = "combine-options" class="hidden mt-6 space-y-4" > +
    + + < select id = "combine-orientation" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" > + + < option value = "horizontal" > Horizontal(Stack pages left to right) + +
    + + < div class="grid grid-cols-1 sm:grid-cols-2 gap-4" > +
    + + < input type = "number" id = "page-spacing" value = "18" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" > +
    + < div > + + < input type = "color" id = "background-color" value = "#FFFFFF" class="w-full h-[42px] bg-gray-700 border border-gray-600 rounded-lg p-1 cursor-pointer" > + + + + < div > + + + + < div id = "separator-options" class="hidden grid grid-cols-1 sm:grid-cols-2 gap-4 p-4 rounded-lg bg-gray-900 border border-gray-700" > +
    + + < input type = "number" id = "separator-thickness" value = "0.5" min = "0.1" max = "10" step = "0.1" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" > +
    + < div > + + < input type = "color" id = "separator-color" value = "#CCCCCC" class="w-full h-[42px] bg-gray-700 border border-gray-600 rounded-lg p-1 cursor-pointer" > + + + + < button id = "process-btn" class="btn-gradient w-full mt-6" > Combine Pages + + `, 'fix-dimensions': () => ` -

    Standardize Page Dimensions

    -

    Convert all pages in your PDF to a uniform size. Choose a standard format or define a custom dimension.

    - ${createFileInputHTML()} -
    - - + -
    - - -
    + < div id = "custom-size-wrapper" class="hidden p-4 rounded-lg bg-gray-900 border border-gray-700 grid grid-cols-3 gap-3" > +
    + + < input type = "number" id = "custom-width" value = "8.5" class="w-full bg-gray-700 border-gray-600 text-white rounded-lg p-2" > +
    + < div > + + < input type = "number" id = "custom-height" value = "11" class="w-full bg-gray-700 border-gray-600 text-white rounded-lg p-2" > + + < div > + + < select id = "custom-units" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2" > + + < option value = "mm" > Millimeters + + + - - - `, + < div > + + < div class="flex gap-4 p-2 rounded-lg bg-gray-900" > + + < label class="flex-1 flex items-center gap-2 p-3 rounded-md hover:bg-gray-700 cursor-pointer" > + +
    + Fill + < p class="text-xs text-gray-400" > Covers the page, may crop content.

    +
    + + + + + < div > + + < input type = "color" id = "background-color" value = "#FFFFFF" class="w-full h-[42px] bg-gray-700 border border-gray-600 rounded-lg p-1 cursor-pointer" > + + + < button id = "process-btn" class="btn-gradient w-full mt-6" > Standardize Pages + + `, 'change-background-color': () => ` -

    Change Background Color

    -

    Select a new background color for every page of your PDF.

    - ${createFileInputHTML()} -
    - - `, + < h2 class="text-2xl font-bold text-white mb-4" > Change Background Color + < p class="mb-6 text-gray-400" > Select a new background color for every page of your PDF.

    + ${ createFileInputHTML() } +
    + < div id = "change-background-color-options" class="hidden mt-6" > + + < input type = "color" id = "background-color" value = "#FFFFFF" class="w-full h-[42px] bg-gray-700 border border-gray-600 rounded-lg p-1 cursor-pointer" > + + + `, 'change-text-color': () => ` -

    Change Text Color

    -

    Change the color of dark text in your PDF. This process converts pages to images, so text will not be selectable in the final file.

    - ${createFileInputHTML()} -
    - + + < button id = "process-btn" class="btn-gradient w-full mt-6" > Apply Color & Download + + `, 'compare-pdfs': () => ` -

    Compare PDFs

    -

    Upload two files to visually compare them using either an overlay or a side-by-side view.

    - -
    -
    -
    - -

    Upload Original PDF

    -
    - -
    -
    -
    - -

    Upload Revised PDF

    -
    - -
    -
    + < h2 class="text-2xl font-bold text-white mb-4" > Compare PDFs + < p class="mb-6 text-gray-400" > Upload two files to visually compare them using either an overlay or a side-by - side view.

    - - `, + < div id = "compare-upload-area" class="grid grid-cols-1 md:grid-cols-2 gap-4" > +
    +
    + + < p class="mb-2 text-sm text-gray-400" > Upload Original PDF < /span>

    +
    + < input id = "file-input-1" type = "file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" accept = "application/pdf" > +
    + < div id = "drop-zone-2" class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-900 hover:bg-gray-700" > +
    + + < p class="mb-2 text-sm text-gray-400" > Upload Revised PDF < /span>

    +
    + < input id = "file-input-2" type = "file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" accept = "application/pdf" > + + + + < div id = "compare-viewer" class="hidden mt-6" > +
    + + Page < span id = "current-page-display-compare" > 1 < /span> of 1 + < button id = "next-page-compare" class="btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50" > +
    + < div class="bg-gray-700 p-1 rounded-md flex gap-1" > + + < button id = "view-mode-side" class="btn px-3 py-1 rounded text-sm font-semibold" > Side - by - Side +
    + < div class="border-l border-gray-600 h-6 mx-2" > + < div id = "overlay-controls" class="flex items-center gap-2" > + + < label for= "opacity-slider" class= "text-sm font-medium text-gray-300" > Opacity: + < input type = "range" id = "opacity-slider" min = "0" max = "1" step = "0.05" value = "0.5" class="w-24" > + + < div id = "side-by-side-controls" class="hidden flex items-center gap-2" > + + + + < div id = "compare-viewer-wrapper" class="compare-viewer-wrapper overlay-mode" > +
    +
    + + + `, 'ocr-pdf': () => ` -

    OCR PDF

    -

    Convert scanned PDFs into searchable documents. Select one or more languages present in your file for the best results.

    + < h2 class="text-2xl font-bold text-white mb-4" > OCR PDF + < p class="mb-6 text-gray-400" > Convert scanned PDFs into searchable documents.Select one or more languages present in your file for the best results.

    + + < div class="p-3 bg-gray-900 rounded-lg border border-gray-700 mb-6" > +

    How it works:

    +
      +
    • Extract Text: Uses Tesseract OCR to recognize text from scanned images or PDFs.
    • +
    • Searchable Output: Creates a new PDF with an invisible text layer, making your document fully searchable while preserving the original appearance.
    • +
    • Character Filtering: Use whitelists to filter out unwanted characters and improve accuracy for specific document types (invoices, forms, etc.).
    • +
    • Multi - language Support: Select multiple languages for documents containing mixed language content.
    • +
    + -
    -

    How it works:

    -
      -
    • Extract Text: Uses Tesseract OCR to recognize text from scanned images or PDFs.
    • -
    • Searchable Output: Creates a new PDF with an invisible text layer, making your document fully searchable while preserving the original appearance.
    • -
    • Character Filtering: Use whitelists to filter out unwanted characters and improve accuracy for specific document types (invoices, forms, etc.).
    • -
    • Multi-language Support: Select multiple languages for documents containing mixed language content.
    • -
    -
    - - ${createFileInputHTML()} -
    - - + < p class="text-xs text-gray-500 mt-1" > Selected: None < /span>

    + - + < !--Advanced settings section-- > +
    + + Advanced Settings(Recommended to improve accuracy) + < i data - lucide="chevron-down" class="w-4 h-4 transition-transform details-icon" > + + < div class="mt-4 space-y-4" > + - - -
    - -
    + < h2 class="text-2xl font-bold text-white mb-4" > Sign PDF + < p class="mb-6 text-gray-400" > Upload a PDF to sign it using the built-in PDF.js viewer.Look for the < strong > signature / pen tool < /strong> in the toolbar to add your signature.

    + ${ createFileInputHTML() } + < div id = "file-display-area" class="mt-4 space-y-2" > - - -`, + < div id = "signature-editor" class="hidden mt-6" > +
    + -
    - - -`, + ${ createFileInputHTML() } +
    + < div id = "form-filler-options" class="hidden mt-6" > +
    + + + + +
    + +
    +
    + + + + + + +
    +
    + + +

    Split PDF

    +

    + Extract pages from a PDF using various methods. +

    + + +
    +
    + +

    Click to select a file or + drag and + drop

    +

    A single PDF file

    +

    Your files never leave your device.

    +
    + +
    + +
    + + + +
    +
    + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index d1bef63..9bd9771 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -63,6 +63,7 @@ export default defineConfig(({ mode }) => ({ 'form-creator': resolve(__dirname, 'src/pages/form-creator.html'), 'repair-pdf': resolve(__dirname, 'src/pages/repair-pdf.html'), 'merge-pdf': resolve(__dirname, 'src/pages/merge-pdf.html'), + 'split-pdf': resolve(__dirname, 'src/pages/split-pdf.html'), }, }, }, From 96200847890ee110f3cdb059ae44e0baef054816 Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Thu, 4 Dec 2025 14:32:37 +0530 Subject: [PATCH 05/21] feat: Add dedicated compress PDF page and reimplement compression logic. --- src/js/config/tools.ts | 4 +- src/js/logic/compress-pdf-page.ts | 562 ++++++++++++ src/js/logic/compress.ts | 404 --------- src/js/logic/index.ts | 4 +- src/js/ui.ts | 1334 ++++++++++++++--------------- src/pages/compress-pdf.html | 251 ++++++ vite.config.ts | 1 + 7 files changed, 1485 insertions(+), 1075 deletions(-) create mode 100644 src/js/logic/compress-pdf-page.ts delete mode 100644 src/js/logic/compress.ts create mode 100644 src/pages/compress-pdf.html diff --git a/src/js/config/tools.ts b/src/js/config/tools.ts index f73abee..d81c640 100644 --- a/src/js/config/tools.ts +++ b/src/js/config/tools.ts @@ -22,7 +22,7 @@ export const categories = [ subtitle: 'Extract a range of pages into a new PDF.', }, { - id: 'compress', + href: '/src/pages/compress-pdf.html', name: 'Compress PDF', icon: 'zap', subtitle: 'Reduce the file size of your PDF.', @@ -438,7 +438,7 @@ export const categories = [ name: 'Optimize & Repair', tools: [ { - id: 'compress', + href: '/src/pages/compress-pdf.html', name: 'Compress PDF', icon: 'zap', subtitle: 'Reduce the file size of your PDF.', diff --git a/src/js/logic/compress-pdf-page.ts b/src/js/logic/compress-pdf-page.ts new file mode 100644 index 0000000..4eac562 --- /dev/null +++ b/src/js/logic/compress-pdf-page.ts @@ -0,0 +1,562 @@ +import { showLoader, hideLoader, showAlert } from '../ui.js'; +import { + downloadFile, + readFileAsArrayBuffer, + formatBytes, + getPDFDocument, +} from '../utils/helpers.js'; +import { state } from '../state.js'; +import { createIcons, icons } from 'lucide'; +import * as pdfjsLib from 'pdfjs-dist'; +import { PDFDocument, PDFName, PDFDict, PDFStream, PDFNumber } from 'pdf-lib'; + +pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); + +function dataUrlToBytes(dataUrl: any) { + const base64 = dataUrl.split(',')[1]; + const binaryString = atob(base64); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; +} + +async function performSmartCompression(arrayBuffer: any, settings: any) { + const pdfDoc = await PDFDocument.load(arrayBuffer, { + ignoreEncryption: true, + }); + const pages = pdfDoc.getPages(); + + if (settings.removeMetadata) { + try { + pdfDoc.setTitle(''); + pdfDoc.setAuthor(''); + pdfDoc.setSubject(''); + pdfDoc.setKeywords([]); + pdfDoc.setCreator(''); + pdfDoc.setProducer(''); + } catch (e) { + console.warn('Could not remove metadata:', e); + } + } + + for (let i = 0; i < pages.length; i++) { + const page = pages[i]; + const resources = page.node.Resources(); + if (!resources) continue; + + const xobjects = resources.lookup(PDFName.of('XObject')); + if (!(xobjects instanceof PDFDict)) continue; + + for (const [key, value] of xobjects.entries()) { + const stream = pdfDoc.context.lookup(value); + if ( + !(stream instanceof PDFStream) || + stream.dict.get(PDFName.of('Subtype')) !== PDFName.of('Image') + ) + continue; + + try { + const imageBytes = stream.getContents(); + if (imageBytes.length < settings.skipSize) continue; + + const width = + stream.dict.get(PDFName.of('Width')) instanceof PDFNumber + ? (stream.dict.get(PDFName.of('Width')) as PDFNumber).asNumber() + : 0; + const height = + stream.dict.get(PDFName.of('Height')) instanceof PDFNumber + ? (stream.dict.get(PDFName.of('Height')) as PDFNumber).asNumber() + : 0; + const bitsPerComponent = + stream.dict.get(PDFName.of('BitsPerComponent')) instanceof PDFNumber + ? ( + stream.dict.get(PDFName.of('BitsPerComponent')) as PDFNumber + ).asNumber() + : 8; + + if (width > 0 && height > 0) { + let newWidth = width; + let newHeight = height; + + const scaleFactor = settings.scaleFactor || 1.0; + newWidth = Math.floor(width * scaleFactor); + newHeight = Math.floor(height * scaleFactor); + + if (newWidth > settings.maxWidth || newHeight > settings.maxHeight) { + const aspectRatio = newWidth / newHeight; + if (newWidth > newHeight) { + newWidth = Math.min(newWidth, settings.maxWidth); + newHeight = newWidth / aspectRatio; + } else { + newHeight = Math.min(newHeight, settings.maxHeight); + newWidth = newHeight * aspectRatio; + } + } + + const minDim = settings.minDimension || 50; + if (newWidth < minDim || newHeight < minDim) continue; + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.width = Math.floor(newWidth); + canvas.height = Math.floor(newHeight); + + const img = new Image(); + const imageUrl = URL.createObjectURL( + new Blob([new Uint8Array(imageBytes)]) + ); + + await new Promise((resolve, reject) => { + img.onload = resolve; + img.onerror = reject; + img.src = imageUrl; + }); + + ctx.imageSmoothingEnabled = settings.smoothing !== false; + ctx.imageSmoothingQuality = settings.smoothingQuality || 'medium'; + + if (settings.grayscale) { + ctx.filter = 'grayscale(100%)'; + } else if (settings.contrast) { + ctx.filter = `contrast(${settings.contrast}) brightness(${settings.brightness || 1})`; + } + + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + + let bestBytes = null; + let bestSize = imageBytes.length; + + const jpegDataUrl = canvas.toDataURL('image/jpeg', settings.quality); + const jpegBytes = dataUrlToBytes(jpegDataUrl); + if (jpegBytes.length < bestSize) { + bestBytes = jpegBytes; + bestSize = jpegBytes.length; + } + + if (settings.tryWebP) { + try { + const webpDataUrl = canvas.toDataURL( + 'image/webp', + settings.quality + ); + const webpBytes = dataUrlToBytes(webpDataUrl); + if (webpBytes.length < bestSize) { + bestBytes = webpBytes; + bestSize = webpBytes.length; + } + } catch (e) { + /* WebP not supported */ + } + } + + if (bestBytes && bestSize < imageBytes.length * settings.threshold) { + (stream as any).contents = bestBytes; + stream.dict.set(PDFName.of('Length'), PDFNumber.of(bestSize)); + stream.dict.set(PDFName.of('Width'), PDFNumber.of(canvas.width)); + stream.dict.set(PDFName.of('Height'), PDFNumber.of(canvas.height)); + stream.dict.set(PDFName.of('Filter'), PDFName.of('DCTDecode')); + stream.dict.delete(PDFName.of('DecodeParms')); + stream.dict.set(PDFName.of('BitsPerComponent'), PDFNumber.of(8)); + + if (settings.grayscale) { + stream.dict.set( + PDFName.of('ColorSpace'), + PDFName.of('DeviceGray') + ); + } + } + URL.revokeObjectURL(imageUrl); + } + } catch (error) { + console.warn('Skipping an uncompressible image in smart mode:', error); + } + } + } + + const saveOptions = { + useObjectStreams: settings.useObjectStreams !== false, + addDefaultPage: false, + objectsPerTick: settings.objectsPerTick || 50, + }; + + return await pdfDoc.save(saveOptions); +} + +async function performLegacyCompression(arrayBuffer: any, settings: any) { + const pdfJsDoc = await getPDFDocument({ data: arrayBuffer }).promise; + const newPdfDoc = await PDFDocument.create(); + + for (let i = 1; i <= pdfJsDoc.numPages; i++) { + const page = await pdfJsDoc.getPage(i); + const viewport = page.getViewport({ scale: settings.scale }); + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + canvas.height = viewport.height; + canvas.width = viewport.width; + + await page.render({ canvasContext: context, viewport, canvas: canvas }) + .promise; + + const jpegBlob = await new Promise((resolve) => + canvas.toBlob(resolve, 'image/jpeg', settings.quality) + ); + const jpegBytes = await (jpegBlob as Blob).arrayBuffer(); + const jpegImage = await newPdfDoc.embedJpg(jpegBytes); + const newPage = newPdfDoc.addPage([viewport.width, viewport.height]); + newPage.drawImage(jpegImage, { + x: 0, + y: 0, + width: viewport.width, + height: viewport.height, + }); + } + return await newPdfDoc.save(); +} + +document.addEventListener('DOMContentLoaded', () => { + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const processBtn = document.getElementById('process-btn'); + const fileDisplayArea = document.getElementById('file-display-area'); + const compressOptions = document.getElementById('compress-options'); + const fileControls = document.getElementById('file-controls'); + const addMoreBtn = document.getElementById('add-more-btn'); + const clearFilesBtn = document.getElementById('clear-files-btn'); + const backBtn = document.getElementById('back-to-tools'); + + if (backBtn) { + backBtn.addEventListener('click', () => { + window.location.href = '/'; + }); + } + + const updateUI = async () => { + if (!fileDisplayArea || !compressOptions || !processBtn || !fileControls) return; + + if (state.files.length > 0) { + fileDisplayArea.innerHTML = ''; + + for (let index = 0; index < state.files.length; index++) { + const file = state.files[index]; + const fileDiv = document.createElement('div'); + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col overflow-hidden'; + + const nameSizeContainer = document.createElement('div'); + nameSizeContainer.className = 'flex items-center gap-2'; + + const nameSpan = document.createElement('span'); + nameSpan.className = 'truncate font-medium text-gray-200'; + nameSpan.textContent = file.name; + + const sizeSpan = document.createElement('span'); + sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs'; + sizeSpan.textContent = `(${formatBytes(file.size)})`; + + nameSizeContainer.append(nameSpan, sizeSpan); + + const pagesSpan = document.createElement('span'); + pagesSpan.className = 'text-xs text-gray-500 mt-0.5'; + pagesSpan.textContent = 'Loading pages...'; + + infoContainer.append(nameSizeContainer, pagesSpan); + + const removeBtn = document.createElement('button'); + removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.innerHTML = ''; + removeBtn.onclick = () => { + state.files = state.files.filter((_, i) => i !== index); + updateUI(); + }; + + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + + try { + const arrayBuffer = await readFileAsArrayBuffer(file); + const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise; + pagesSpan.textContent = `${pdfDoc.numPages} Pages`; + } catch (error) { + console.error('Error loading PDF:', error); + pagesSpan.textContent = 'Could not load page count'; + } + } + + createIcons({ icons }); + fileControls.classList.remove('hidden'); + compressOptions.classList.remove('hidden'); + (processBtn as HTMLButtonElement).disabled = false; + } else { + fileDisplayArea.innerHTML = ''; + fileControls.classList.add('hidden'); + compressOptions.classList.add('hidden'); + (processBtn as HTMLButtonElement).disabled = true; + } + }; + + const resetState = () => { + state.files = []; + state.pdfDoc = null; + + const compressionLevel = document.getElementById('compression-level') as HTMLSelectElement; + if (compressionLevel) compressionLevel.value = 'balanced'; + + const compressionAlgorithm = document.getElementById('compression-algorithm') as HTMLSelectElement; + if (compressionAlgorithm) compressionAlgorithm.value = 'vector'; + + updateUI(); + }; + + const compress = async () => { + const level = (document.getElementById('compression-level') as HTMLSelectElement).value; + const algorithm = (document.getElementById('compression-algorithm') as HTMLSelectElement).value; + + const settings = { + balanced: { + smart: { + quality: 0.5, + threshold: 0.95, + maxWidth: 1800, + maxHeight: 1800, + skipSize: 3000, + }, + legacy: { scale: 1.5, quality: 0.6 }, + }, + 'high-quality': { + smart: { + quality: 0.7, + threshold: 0.98, + maxWidth: 2500, + maxHeight: 2500, + skipSize: 5000, + }, + legacy: { scale: 2.0, quality: 0.9 }, + }, + 'small-size': { + smart: { + quality: 0.3, + threshold: 0.95, + maxWidth: 1200, + maxHeight: 1200, + skipSize: 2000, + }, + legacy: { scale: 1.2, quality: 0.4 }, + }, + extreme: { + smart: { + quality: 0.1, + threshold: 0.95, + maxWidth: 1000, + maxHeight: 1000, + skipSize: 1000, + }, + legacy: { scale: 1.0, quality: 0.2 }, + }, + }; + + const smartSettings = { ...settings[level].smart, removeMetadata: true }; + const legacySettings = settings[level].legacy; + + try { + if (state.files.length === 0) { + showAlert('No Files', 'Please select at least one PDF file.'); + hideLoader(); + return; + } + + if (state.files.length === 1) { + const originalFile = state.files[0]; + const arrayBuffer = await readFileAsArrayBuffer(originalFile); + + let resultBytes; + let usedMethod; + + if (algorithm === 'vector') { + showLoader('Running Vector (Smart) compression...'); + resultBytes = await performSmartCompression(arrayBuffer, smartSettings); + usedMethod = 'Vector'; + } else if (algorithm === 'photon') { + showLoader('Running Photon (Rasterize) compression...'); + resultBytes = await performLegacyCompression(arrayBuffer, legacySettings); + usedMethod = 'Photon'; + } else { + showLoader('Running Automatic (Vector first)...'); + const vectorResultBytes = await performSmartCompression( + arrayBuffer, + smartSettings + ); + + if (vectorResultBytes.length < originalFile.size) { + resultBytes = vectorResultBytes; + usedMethod = 'Vector (Automatic)'; + } else { + showAlert('Vector failed to reduce size. Trying Photon...', 'info'); + showLoader('Running Automatic (Photon fallback)...'); + resultBytes = await performLegacyCompression( + arrayBuffer, + legacySettings + ); + usedMethod = 'Photon (Automatic)'; + } + } + + const originalSize = formatBytes(originalFile.size); + const compressedSize = formatBytes(resultBytes.length); + const savings = originalFile.size - resultBytes.length; + const savingsPercent = + savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0; + + downloadFile( + new Blob([resultBytes], { type: 'application/pdf' }), + 'compressed-final.pdf' + ); + + hideLoader(); + + if (savings > 0) { + showAlert( + 'Compression Complete', + `Method: ${usedMethod}. File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).`, + 'success', + () => resetState() + ); + } else { + showAlert( + 'Compression Finished', + `Method: ${usedMethod}. Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`, + 'warning', + () => resetState() + ); + } + } else { + showLoader('Compressing multiple PDFs...'); + const JSZip = (await import('jszip')).default; + const zip = new JSZip(); + let totalOriginalSize = 0; + let totalCompressedSize = 0; + + for (let i = 0; i < state.files.length; i++) { + const file = state.files[i]; + showLoader(`Compressing ${i + 1}/${state.files.length}: ${file.name}...`); + const arrayBuffer = await readFileAsArrayBuffer(file); + totalOriginalSize += file.size; + + let resultBytes; + if (algorithm === 'vector') { + resultBytes = await performSmartCompression(arrayBuffer, smartSettings); + } else if (algorithm === 'photon') { + resultBytes = await performLegacyCompression(arrayBuffer, legacySettings); + } else { + const vectorResultBytes = await performSmartCompression( + arrayBuffer, + smartSettings + ); + resultBytes = vectorResultBytes.length < file.size + ? vectorResultBytes + : await performLegacyCompression(arrayBuffer, legacySettings); + } + + totalCompressedSize += resultBytes.length; + const baseName = file.name.replace(/\.pdf$/i, ''); + zip.file(`${baseName}_compressed.pdf`, resultBytes); + } + + const zipBlob = await zip.generateAsync({ type: 'blob' }); + const totalSavings = totalOriginalSize - totalCompressedSize; + const totalSavingsPercent = + totalSavings > 0 + ? ((totalSavings / totalOriginalSize) * 100).toFixed(1) + : 0; + + downloadFile(zipBlob, 'compressed-pdfs.zip'); + + hideLoader(); + + if (totalSavings > 0) { + showAlert( + 'Compression Complete', + `Compressed ${state.files.length} PDF(s). Total size reduced from ${formatBytes(totalOriginalSize)} to ${formatBytes(totalCompressedSize)} (Saved ${totalSavingsPercent}%).`, + 'success', + () => resetState() + ); + } else { + showAlert( + 'Compression Finished', + `Compressed ${state.files.length} PDF(s). Total size: ${formatBytes(totalCompressedSize)}.`, + 'info', + () => resetState() + ); + } + } + } catch (e: any) { + hideLoader(); + showAlert( + 'Error', + `An error occurred during compression. Error: ${e.message}` + ); + } + }; + + const handleFileSelect = (files: FileList | null) => { + if (files && files.length > 0) { + state.files = [...state.files, ...Array.from(files)]; + updateUI(); + } + }; + + if (fileInput && dropZone) { + fileInput.addEventListener('change', (e) => { + handleFileSelect((e.target as HTMLInputElement).files); + fileInput.value = ''; + }); + + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); + }); + + dropZone.addEventListener('dragleave', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + const files = e.dataTransfer?.files; + if (files && files.length > 0) { + const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')); + if (pdfFiles.length > 0) { + const dataTransfer = new DataTransfer(); + pdfFiles.forEach(f => dataTransfer.items.add(f)); + handleFileSelect(dataTransfer.files); + } + } + }); + + dropZone.addEventListener('click', () => { + fileInput.click(); + }); + } + + if (addMoreBtn) { + addMoreBtn.addEventListener('click', () => { + fileInput.click(); + }); + } + + if (clearFilesBtn) { + clearFilesBtn.addEventListener('click', () => { + resetState(); + }); + } + + if (processBtn) { + processBtn.addEventListener('click', compress); + } +}); diff --git a/src/js/logic/compress.ts b/src/js/logic/compress.ts deleted file mode 100644 index 2827189..0000000 --- a/src/js/logic/compress.ts +++ /dev/null @@ -1,404 +0,0 @@ -import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { - downloadFile, - readFileAsArrayBuffer, - formatBytes, - getPDFDocument, -} from '../utils/helpers.js'; -import { state } from '../state.js'; -import * as pdfjsLib from 'pdfjs-dist'; -import { PDFDocument, PDFName, PDFDict, PDFStream, PDFNumber } from 'pdf-lib'; - -pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); - -function dataUrlToBytes(dataUrl: any) { - const base64 = dataUrl.split(',')[1]; - const binaryString = atob(base64); - const len = binaryString.length; - const bytes = new Uint8Array(len); - for (let i = 0; i < len; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes; -} - -async function performSmartCompression(arrayBuffer: any, settings: any) { - const pdfDoc = await PDFDocument.load(arrayBuffer, { - ignoreEncryption: true, - }); - const pages = pdfDoc.getPages(); - - if (settings.removeMetadata) { - try { - pdfDoc.setTitle(''); - pdfDoc.setAuthor(''); - pdfDoc.setSubject(''); - pdfDoc.setKeywords([]); - pdfDoc.setCreator(''); - pdfDoc.setProducer(''); - } catch (e) { - console.warn('Could not remove metadata:', e); - } - } - - for (let i = 0; i < pages.length; i++) { - const page = pages[i]; - const resources = page.node.Resources(); - if (!resources) continue; - - const xobjects = resources.lookup(PDFName.of('XObject')); - if (!(xobjects instanceof PDFDict)) continue; - - for (const [key, value] of xobjects.entries()) { - const stream = pdfDoc.context.lookup(value); - if ( - !(stream instanceof PDFStream) || - stream.dict.get(PDFName.of('Subtype')) !== PDFName.of('Image') - ) - continue; - - try { - const imageBytes = stream.getContents(); - if (imageBytes.length < settings.skipSize) continue; - - const width = - stream.dict.get(PDFName.of('Width')) instanceof PDFNumber - ? (stream.dict.get(PDFName.of('Width')) as PDFNumber).asNumber() - : 0; - const height = - stream.dict.get(PDFName.of('Height')) instanceof PDFNumber - ? (stream.dict.get(PDFName.of('Height')) as PDFNumber).asNumber() - : 0; - const bitsPerComponent = - stream.dict.get(PDFName.of('BitsPerComponent')) instanceof PDFNumber - ? ( - stream.dict.get(PDFName.of('BitsPerComponent')) as PDFNumber - ).asNumber() - : 8; - - if (width > 0 && height > 0) { - let newWidth = width; - let newHeight = height; - - const scaleFactor = settings.scaleFactor || 1.0; - newWidth = Math.floor(width * scaleFactor); - newHeight = Math.floor(height * scaleFactor); - - if (newWidth > settings.maxWidth || newHeight > settings.maxHeight) { - const aspectRatio = newWidth / newHeight; - if (newWidth > newHeight) { - newWidth = Math.min(newWidth, settings.maxWidth); - newHeight = newWidth / aspectRatio; - } else { - newHeight = Math.min(newHeight, settings.maxHeight); - newWidth = newHeight * aspectRatio; - } - } - - const minDim = settings.minDimension || 50; - if (newWidth < minDim || newHeight < minDim) continue; - - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - canvas.width = Math.floor(newWidth); - canvas.height = Math.floor(newHeight); - - const img = new Image(); - const imageUrl = URL.createObjectURL( - new Blob([new Uint8Array(imageBytes)]) - ); - - await new Promise((resolve, reject) => { - img.onload = resolve; - img.onerror = reject; - img.src = imageUrl; - }); - - ctx.imageSmoothingEnabled = settings.smoothing !== false; - ctx.imageSmoothingQuality = settings.smoothingQuality || 'medium'; - - if (settings.grayscale) { - ctx.filter = 'grayscale(100%)'; - } else if (settings.contrast) { - ctx.filter = `contrast(${settings.contrast}) brightness(${settings.brightness || 1})`; - } - - ctx.drawImage(img, 0, 0, canvas.width, canvas.height); - - let bestBytes = null; - let bestSize = imageBytes.length; - - const jpegDataUrl = canvas.toDataURL('image/jpeg', settings.quality); - const jpegBytes = dataUrlToBytes(jpegDataUrl); - if (jpegBytes.length < bestSize) { - bestBytes = jpegBytes; - bestSize = jpegBytes.length; - } - - if (settings.tryWebP) { - try { - const webpDataUrl = canvas.toDataURL( - 'image/webp', - settings.quality - ); - const webpBytes = dataUrlToBytes(webpDataUrl); - if (webpBytes.length < bestSize) { - bestBytes = webpBytes; - bestSize = webpBytes.length; - } - } catch (e) { - /* WebP not supported */ - } - } - - if (bestBytes && bestSize < imageBytes.length * settings.threshold) { - (stream as any).contents = bestBytes; - stream.dict.set(PDFName.of('Length'), PDFNumber.of(bestSize)); - stream.dict.set(PDFName.of('Width'), PDFNumber.of(canvas.width)); - stream.dict.set(PDFName.of('Height'), PDFNumber.of(canvas.height)); - stream.dict.set(PDFName.of('Filter'), PDFName.of('DCTDecode')); - stream.dict.delete(PDFName.of('DecodeParms')); - stream.dict.set(PDFName.of('BitsPerComponent'), PDFNumber.of(8)); - - if (settings.grayscale) { - stream.dict.set( - PDFName.of('ColorSpace'), - PDFName.of('DeviceGray') - ); - } - } - URL.revokeObjectURL(imageUrl); - } - } catch (error) { - console.warn('Skipping an uncompressible image in smart mode:', error); - } - } - } - - const saveOptions = { - useObjectStreams: settings.useObjectStreams !== false, - addDefaultPage: false, - objectsPerTick: settings.objectsPerTick || 50, - }; - - return await pdfDoc.save(saveOptions); -} - -async function performLegacyCompression(arrayBuffer: any, settings: any) { - const pdfJsDoc = await getPDFDocument({ data: arrayBuffer }).promise; - const newPdfDoc = await PDFDocument.create(); - - for (let i = 1; i <= pdfJsDoc.numPages; i++) { - const page = await pdfJsDoc.getPage(i); - const viewport = page.getViewport({ scale: settings.scale }); - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - canvas.height = viewport.height; - canvas.width = viewport.width; - - await page.render({ canvasContext: context, viewport, canvas: canvas }) - .promise; - - const jpegBlob = await new Promise((resolve) => - canvas.toBlob(resolve, 'image/jpeg', settings.quality) - ); - // @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message - const jpegBytes = await jpegBlob.arrayBuffer(); - const jpegImage = await newPdfDoc.embedJpg(jpegBytes); - const newPage = newPdfDoc.addPage([viewport.width, viewport.height]); - newPage.drawImage(jpegImage, { - x: 0, - y: 0, - width: viewport.width, - height: viewport.height, - }); - } - return await newPdfDoc.save(); -} - -export async function compress() { - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const level = document.getElementById('compression-level').value; - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const algorithm = document.getElementById('compression-algorithm').value; - - const settings = { - balanced: { - smart: { - quality: 0.5, - threshold: 0.95, - maxWidth: 1800, - maxHeight: 1800, - skipSize: 3000, - }, - legacy: { scale: 1.5, quality: 0.6 }, - }, - 'high-quality': { - smart: { - quality: 0.7, - threshold: 0.98, - maxWidth: 2500, - maxHeight: 2500, - skipSize: 5000, - }, - legacy: { scale: 2.0, quality: 0.9 }, - }, - 'small-size': { - smart: { - quality: 0.3, - threshold: 0.95, - maxWidth: 1200, - maxHeight: 1200, - skipSize: 2000, - }, - legacy: { scale: 1.2, quality: 0.4 }, - }, - extreme: { - smart: { - quality: 0.1, - threshold: 0.95, - maxWidth: 1000, - maxHeight: 1000, - skipSize: 1000, - }, - legacy: { scale: 1.0, quality: 0.2 }, - }, - }; - - const smartSettings = { ...settings[level].smart, removeMetadata: true }; - const legacySettings = settings[level].legacy; - - try { - if (state.files.length === 0) { - showAlert('No Files', 'Please select at least one PDF file.'); - hideLoader(); - return; - } - - if (state.files.length === 1) { - const originalFile = state.files[0]; - const arrayBuffer = await readFileAsArrayBuffer(originalFile); - - let resultBytes; - let usedMethod; - - if (algorithm === 'vector') { - showLoader('Running Vector (Smart) compression...'); - resultBytes = await performSmartCompression(arrayBuffer, smartSettings); - usedMethod = 'Vector'; - } else if (algorithm === 'photon') { - showLoader('Running Photon (Rasterize) compression...'); - resultBytes = await performLegacyCompression(arrayBuffer, legacySettings); - usedMethod = 'Photon'; - } else { - showLoader('Running Automatic (Vector first)...'); - const vectorResultBytes = await performSmartCompression( - arrayBuffer, - smartSettings - ); - - if (vectorResultBytes.length < originalFile.size) { - resultBytes = vectorResultBytes; - usedMethod = 'Vector (Automatic)'; - } else { - showAlert('Vector failed to reduce size. Trying Photon...', 'info'); - showLoader('Running Automatic (Photon fallback)...'); - resultBytes = await performLegacyCompression( - arrayBuffer, - legacySettings - ); - usedMethod = 'Photon (Automatic)'; - } - } - - const originalSize = formatBytes(originalFile.size); - const compressedSize = formatBytes(resultBytes.length); - const savings = originalFile.size - resultBytes.length; - const savingsPercent = - savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0; - - if (savings > 0) { - showAlert( - 'Compression Complete', - `Method: **${usedMethod}**. ` + - `File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).` - ); - } else { - showAlert( - 'Compression Finished', - `Method: **${usedMethod}**. ` + - `Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`, - // @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3. - 'warning' - ); - } - - downloadFile( - new Blob([resultBytes], { type: 'application/pdf' }), - 'compressed-final.pdf' - ); - } else { - showLoader('Compressing multiple PDFs...'); - const JSZip = (await import('jszip')).default; - const zip = new JSZip(); - let totalOriginalSize = 0; - let totalCompressedSize = 0; - - for (let i = 0; i < state.files.length; i++) { - const file = state.files[i]; - showLoader(`Compressing ${i + 1}/${state.files.length}: ${file.name}...`); - const arrayBuffer = await readFileAsArrayBuffer(file); - totalOriginalSize += file.size; - - let resultBytes; - if (algorithm === 'vector') { - resultBytes = await performSmartCompression(arrayBuffer, smartSettings); - } else if (algorithm === 'photon') { - resultBytes = await performLegacyCompression(arrayBuffer, legacySettings); - } else { - const vectorResultBytes = await performSmartCompression( - arrayBuffer, - smartSettings - ); - resultBytes = vectorResultBytes.length < file.size - ? vectorResultBytes - : await performLegacyCompression(arrayBuffer, legacySettings); - } - - totalCompressedSize += resultBytes.length; - const baseName = file.name.replace(/\.pdf$/i, ''); - zip.file(`${baseName}_compressed.pdf`, resultBytes); - } - - const zipBlob = await zip.generateAsync({ type: 'blob' }); - const totalSavings = totalOriginalSize - totalCompressedSize; - const totalSavingsPercent = - totalSavings > 0 - ? ((totalSavings / totalOriginalSize) * 100).toFixed(1) - : 0; - - if (totalSavings > 0) { - showAlert( - 'Compression Complete', - `Compressed ${state.files.length} PDF(s). ` + - `Total size reduced from ${formatBytes(totalOriginalSize)} to ${formatBytes(totalCompressedSize)} (Saved ${totalSavingsPercent}%).` - ); - } else { - showAlert( - 'Compression Finished', - `Compressed ${state.files.length} PDF(s). ` + - `Total size: ${formatBytes(totalCompressedSize)}.` - ); - } - - downloadFile(zipBlob, 'compressed-pdfs.zip'); - } - } catch (e) { - showAlert( - 'Error', - `An error occurred during compression. Error: ${e.message}` - ); - } finally { - hideLoader(); - } -} diff --git a/src/js/logic/index.ts b/src/js/logic/index.ts index 033d1be..4c23184 100644 --- a/src/js/logic/index.ts +++ b/src/js/logic/index.ts @@ -8,7 +8,7 @@ import { addPageNumbers } from './add-page-numbers.js'; import { pdfToJpg } from './pdf-to-jpg.js'; import { jpgToPdf } from './jpg-to-pdf.js'; import { scanToPdf } from './scan-to-pdf.js'; -import { compress } from './compress.js'; + import { pdfToGreyscale } from './pdf-to-greyscale.js'; import { pdfToZip } from './pdf-to-zip.js'; import { editMetadata } from './edit-metadata.js'; @@ -81,7 +81,7 @@ export const toolLogic = { 'pdf-to-jpg': pdfToJpg, 'jpg-to-pdf': jpgToPdf, 'scan-to-pdf': scanToPdf, - compress, + 'pdf-to-greyscale': pdfToGreyscale, 'pdf-to-zip': pdfToZip, 'edit-metadata': editMetadata, diff --git a/src/js/ui.ts b/src/js/ui.ts index 4fd847b..f0fab97 100644 --- a/src/js/ui.ts +++ b/src/js/ui.ts @@ -447,143 +447,143 @@ export const toolTemplates = { encrypt: () => ` - < h2 class="text-2xl font-bold text-white mb-4" > Encrypt PDF - < p class="mb-6 text-gray-400" > Add 256 - bit AES password protection to your PDF.

    +

    Encrypt PDF

    +

    Add 256 - bit AES password protection to your PDF.

    ${ createFileInputHTML() }
    - < div id = "encrypt-options" class="hidden space-y-4 mt-6" > + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index dfe9c92..d91bcdd 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -66,6 +66,8 @@ export default defineConfig(({ mode }) => ({ 'split-pdf': resolve(__dirname, 'src/pages/split-pdf.html'), 'compress-pdf': resolve(__dirname, 'src/pages/compress-pdf.html'), 'edit-pdf': resolve(__dirname, 'src/pages/edit-pdf.html'), + 'jpg-to-pdf': resolve(__dirname, 'src/pages/jpg-to-pdf.html'), + }, }, }, From b279c05281a0993a7d79ba603f28e38f74160625 Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Thu, 4 Dec 2025 23:53:00 +0530 Subject: [PATCH 08/21] feat: Add subdirectory hosting support and fix asset path resolution - Update README with comprehensive subdirectory hosting instructions and BASE_URL configuration guide - Convert absolute asset paths to relative paths in index.html for proper subdirectory deployment - Update all worker script imports to use relative paths instead of absolute paths - Fix favicon and image references to work correctly when hosted in nested directories - Normalize whitespace and formatting across worker files for consistency - Update vite.config.ts to properly handle BASE_URL configuration for subdirectory deployments - Ensure all tool pages and logic files maintain compatibility with subdirectory hosting - Enable BentoPDF to be deployed at any URL path (e.g., example.com/tools/bentopdf/) without breaking asset loading --- README.md | 26 +++++++++++++- index.html | 16 ++++----- public/workers/add-attachments.worker.js | 2 +- public/workers/alternate-merge.worker.js | 2 +- public/workers/edit-attachments.worker.js | 30 ++++++++-------- public/workers/extract-attachments.worker.js | 8 ++--- public/workers/merge.worker.js | 2 +- src/js/config/tools.ts | 36 ++++++++++---------- src/js/logic/add-attachments.ts | 2 +- src/js/logic/add-stamps.ts | 2 +- src/js/logic/alternate-merge.ts | 2 +- src/js/logic/bookmark-pdf.ts | 4 +-- src/js/logic/compress-pdf-page.ts | 2 +- src/js/logic/edit-attachments.ts | 2 +- src/js/logic/edit-pdf-page.ts | 4 +-- src/js/logic/extract-attachments.ts | 2 +- src/js/logic/form-creator.ts | 2 +- src/js/logic/jpg-to-pdf-page.ts | 2 +- src/js/logic/json-to-pdf.ts | 4 +-- src/js/logic/merge-pdf-page.ts | 4 +-- src/js/logic/pdf-multi-tool.ts | 2 +- src/js/logic/pdf-to-json.ts | 4 +-- src/js/logic/repair-pdf-page.ts | 2 +- src/js/logic/split-pdf-page.ts | 2 +- src/js/logic/table-of-contents.ts | 4 +-- src/pages/add-stamps.html | 14 ++++---- src/pages/bookmark.html | 22 ++++++------ src/pages/compress-pdf.html | 12 +++---- src/pages/edit-pdf.html | 22 ++++++------ src/pages/form-creator.html | 14 ++++---- src/pages/jpg-to-pdf.html | 22 ++++++------ src/pages/json-to-pdf.html | 14 ++++---- src/pages/merge-pdf.html | 22 ++++++------ src/pages/pdf-multi-tool.html | 4 +-- src/pages/pdf-to-json.html | 14 ++++---- src/pages/repair-pdf.html | 22 ++++++------ src/pages/split-pdf.html | 22 ++++++------ src/pages/table-of-contents.html | 14 ++++---- vite.config.ts | 1 + 39 files changed, 206 insertions(+), 181 deletions(-) diff --git a/README.md b/README.md index d3c520e..77d8ba4 100644 --- a/README.md +++ b/README.md @@ -249,9 +249,33 @@ npm run package # Serve the dist folder npx serve dist + +The website can be accessible at: http://localhost:3000/ + ``` -The website can be accessible at: ```http://localhost:3000/``` +**Subdirectory Hosting:** + +BentoPDF can also be hosted from a subdirectory (e.g., `example.com/tools/bentopdf/`): + +```bash + +# Example: +# 1. Build the app with the specific BASE_URL. BASE_URL must have a trailing and leading slash. The BASE_URL can be any url of your choice. Here we are using /tools/bentopdf/ as an example. + +BASE_URL=/tools/bentopdf/ npm run build + +# 2. Create the nested directory structure inside serve-test (or any folder of your choice for local testing. In case of production, create the nested directory structure inside the root directory) +mkdir -p serve-test/tools/bentopdf + +# 3. Copy all files from the 'dist' folder into that nested directory +cp -r dist/* serve-test/tools/bentopdf/ + +# 4. Serve the 'serve-test' folder +npx serve serve-test +``` + +The website can be accessible at: ```http://localhost:3000/tools/bentopdf/``` The `npm run package` command creates a `dist-{version}.zip` file that you can use for self-hosting. diff --git a/index.html b/index.html index 3dcd988..1a3fa00 100644 --- a/index.html +++ b/index.html @@ -5,11 +5,11 @@ BentoPDF - The Privacy First PDF Toolkit - - - - - + + + + + @@ -502,7 +502,7 @@
    - GDPR compliance + GDPR compliance

    GDPR compliance @@ -515,7 +515,7 @@
    - CCPA compliance + CCPA compliance

    CCPA compliance @@ -528,7 +528,7 @@
    - HIPAA compliance + HIPAA compliance

    HIPAA compliance diff --git a/public/workers/add-attachments.worker.js b/public/workers/add-attachments.worker.js index b8cc8dc..9139468 100644 --- a/public/workers/add-attachments.worker.js +++ b/public/workers/add-attachments.worker.js @@ -1,4 +1,4 @@ -self.importScripts('/coherentpdf.browser.min.js'); +self.importScripts('../coherentpdf.browser.min.js'); function parsePageRange(rangeString, totalPages) { const pages = new Set(); diff --git a/public/workers/alternate-merge.worker.js b/public/workers/alternate-merge.worker.js index 7f66247..1a2cc2c 100644 --- a/public/workers/alternate-merge.worker.js +++ b/public/workers/alternate-merge.worker.js @@ -1,4 +1,4 @@ -self.importScripts('/coherentpdf.browser.min.js'); +self.importScripts('../coherentpdf.browser.min.js'); self.onmessage = function (e) { const { command, files } = e.data; diff --git a/public/workers/edit-attachments.worker.js b/public/workers/edit-attachments.worker.js index 4d20160..8b09478 100644 --- a/public/workers/edit-attachments.worker.js +++ b/public/workers/edit-attachments.worker.js @@ -1,9 +1,9 @@ -self.importScripts('/coherentpdf.browser.min.js'); +self.importScripts('../coherentpdf.browser.min.js'); function getAttachmentsFromPDFInWorker(fileBuffer, fileName) { try { const uint8Array = new Uint8Array(fileBuffer); - + let pdf; try { pdf = coherentpdf.fromMemory(uint8Array, ''); @@ -40,8 +40,8 @@ function getAttachmentsFromPDFInWorker(fileBuffer, fileName) { attachments.push({ index: i, - name: String(name), - page: Number(page), + name: String(name), + page: Number(page), data: buffer }); } catch (error) { @@ -73,7 +73,7 @@ function getAttachmentsFromPDFInWorker(fileBuffer, fileName) { function editAttachmentsInPDFInWorker(fileBuffer, fileName, attachmentsToRemove) { try { const uint8Array = new Uint8Array(fileBuffer); - + let pdf; try { pdf = coherentpdf.fromMemory(uint8Array, ''); @@ -89,28 +89,28 @@ function editAttachmentsInPDFInWorker(fileBuffer, fileName, attachmentsToRemove) coherentpdf.startGetAttachments(pdf); const attachmentCount = coherentpdf.numberGetAttachments(); const attachmentsToKeep = []; - + for (let i = 0; i < attachmentCount; i++) { if (!attachmentsToRemove.includes(i)) { const name = coherentpdf.getAttachmentName(i); const page = coherentpdf.getAttachmentPage(i); const data = coherentpdf.getAttachmentData(i); - + const dataCopy = new Uint8Array(data.length); dataCopy.set(new Uint8Array(data)); - - attachmentsToKeep.push({ - name: String(name), - page: Number(page), - data: dataCopy + + attachmentsToKeep.push({ + name: String(name), + page: Number(page), + data: dataCopy }); } } - + coherentpdf.endGetAttachments(); coherentpdf.removeAttachedFiles(pdf); - + for (const attachment of attachmentsToKeep) { if (attachment.page === 0) { coherentpdf.attachFileFromMemory(attachment.data, attachment.name, pdf); @@ -119,7 +119,7 @@ function editAttachmentsInPDFInWorker(fileBuffer, fileName, attachmentsToRemove) } } } - + const modifiedBytes = coherentpdf.toMemory(pdf, false, true); coherentpdf.deletePdf(pdf); diff --git a/public/workers/extract-attachments.worker.js b/public/workers/extract-attachments.worker.js index 0327c3a..789af57 100644 --- a/public/workers/extract-attachments.worker.js +++ b/public/workers/extract-attachments.worker.js @@ -1,15 +1,15 @@ -self.importScripts('/coherentpdf.browser.min.js'); +self.importScripts('../coherentpdf.browser.min.js'); function extractAttachmentsFromPDFsInWorker(fileBuffers, fileNames) { try { const allAttachments = []; const totalFiles = fileBuffers.length; - + for (let i = 0; i < totalFiles; i++) { const buffer = fileBuffers[i]; const fileName = fileNames[i]; const uint8Array = new Uint8Array(buffer); - + let pdf; try { pdf = coherentpdf.fromMemory(uint8Array, ''); @@ -73,7 +73,7 @@ function extractAttachmentsFromPDFsInWorker(fileBuffers, fileNames) { }); return; } - + const response = { status: 'success', attachments: [] diff --git a/public/workers/merge.worker.js b/public/workers/merge.worker.js index 425afc5..d37688f 100644 --- a/public/workers/merge.worker.js +++ b/public/workers/merge.worker.js @@ -1,4 +1,4 @@ -self.importScripts('/coherentpdf.browser.min.js'); +self.importScripts('../coherentpdf.browser.min.js'); self.onmessage = function (e) { const { command, files, jobs } = e.data; diff --git a/src/js/config/tools.ts b/src/js/config/tools.ts index 5c88043..ce2800f 100644 --- a/src/js/config/tools.ts +++ b/src/js/config/tools.ts @@ -4,38 +4,38 @@ export const categories = [ name: 'Popular Tools', tools: [ { - href: '/src/pages/pdf-multi-tool.html', + href: import.meta.env.BASE_URL + 'src/pages/pdf-multi-tool.html', name: 'PDF Multi Tool', icon: 'pencil-ruler', subtitle: 'Merge, Split, Organize, Delete, Rotate, Add Blank Pages, Extract and Duplicate in an unified interface.', }, { - href: '/src/pages/merge-pdf.html', + href: import.meta.env.BASE_URL + '/src/pages/merge-pdf.html', name: 'Merge PDF', icon: 'combine', subtitle: 'Combine multiple PDFs into one file. Preserves Bookmarks.', }, { - href: '/src/pages/split-pdf.html', + href: import.meta.env.BASE_URL + 'src/pages/split-pdf.html', name: 'Split PDF', icon: 'scissors', subtitle: 'Extract a range of pages into a new PDF.', }, { - href: '/src/pages/compress-pdf.html', + href: import.meta.env.BASE_URL + 'src/pages/compress-pdf.html', name: 'Compress PDF', icon: 'zap', subtitle: 'Reduce the file size of your PDF.', }, { - href: '/src/pages/edit-pdf.html', + href: import.meta.env.BASE_URL + 'src/pages/edit-pdf.html', name: 'PDF Editor', icon: 'pocket-knife', subtitle: 'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs', }, { - href: '/src/pages/jpg-to-pdf.html', + href: import.meta.env.BASE_URL + 'src/pages/jpg-to-pdf.html', name: 'JPG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more JPG images.', @@ -76,7 +76,7 @@ export const categories = [ name: 'Edit & Annotate', tools: [ { - href: '/src/pages/edit-pdf.html', + href: import.meta.env.BASE_URL + 'src/pages/edit-pdf.html', name: 'PDF Editor', icon: 'pocket-knife', subtitle: @@ -84,13 +84,13 @@ export const categories = [ }, { // id: 'bookmark-pdf', - href: '/src/pages/bookmark.html', + href: import.meta.env.BASE_URL + 'src/pages/bookmark.html', name: 'Edit Bookmarks', icon: 'bookmark', subtitle: 'Add, edit, import, delete and extract PDF bookmarks.', }, { - href: '/src/pages/table-of-contents.html', + href: import.meta.env.BASE_URL + 'src/pages/table-of-contents.html', name: 'Table of Contents', icon: 'list', subtitle: 'Generate a table of contents page from PDF bookmarks.', @@ -138,7 +138,7 @@ export const categories = [ subtitle: 'Draw, type, or upload your signature.', }, { - href: '/src/pages/add-stamps.html', + href: import.meta.env.BASE_URL + 'src/pages/add-stamps.html', name: 'Add Stamps', icon: 'stamp', subtitle: 'Add image stamps to your PDF using the annotation toolbar.', @@ -162,7 +162,7 @@ export const categories = [ subtitle: 'Fill in forms directly in the browser. Also supports XFA forms.', }, { - href: '/src/pages/form-creator.html', + href: import.meta.env.BASE_URL + 'src/pages/form-creator.html', name: 'Create PDF Form', icon: 'file-input', subtitle: 'Create fillable PDF forms with drag-and-drop text fields.', @@ -185,7 +185,7 @@ export const categories = [ subtitle: 'Convert JPG, PNG, WebP, BMP, TIFF, SVG, HEIC to PDF.', }, { - href: '/src/pages/jpg-to-pdf.html', + href: import.meta.env.BASE_URL + 'src/pages/jpg-to-pdf.html', name: 'JPG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more JPG images.', @@ -233,7 +233,7 @@ export const categories = [ subtitle: 'Convert a plain text file into a PDF.', }, { - href: '/src/pages/json-to-pdf.html', + href: import.meta.env.BASE_URL + 'src/pages/json-to-pdf.html', name: 'JSON to PDF', icon: 'file-code', subtitle: 'Convert JSON files to PDF format.', @@ -283,7 +283,7 @@ export const categories = [ subtitle: 'Convert all colors to black and white.', }, { - href: '/src/pages/pdf-to-json.html', + href: import.meta.env.BASE_URL + 'src/pages/pdf-to-json.html', name: 'PDF to JSON', icon: 'file-code', subtitle: 'Convert PDF files to JSON format.', @@ -301,7 +301,7 @@ export const categories = [ subtitle: 'Make a PDF searchable and copyable.', }, { - href: '/src/pages/merge-pdf.html', + href: import.meta.env.BASE_URL + 'src/pages/merge-pdf.html', name: 'Merge PDF', icon: 'combine', subtitle: 'Combine multiple PDFs into one file.', @@ -343,7 +343,7 @@ export const categories = [ subtitle: 'View or remove attachments in your PDF.', }, { - href: '/src/pages/pdf-multi-tool.html', + href: import.meta.env.BASE_URL + 'src/pages/pdf-multi-tool.html', name: 'PDF Multi Tool', icon: 'pencil-ruler', subtitle: 'Full-featured PDF editor with page management.', @@ -438,7 +438,7 @@ export const categories = [ name: 'Optimize & Repair', tools: [ { - href: '/src/pages/compress-pdf.html', + href: import.meta.env.BASE_URL + 'src/pages/compress-pdf.html', name: 'Compress PDF', icon: 'zap', subtitle: 'Reduce the file size of your PDF.', @@ -469,7 +469,7 @@ export const categories = [ 'Remove password protection and security restrictions associated with digitally signed PDF files.', }, { - href: '/src/pages/repair-pdf.html', + href: import.meta.env.BASE_URL + 'src/pages/repair-pdf.html', name: 'Repair PDF', icon: 'wrench', subtitle: 'Recover data from corrupted or damaged PDF files.', diff --git a/src/js/logic/add-attachments.ts b/src/js/logic/add-attachments.ts index 46a7a52..b8adf99 100644 --- a/src/js/logic/add-attachments.ts +++ b/src/js/logic/add-attachments.ts @@ -2,7 +2,7 @@ import { showLoader, hideLoader, showAlert } from '../ui'; import { readFileAsArrayBuffer, downloadFile } from '../utils/helpers'; import { state } from '../state'; -const worker = new Worker('/workers/add-attachments.worker.js'); +const worker = new Worker(import.meta.env.BASE_URL + 'workers/add-attachments.worker.js'); let attachments: File[] = []; diff --git a/src/js/logic/add-stamps.ts b/src/js/logic/add-stamps.ts index ab369ab..408ff34 100644 --- a/src/js/logic/add-stamps.ts +++ b/src/js/logic/add-stamps.ts @@ -172,7 +172,7 @@ if (saveStampedBtn) { if (backToToolsBtn) { backToToolsBtn.addEventListener('click', () => { - window.location.href = '/' + window.location.href = import.meta.env.BASE_URL }) } diff --git a/src/js/logic/alternate-merge.ts b/src/js/logic/alternate-merge.ts index be25ca3..db7a961 100644 --- a/src/js/logic/alternate-merge.ts +++ b/src/js/logic/alternate-merge.ts @@ -13,7 +13,7 @@ const alternateMergeState: AlternateMergeState = { pdfBytes: {}, }; -const alternateMergeWorker = new Worker('/workers/alternate-merge.worker.js'); +const alternateMergeWorker = new Worker(import.meta.env.BASE_URL + 'workers/alternate-merge.worker.js'); export async function setupAlternateMergeTool() { const optionsDiv = document.getElementById('alternate-merge-options'); diff --git a/src/js/logic/bookmark-pdf.ts b/src/js/logic/bookmark-pdf.ts index 2347f46..d2ca899 100644 --- a/src/js/logic/bookmark-pdf.ts +++ b/src/js/logic/bookmark-pdf.ts @@ -1957,13 +1957,13 @@ async function extractExistingBookmarks(doc) { // Back to tools button if (backToToolsBtn) { backToToolsBtn.addEventListener('click', () => { - window.location.href = '/'; + window.location.href = import.meta.env.BASE_URL; }); } if (closeBtn) { closeBtn.addEventListener('click', () => { - window.location.href = '/'; + window.location.href = import.meta.env.BASE_URL; }); } diff --git a/src/js/logic/compress-pdf-page.ts b/src/js/logic/compress-pdf-page.ts index 4eac562..097b719 100644 --- a/src/js/logic/compress-pdf-page.ts +++ b/src/js/logic/compress-pdf-page.ts @@ -229,7 +229,7 @@ document.addEventListener('DOMContentLoaded', () => { if (backBtn) { backBtn.addEventListener('click', () => { - window.location.href = '/'; + window.location.href = import.meta.env.BASE_URL; }); } diff --git a/src/js/logic/edit-attachments.ts b/src/js/logic/edit-attachments.ts index 098ccd1..fd9de00 100644 --- a/src/js/logic/edit-attachments.ts +++ b/src/js/logic/edit-attachments.ts @@ -2,7 +2,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js'; import { state } from '../state.js'; -const worker = new Worker('/workers/edit-attachments.worker.js'); +const worker = new Worker(import.meta.env.BASE_URL + 'workers/edit-attachments.worker.js'); let allAttachments: Array<{ index: number; name: string; page: number; data: Uint8Array }> = []; let attachmentsToRemove: Set = new Set(); diff --git a/src/js/logic/edit-pdf-page.ts b/src/js/logic/edit-pdf-page.ts index 788a53e..c064427 100644 --- a/src/js/logic/edit-pdf-page.ts +++ b/src/js/logic/edit-pdf-page.ts @@ -45,7 +45,7 @@ function initializePage() { } document.getElementById('back-to-tools')?.addEventListener('click', () => { - window.location.href = '/'; + window.location.href = import.meta.env.BASE_URL; }); } @@ -114,7 +114,7 @@ async function handleFiles(files: FileList) { URL.revokeObjectURL(currentPdfUrl); currentPdfUrl = null; } - window.location.href = '/'; + window.location.href = import.meta.env.BASE_URL; }); } diff --git a/src/js/logic/extract-attachments.ts b/src/js/logic/extract-attachments.ts index 8183652..331d1fe 100644 --- a/src/js/logic/extract-attachments.ts +++ b/src/js/logic/extract-attachments.ts @@ -3,7 +3,7 @@ import { state } from '../state.js'; import { showAlert } from '../ui.js'; import JSZip from 'jszip'; -const worker = new Worker('/workers/extract-attachments.worker.js'); +const worker = new Worker(import.meta.env.BASE_URL + 'workers/extract-attachments.worker.js'); interface ExtractAttachmentSuccessResponse { status: 'success'; diff --git a/src/js/logic/form-creator.ts b/src/js/logic/form-creator.ts index db5808c..efcf3f4 100644 --- a/src/js/logic/form-creator.ts +++ b/src/js/logic/form-creator.ts @@ -1962,7 +1962,7 @@ downloadBtn.addEventListener('click', async () => { const backToToolsBtns = document.querySelectorAll('[id^="back-to-tools"]') as NodeListOf backToToolsBtns.forEach(btn => { btn.addEventListener('click', () => { - window.location.href = '/' + window.location.href = import.meta.env.BASE_URL }) }) diff --git a/src/js/logic/jpg-to-pdf-page.ts b/src/js/logic/jpg-to-pdf-page.ts index 523d310..1d23fb4 100644 --- a/src/js/logic/jpg-to-pdf-page.ts +++ b/src/js/logic/jpg-to-pdf-page.ts @@ -66,7 +66,7 @@ function initializePage() { } document.getElementById('back-to-tools')?.addEventListener('click', () => { - window.location.href = '/'; + window.location.href = import.meta.env.BASE_URL; }); } diff --git a/src/js/logic/json-to-pdf.ts b/src/js/logic/json-to-pdf.ts index 8570086..e483781 100644 --- a/src/js/logic/json-to-pdf.ts +++ b/src/js/logic/json-to-pdf.ts @@ -2,7 +2,7 @@ import JSZip from 'jszip' import { downloadFile, formatBytes, readFileAsArrayBuffer } from '../utils/helpers'; import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'; -const worker = new Worker(new URL('/workers/json-to-pdf.worker.js', import.meta.url)); +const worker = new Worker(import.meta.env.BASE_URL + 'workers/json-to-pdf.worker.js'); let selectedFiles: File[] = [] @@ -148,7 +148,7 @@ worker.onmessage = async (e: MessageEvent) => { if (backToToolsBtn) { backToToolsBtn.addEventListener('click', () => { - window.location.href = '/' + window.location.href = import.meta.env.BASE_URL }) } diff --git a/src/js/logic/merge-pdf-page.ts b/src/js/logic/merge-pdf-page.ts index 25712f2..8b837af 100644 --- a/src/js/logic/merge-pdf-page.ts +++ b/src/js/logic/merge-pdf-page.ts @@ -35,7 +35,7 @@ const mergeState: MergeState = { mergeSuccess: false, }; -const mergeWorker = new Worker('/workers/merge.worker.js'); +const mergeWorker = new Worker(import.meta.env.BASE_URL + 'workers/merge.worker.js'); function initializeFileListSortable() { const fileList = document.getElementById('file-list'); @@ -545,7 +545,7 @@ document.addEventListener('DOMContentLoaded', () => { if (backBtn) { backBtn.addEventListener('click', () => { - window.location.href = '/'; + window.location.href = import.meta.env.BASE_URL; }); } diff --git a/src/js/logic/pdf-multi-tool.ts b/src/js/logic/pdf-multi-tool.ts index 5576eef..99d6711 100644 --- a/src/js/logic/pdf-multi-tool.ts +++ b/src/js/logic/pdf-multi-tool.ts @@ -152,7 +152,7 @@ function initializeTool() { initializeGlobalShortcuts(); document.getElementById('close-tool-btn')?.addEventListener('click', () => { - window.location.href = '/'; + window.location.href = import.meta.env.BASE_URL; }); document.getElementById('upload-pdfs-btn')?.addEventListener('click', () => { diff --git a/src/js/logic/pdf-to-json.ts b/src/js/logic/pdf-to-json.ts index e132c16..babeaeb 100644 --- a/src/js/logic/pdf-to-json.ts +++ b/src/js/logic/pdf-to-json.ts @@ -2,7 +2,7 @@ import JSZip from 'jszip' import { downloadFile, formatBytes, readFileAsArrayBuffer } from '../utils/helpers'; import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'; -const worker = new Worker(new URL('/workers/pdf-to-json.worker.js', import.meta.url)); +const worker = new Worker(import.meta.env.BASE_URL + 'workers/pdf-to-json.worker.js'); let selectedFiles: File[] = [] @@ -144,7 +144,7 @@ worker.onmessage = async (e: MessageEvent) => { if (backToToolsBtn) { backToToolsBtn.addEventListener('click', () => { - window.location.href = '/' + window.location.href = import.meta.env.BASE_URL }) } diff --git a/src/js/logic/repair-pdf-page.ts b/src/js/logic/repair-pdf-page.ts index 5f9ed62..e135189 100644 --- a/src/js/logic/repair-pdf-page.ts +++ b/src/js/logic/repair-pdf-page.ts @@ -15,7 +15,7 @@ document.addEventListener('DOMContentLoaded', () => { if (backBtn) { backBtn.addEventListener('click', () => { - window.location.href = '/'; + window.location.href = import.meta.env.BASE_URL; }); } diff --git a/src/js/logic/split-pdf-page.ts b/src/js/logic/split-pdf-page.ts index 5cf7f4e..11ab9dd 100644 --- a/src/js/logic/split-pdf-page.ts +++ b/src/js/logic/split-pdf-page.ts @@ -33,7 +33,7 @@ document.addEventListener('DOMContentLoaded', () => { if (backBtn) { backBtn.addEventListener('click', () => { - window.location.href = '/'; + window.location.href = import.meta.env.BASE_URL; }); } diff --git a/src/js/logic/table-of-contents.ts b/src/js/logic/table-of-contents.ts index 5b9076e..1850d74 100644 --- a/src/js/logic/table-of-contents.ts +++ b/src/js/logic/table-of-contents.ts @@ -2,7 +2,7 @@ import { downloadFile, formatBytes } from "../utils/helpers"; import { initializeGlobalShortcuts } from "../utils/shortcuts-init.js"; -const worker = new Worker('/workers/table-of-contents.worker.js'); +const worker = new Worker(import.meta.env.BASE_URL + 'workers/table-of-contents.worker.js'); let pdfFile: File | null = null; @@ -199,7 +199,7 @@ worker.onerror = (error) => { if (backToToolsBtn) { backToToolsBtn.addEventListener('click', () => { - window.location.href = '/'; + window.location.href = import.meta.env.BASE_URL; }); } diff --git a/src/pages/add-stamps.html b/src/pages/add-stamps.html index ab1b0c1..f9a4bec 100644 --- a/src/pages/add-stamps.html +++ b/src/pages/add-stamps.html @@ -17,18 +17,18 @@
    @@ -53,10 +53,10 @@ @@ -113,7 +113,7 @@
    - Bento PDF Logo + Bento PDF Logo BentoPDF

    diff --git a/src/pages/bookmark.html b/src/pages/bookmark.html index e4d1112..5004e59 100644 --- a/src/pages/bookmark.html +++ b/src/pages/bookmark.html @@ -18,18 +18,18 @@

    @@ -56,10 +56,10 @@ @@ -421,7 +421,7 @@
    - Bento PDF Logo + Bento PDF Logo BentoPDF

    diff --git a/src/pages/compress-pdf.html b/src/pages/compress-pdf.html index 0b7ea49..830e98c 100644 --- a/src/pages/compress-pdf.html +++ b/src/pages/compress-pdf.html @@ -16,17 +16,17 @@

    @@ -51,10 +51,10 @@ diff --git a/src/pages/edit-pdf.html b/src/pages/edit-pdf.html index 7ce1971..62208ae 100644 --- a/src/pages/edit-pdf.html +++ b/src/pages/edit-pdf.html @@ -16,17 +16,17 @@
    @@ -51,10 +51,10 @@ @@ -121,7 +121,7 @@
    - Bento PDF Logo + Bento PDF Logo BentoPDF

    diff --git a/src/pages/form-creator.html b/src/pages/form-creator.html index e0deb9b..6f4aa24 100644 --- a/src/pages/form-creator.html +++ b/src/pages/form-creator.html @@ -17,18 +17,18 @@

    @@ -55,10 +55,10 @@ @@ -327,7 +327,7 @@
    - Bento PDF Logo + Bento PDF Logo BentoPDF

    diff --git a/src/pages/jpg-to-pdf.html b/src/pages/jpg-to-pdf.html index fae13a1..2baf501 100644 --- a/src/pages/jpg-to-pdf.html +++ b/src/pages/jpg-to-pdf.html @@ -16,17 +16,17 @@

    @@ -51,10 +51,10 @@ @@ -150,7 +150,7 @@
    - Bento PDF Logo + Bento PDF Logo BentoPDF

    diff --git a/src/pages/json-to-pdf.html b/src/pages/json-to-pdf.html index a7df403..e0d359c 100644 --- a/src/pages/json-to-pdf.html +++ b/src/pages/json-to-pdf.html @@ -17,18 +17,18 @@

    @@ -53,10 +53,10 @@ @@ -113,7 +113,7 @@
    - Bento PDF Logo + Bento PDF Logo BentoPDF

    diff --git a/src/pages/merge-pdf.html b/src/pages/merge-pdf.html index 278c1eb..36f1d45 100644 --- a/src/pages/merge-pdf.html +++ b/src/pages/merge-pdf.html @@ -16,18 +16,18 @@

    @@ -54,10 +54,10 @@ @@ -170,7 +170,7 @@
    - Bento PDF Logo + Bento PDF Logo BentoPDF

    diff --git a/src/pages/pdf-multi-tool.html b/src/pages/pdf-multi-tool.html index 546ab11..f5fb555 100644 --- a/src/pages/pdf-multi-tool.html +++ b/src/pages/pdf-multi-tool.html @@ -46,9 +46,9 @@

    - Bento PDF Logo + Bento PDF Logo - BentoPDF + BentoPDF PDF Multi Tool
    diff --git a/src/pages/pdf-to-json.html b/src/pages/pdf-to-json.html index 2b3657c..4c1c9b2 100644 --- a/src/pages/pdf-to-json.html +++ b/src/pages/pdf-to-json.html @@ -17,18 +17,18 @@
    @@ -53,10 +53,10 @@ @@ -107,7 +107,7 @@
    - Bento PDF Logo + Bento PDF Logo BentoPDF

    diff --git a/src/pages/repair-pdf.html b/src/pages/repair-pdf.html index 43521da..407e21e 100644 --- a/src/pages/repair-pdf.html +++ b/src/pages/repair-pdf.html @@ -14,18 +14,18 @@

    @@ -52,10 +52,10 @@ @@ -132,7 +132,7 @@
    - Bento PDF Logo + Bento PDF Logo BentoPDF

    diff --git a/src/pages/split-pdf.html b/src/pages/split-pdf.html index ec664b3..ca3d264 100644 --- a/src/pages/split-pdf.html +++ b/src/pages/split-pdf.html @@ -16,18 +16,18 @@

    @@ -56,10 +56,10 @@ @@ -243,7 +243,7 @@
    - Bento PDF Logo + Bento PDF Logo BentoPDF

    diff --git a/src/pages/table-of-contents.html b/src/pages/table-of-contents.html index 71de3cc..f1073a5 100644 --- a/src/pages/table-of-contents.html +++ b/src/pages/table-of-contents.html @@ -17,18 +17,18 @@

    @@ -55,10 +55,10 @@ @@ -179,7 +179,7 @@
    - Bento PDF Logo + Bento PDF Logo BentoPDF

    diff --git a/vite.config.ts b/vite.config.ts index d91bcdd..49580bc 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,6 +4,7 @@ import { nodePolyfills } from 'vite-plugin-node-polyfills'; import { resolve } from 'path'; export default defineConfig(({ mode }) => ({ + base: process.env.BASE_URL || '/', plugins: [ tailwindcss(), nodePolyfills({ From af01a041bf81287639e023cb3dec60a58be7e8ed Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Thu, 4 Dec 2025 23:53:31 +0530 Subject: [PATCH 09/21] Release v1.10.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b437c5..7514233 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bento-pdf", "private": true, - "version": "1.10.1", + "version": "1.10.2", "license": "Apache-2.0", "type": "module", "scripts": { From a976295b4cc10c7a6fe55d0bf646490fc7602339 Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Thu, 4 Dec 2025 23:57:42 +0530 Subject: [PATCH 10/21] Release v1.10.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7514233..302e20a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bento-pdf", "private": true, - "version": "1.10.2", + "version": "1.10.3", "license": "Apache-2.0", "type": "module", "scripts": { From c682f9d9b3028bcb0dea57d878347a1af261e4df Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Fri, 5 Dec 2025 00:38:08 +0530 Subject: [PATCH 11/21] fix(config): Remove leading slash from merge PDF tool path - Remove leading slash from merge-pdf.html path in tools configuration - Ensure consistent path resolution with BASE_URL environment variable - Fix asset path handling for subdirectory hosting compatibility --- src/js/config/tools.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/config/tools.ts b/src/js/config/tools.ts index ce2800f..2f664c0 100644 --- a/src/js/config/tools.ts +++ b/src/js/config/tools.ts @@ -10,7 +10,7 @@ export const categories = [ subtitle: 'Merge, Split, Organize, Delete, Rotate, Add Blank Pages, Extract and Duplicate in an unified interface.', }, { - href: import.meta.env.BASE_URL + '/src/pages/merge-pdf.html', + href: import.meta.env.BASE_URL + 'src/pages/merge-pdf.html', name: 'Merge PDF', icon: 'combine', subtitle: 'Combine multiple PDFs into one file. Preserves Bookmarks.', From e0381e6aa06564d8d93d4467c9274c76ee140b15 Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Fri, 5 Dec 2025 00:38:15 +0530 Subject: [PATCH 12/21] Release v1.10.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 302e20a..445190d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bento-pdf", "private": true, - "version": "1.10.3", + "version": "1.10.4", "license": "Apache-2.0", "type": "module", "scripts": { From 5e23a7e77ebdcaa5ae9ecb8ae00042796d16e85e Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Fri, 5 Dec 2025 14:57:35 +0530 Subject: [PATCH 13/21] feat: Add Contributor License Agreements (CLA) for individual and corporate contributors with automated enforcement. --- .github/cla.json | 18 ++++ .github/pull_request_template.md | 1 + .github/workflows/cla.yml | 70 ++++++++++++++++ CCLA.md | 140 +++++++++++++++++++++++++++++++ CONTRIBUTING.md | 28 +++++++ ICLA.md | 100 ++++++++++++++++++++++ 6 files changed, 357 insertions(+) create mode 100644 .github/cla.json create mode 100644 .github/workflows/cla.yml create mode 100644 CCLA.md create mode 100644 ICLA.md diff --git a/.github/cla.json b/.github/cla.json new file mode 100644 index 0000000..20fb311 --- /dev/null +++ b/.github/cla.json @@ -0,0 +1,18 @@ +{ + "name": "BentoPDF CLA", + "text": [ + "Thank you for your contribution to BentoPDF!", + "", + "By signing this CLA, you agree to the terms of our Contributor License Agreement.", + "", + "**Individual Contributors**: Please review and sign our [Individual CLA (ICLA)](ICLA.md)", + "", + "**Corporate Contributors**: If you are contributing on behalf of your employer, please have your organization sign our [Corporate CLA (CCLA)](CCLA.md) by contacting contact@bentopdf.com" + ], + "notes": { + "purpose": "This CLA enables BentoPDF's dual licensing model (AGPL-3.0 + commercial license)", + "icla_url": "https://github.com/alam00000/bentopdf/blob/main/ICLA.md", + "ccla_url": "https://github.com/alam00000/bentopdf/blob/main/CCLA.md", + "contact": "contact@bentopdf.com" + } +} \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 25cc0a2..905564f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -33,6 +33,7 @@ Please describe **how you tested your changes** so that maintainers can verify t ### Checklist: +- [ ] I have signed the [Contributor License Agreement (CLA)](ICLA.md) or my organization has signed the [Corporate CLA](CCLA.md) - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 0000000..3cf3384 --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,70 @@ +name: "CLA Assistant" + +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened, closed, synchronize] + +# Permissions needed for the workflow +permissions: + actions: write + contents: read + pull-requests: write + statuses: write + +jobs: + cla-check: + runs-on: ubuntu-latest + steps: + - name: "CLA Assistant" + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' + uses: cla-assistant/github-action@v2.5.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_ASSISTANT_TOKEN }} + with: + # Path to the CLA document(s) + path-to-signatures: 'signatures/cla.json' + path-to-document: 'https://github.com/${{ github.repository }}/blob/main/ICLA.md' + + # Branch to store signatures + branch: 'main' + + # Allowlist for bot accounts (won't need to sign CLA) + allowlist: 'bot*,*[bot],dependabot*,renovate*,github-actions*' + + # Custom messages + custom-notsigned-prcomment: | + ## CLA Signature Required + + Thank you for your contribution! Before we can accept your pull request, you need to sign our [Contributor License Agreement (CLA)](https://github.com/${{ github.repository }}/blob/main/ICLA.md). + + ### Why do we need a CLA? + + BentoPDF uses a dual licensing model (AGPL-3.0 for open source + commercial license). The CLA allows us to: + - Include your contributions in both open source and commercial versions + - Protect you and us legally + - Keep the project sustainable + + ### How to sign + + To sign the CLA, please comment on this PR with: + + ``` + I have read the CLA Document and I hereby sign the CLA + ``` + + **For corporate contributions**, please have your organization sign our [Corporate CLA (CCLA)](https://github.com/${{ github.repository }}/blob/main/CCLA.md) by contacting us at contact@bentopdf.com. + + --- + + ⚠️ **Note**: You only need to sign the CLA once. Future contributions to this repository will not require re-signing. + + custom-pr-sign-comment: 'I have read the CLA Document and I hereby sign the CLA' + + # Lock PR while waiting for CLA signature + lock-pullrequest-aftermerge: false + + # Use issues to store signatures (alternative to branch storage) + use-dco-flag: false diff --git a/CCLA.md b/CCLA.md new file mode 100644 index 0000000..8a87b24 --- /dev/null +++ b/CCLA.md @@ -0,0 +1,140 @@ +# BentoPDF Corporate Contributor License Agreement (CCLA) + +Thank you for your organization's interest in contributing to BentoPDF. This Corporate Contributor License Agreement ("Agreement") documents the rights granted by corporate contributors to the Project. + +By signing this Agreement, you accept and agree to the following terms and conditions for your organization's present and future Contributions submitted to the Project. + +## 1. Definitions + +**"You" (or "Your")** means the legal entity on behalf of which this Agreement is being entered into. + +**"Contributor"** means any employee, contractor, or authorized agent of You who submits Contributions on Your behalf. + +**"Contribution"** means any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You or any of Your Contributors to the Project for inclusion in, or documentation of, the Project. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Project or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Project for the purpose of discussing and improving the Project. + +**"Project"** means the BentoPDF software project and all associated repositories, documentation, and related materials maintained at https://github.com/alam00000/bentopdf. + +**"Project Owner"** means the owners and maintainers of the Project. + +## 2. Grant of Copyright License + +Subject to the terms and conditions of this Agreement, You hereby grant to the Project Owner and to recipients of software distributed by the Project Owner a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to: + +- Reproduce, prepare derivative works of, publicly display, publicly perform, and distribute Contributions made by You and Your Contributors, and such derivative works +- Sublicense any or all of the foregoing rights to third parties +- **Relicense Contributions under any license**, including but not limited to proprietary licenses, commercial licenses, or any other open source license + +This grant specifically enables the Project Owner to offer commercial licenses of the Project incorporating Your Contributions, consistent with the Project's dual licensing model (AGPL-3.0 for open source use, and a separate commercial license for proprietary use). + +## 3. Grant of Patent License + +Subject to the terms and conditions of this Agreement, You hereby grant to the Project Owner and to recipients of software distributed by the Project Owner a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Contribution, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Project to which such Contribution(s) was submitted. + +If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that Your Contribution, or the Project to which You have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Project shall terminate as of the date such litigation is filed. + +## 4. Authorized Contributors + +You are responsible for: + +a) Identifying the initial list of Contributors authorized to submit Contributions on Your behalf by listing them in Schedule A below + +b) Keeping the list of authorized Contributors current by notifying the Project Owner when Contributors should be added or removed + +c) Ensuring that all authorized Contributors are aware of and comply with the terms of this Agreement + +All Contributions made by Your authorized Contributors shall be deemed to be made on Your behalf and subject to this Agreement. + +## 5. Representations and Warranties + +You represent and warrant that: + +a) **Authority**: You are legally entitled to grant the above licenses and have the authority to bind the legal entity You represent. You have taken all necessary corporate action to authorize the execution of this Agreement. + +b) **Contributor Authorization**: Each Contributor identified by You is authorized to submit Contributions on Your behalf. + +c) **Originality**: Each Contribution is Your organization's original creation, or You have sufficient rights to submit the Contribution on behalf of the original authors. + +d) **Third-Party Rights**: Your Contributions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which You are aware and which are associated with any part of Your Contributions. + +e) **No Conflicting Obligations**: Your Contributions do not violate any agreement or obligation You have with any third party. + +f) **Accuracy**: All information You provide in connection with this Agreement and Your Contributions is accurate and complete. + +## 6. Moral Rights Waiver + +To the fullest extent permitted under applicable law, You hereby waive, and agree to cause Your Contributors to waive, any and all moral rights in or relating to Contributions, including without limitation: + +- The right of attribution +- The right of integrity +- The right to object to derogatory treatment +- Any similar rights existing under the laws of any jurisdiction + +You acknowledge that the Project Owner may modify, adapt, translate, or otherwise change Contributions without consent and without attribution. + +## 7. No Revocation + +**This license grant is irrevocable.** Once a Contribution has been submitted under this Agreement, You may not revoke or withdraw the licenses granted herein. The Project Owner and all downstream recipients may continue to use, distribute, modify, and sublicense Contributions indefinitely. + +## 8. Retention of Copyright + +You retain all right, title, and interest in and to the copyright of Your Contributions. Nothing in this Agreement is intended to transfer ownership of Your Contributions to the Project Owner. You are free to use Your Contributions for any other purpose. + +## 9. No Obligation + +You understand that the decision to include any Contribution in any project or source repository is entirely at the discretion of the Project Owner. The Project Owner is under no obligation to accept, use, or include any Contribution. + +## 10. Support and Warranty Disclaimer + +Unless required by applicable law or agreed to in writing, You provide Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + +You are not expected to provide support for Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. + +## 11. Notification + +You agree to notify the Project Owner of any facts or circumstances of which You become aware that would make any of Your representations in this Agreement inaccurate in any respect. + +## 12. Governing Law + +This Agreement shall be governed by and construed in accordance with the laws of India, without regard to its conflict of law provisions. + +--- + +## Signature + +By signing this Agreement, You confirm that You have the authority to bind the legal entity named below, and that entity agrees to the terms of this Corporate Contributor License Agreement. + +**Legal Entity Name:** ___________________________ + +**Address:** ___________________________ + +**Point of Contact Name:** ___________________________ + +**Point of Contact Email:** ___________________________ + +**Point of Contact Phone:** ___________________________ + +**Signature:** ___________________________ + +**Title:** ___________________________ + +**Date:** ___________________________ + +--- + +## Schedule A: Authorized Contributors + +The following individuals are authorized to submit Contributions to the Project on behalf of the above-named legal entity: + +| Full Name | GitHub Username | Email Address | Date Added | +|-----------|-----------------|---------------|------------| +| | | | | +| | | | | +| | | | | +| | | | | +| | | | | + +*Add additional rows as needed. To update this list, contact the Project Owner at contact@bentopdf.com* + +--- + +*This CLA is based on the Apache Corporate Contributor License Agreement and has been adapted for BentoPDF's dual licensing model.* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e106dec..c5ffb7b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,6 +6,34 @@ This document outlines how to contribute, report issues, and get involved in the --- +## Contributor License Agreement (CLA) + +Before we can accept your contributions, you must sign our Contributor License Agreement (CLA). This is required because BentoPDF uses a dual licensing model: + +- **AGPL-3.0** for open source use +- **Commercial license** for proprietary use + +The CLA ensures we can include your contributions in both versions of the project. + +### For Individual Contributors + +Sign our [Individual Contributor License Agreement (ICLA)](ICLA.md). When you submit your first pull request, the CLA Assistant bot will automatically ask you to sign by commenting on the PR. + +### For Corporate Contributors + +If you are contributing on behalf of your employer, your organization needs to sign our [Corporate Contributor License Agreement (CCLA)](CCLA.md). Please contact us at [contact@bentopdf.com](mailto:contact@bentopdf.com) to arrange corporate CLA signing. + +### What the CLA Grants + +By signing the CLA, you: + +- Grant us a broad copyright license to use, modify, and relicense your contributions (including for commercial use) +- Grant a patent license for any patents covering your contribution +- Represent that you have the authority to make the contribution +- Retain full copyright ownership of your contributions + +--- + ## 1. How to Contribute You can contribute in several ways: diff --git a/ICLA.md b/ICLA.md new file mode 100644 index 0000000..48086fb --- /dev/null +++ b/ICLA.md @@ -0,0 +1,100 @@ +# BentoPDF Individual Contributor License Agreement (ICLA) + +Thank you for your interest in contributing to BentoPDF. This Individual Contributor License Agreement ("Agreement") documents the rights granted by contributors to the Project. + +By signing this Agreement, you accept and agree to the following terms and conditions for your present and future Contributions submitted to the Project. + +## 1. Definitions + +**"You" (or "Your")** means the individual who is signing this Agreement and submitting Contributions to the Project. + +**"Contribution"** means any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to the Project for inclusion in, or documentation of, the Project. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Project or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Project for the purpose of discussing and improving the Project. + +**"Project"** means the BentoPDF software project and all associated repositories, documentation, and related materials maintained at https://github.com/alam00000/bentopdf. + +**"Project Owner"** means the owners and maintainers of the Project. + +## 2. Grant of Copyright License + +Subject to the terms and conditions of this Agreement, You hereby grant to the Project Owner and to recipients of software distributed by the Project Owner a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to: + +- Reproduce, prepare derivative works of, publicly display, publicly perform, and distribute Your Contributions and such derivative works +- Sublicense any or all of the foregoing rights to third parties +- **Relicense Your Contributions under any license**, including but not limited to proprietary licenses, commercial licenses, or any other open source license + +This grant specifically enables the Project Owner to offer commercial licenses of the Project incorporating Your Contributions, consistent with the Project's dual licensing model (AGPL-3.0 for open source use, and a separate commercial license for proprietary use). + +## 3. Grant of Patent License + +Subject to the terms and conditions of this Agreement, You hereby grant to the Project Owner and to recipients of software distributed by the Project Owner a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Contribution, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Project to which such Contribution(s) was submitted. + +If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that Your Contribution, or the Project to which You have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Project shall terminate as of the date such litigation is filed. + +## 4. Representations and Warranties + +You represent and warrant that: + +a) **Authority**: You are legally entitled to grant the above licenses. If Your employer(s) has rights to intellectual property that You create, You represent that You have received permission to make Contributions on behalf of that employer, that Your employer has waived such rights for Your Contributions, or that Your employer has executed a separate Corporate CLA with the Project. + +b) **Originality**: Each of Your Contributions is Your original creation. You represent that Your Contributions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which You are aware and which are associated with any part of Your Contributions. + +c) **No Conflicting Obligations**: Your Contribution does not violate any agreement or obligation You have with any third party. + +d) **Accuracy**: All information You provide in connection with this Agreement and Your Contributions is accurate and complete. + +## 5. Moral Rights Waiver + +To the fullest extent permitted under applicable law, You hereby waive, and agree not to assert, any and all moral rights You may have in or relating to Your Contributions, including without limitation: + +- The right of attribution +- The right of integrity +- The right to object to derogatory treatment +- Any similar rights existing under the laws of any jurisdiction + +You acknowledge that the Project Owner may modify, adapt, translate, or otherwise change Your Contributions without Your consent and without attribution. + +## 6. No Revocation + +**This license grant is irrevocable.** Once You have submitted a Contribution under this Agreement, You may not revoke or withdraw the licenses granted herein. The Project Owner and all downstream recipients may continue to use, distribute, modify, and sublicense Your Contribution indefinitely. + +## 7. Retention of Copyright + +You retain all right, title, and interest in and to the copyright of Your Contributions. Nothing in this Agreement is intended to transfer ownership of Your Contributions to the Project Owner. You are free to use Your Contributions for any other purpose. + +## 8. No Obligation + +You understand that the decision to include Your Contribution in any project or source repository is entirely at the discretion of the Project Owner. The Project Owner is under no obligation to accept, use, or include any Contribution. + +## 9. Support and Warranty Disclaimer + +Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + +You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. + +## 10. Notification + +You agree to notify the Project Owner of any facts or circumstances of which You become aware that would make any of Your representations in this Agreement inaccurate in any respect. + +## 11. Governing Law + +This Agreement shall be governed by and construed in accordance with the laws of India, without regard to its conflict of law provisions. + +--- + +## Signature + +By submitting a pull request or other Contribution to the Project, and by typing your name and date below (or by signing electronically via CLA Assistant), you agree to the terms of this Individual Contributor License Agreement. + +**Full Legal Name:** ___________________________ + +**GitHub Username:** ___________________________ + +**Email Address:** ___________________________ + +**Date:** ___________________________ + +**Signature:** ___________________________ + +--- + +*This CLA is based on the Apache Individual Contributor License Agreement and has been adapted for BentoPDF's dual licensing model.* From fe3e54f9794b54132eb556cd9c3b8cc385bb4899 Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Fri, 5 Dec 2025 14:57:46 +0530 Subject: [PATCH 14/21] Release v1.10.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 445190d..00233c5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bento-pdf", "private": true, - "version": "1.10.4", + "version": "1.10.5", "license": "Apache-2.0", "type": "module", "scripts": { From 529958e3253e3c329ffa700a5c7c43a6f8103983 Mon Sep 17 00:00:00 2001 From: Alam <50314772+alam00000@users.noreply.github.com> Date: Sat, 6 Dec 2025 02:04:01 +0530 Subject: [PATCH 15/21] Update cla.yml --- .github/workflows/cla.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 3cf3384..ff76093 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -16,12 +16,24 @@ permissions: jobs: cla-check: runs-on: ubuntu-latest + # OPTIMIZATION: This logic is moved here. + # The job will only start if it's a PR update, OR a comment on a PR containing the magic words. + if: > + github.event_name == 'pull_request_target' || + ( + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + ( + github.event.comment.body == 'recheck' || + github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA' + ) + ) steps: - name: "CLA Assistant" - if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' uses: cla-assistant/github-action@v2.5.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Ensure this secret is set in your repo settings! PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_ASSISTANT_TOKEN }} with: # Path to the CLA document(s) From f35ed5f464b0eb7fc805e3d87fd573836108b445 Mon Sep 17 00:00:00 2001 From: Alam <50314772+alam00000@users.noreply.github.com> Date: Sat, 6 Dec 2025 02:24:40 +0530 Subject: [PATCH 16/21] Add initial CLA JSON structure --- signatures/cla.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 signatures/cla.json diff --git a/signatures/cla.json b/signatures/cla.json new file mode 100644 index 0000000..eae73d4 --- /dev/null +++ b/signatures/cla.json @@ -0,0 +1,3 @@ +{ + "signed": [] +} From 9c46b4fb1a9498da0ce9cce089f2d59f2633b3d4 Mon Sep 17 00:00:00 2001 From: Alam <50314772+alam00000@users.noreply.github.com> Date: Sat, 6 Dec 2025 02:34:57 +0530 Subject: [PATCH 17/21] Remove content from cla.json file --- signatures/cla.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/signatures/cla.json b/signatures/cla.json index eae73d4..fe51488 100644 --- a/signatures/cla.json +++ b/signatures/cla.json @@ -1,3 +1 @@ -{ - "signed": [] -} +[] From 947a6b57c07421db1b4a4eb3c4c3fa8b03476775 Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Sat, 6 Dec 2025 02:46:11 +0530 Subject: [PATCH 18/21] fix: correct CLA signatures file structure --- signatures/cla.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/signatures/cla.json b/signatures/cla.json index fe51488..3bae4f0 100644 --- a/signatures/cla.json +++ b/signatures/cla.json @@ -1 +1,3 @@ -[] +{ + "signedContributors": [] +} \ No newline at end of file From df346eb08a1b79915502463ecd039b7fe2b04b4c Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Sat, 6 Dec 2025 04:01:01 +0530 Subject: [PATCH 19/21] ci: Update CLA workflow to grant write permissions for contents and remove internal comments. --- .github/workflows/cla.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index ff76093..1c4cafd 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -9,15 +9,13 @@ on: # Permissions needed for the workflow permissions: actions: write - contents: read + contents: write pull-requests: write statuses: write jobs: cla-check: runs-on: ubuntu-latest - # OPTIMIZATION: This logic is moved here. - # The job will only start if it's a PR update, OR a comment on a PR containing the magic words. if: > github.event_name == 'pull_request_target' || ( @@ -33,10 +31,8 @@ jobs: uses: cla-assistant/github-action@v2.5.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Ensure this secret is set in your repo settings! PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_ASSISTANT_TOKEN }} with: - # Path to the CLA document(s) path-to-signatures: 'signatures/cla.json' path-to-document: 'https://github.com/${{ github.repository }}/blob/main/ICLA.md' From 428cab115ac2ed798563450abe00832bf526864a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 22:37:40 +0000 Subject: [PATCH 20/21] @alam00000 has signed the CLA in alam00000/bentopdf#225 --- signatures/cla.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/signatures/cla.json b/signatures/cla.json index 3bae4f0..a372d4b 100644 --- a/signatures/cla.json +++ b/signatures/cla.json @@ -1,3 +1,12 @@ { - "signedContributors": [] + "signedContributors": [ + { + "name": "Akhrameev", + "id": 1945749, + "comment_id": 3618670485, + "created_at": "2025-12-05T21:32:56Z", + "repoId": 1074785178, + "pullRequestNo": 225 + } + ] } \ No newline at end of file From 1a18a630bcd19aa2de936a8198172bb7f94a94d2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 23:29:32 +0000 Subject: [PATCH 21/21] @Skillkiller has signed the CLA in alam00000/bentopdf#215 --- signatures/cla.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/signatures/cla.json b/signatures/cla.json index a372d4b..a04382d 100644 --- a/signatures/cla.json +++ b/signatures/cla.json @@ -7,6 +7,14 @@ "created_at": "2025-12-05T21:32:56Z", "repoId": 1074785178, "pullRequestNo": 225 + }, + { + "name": "Skillkiller", + "id": 18444809, + "comment_id": 3618954326, + "created_at": "2025-12-05T23:29:22Z", + "repoId": 1074785178, + "pullRequestNo": 215 } ] } \ No newline at end of file