From 6676fe9f8915fe08029efa5513d9b1518fbc0bbb Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Thu, 4 Dec 2025 14:02:20 +0530 Subject: [PATCH] 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:

+ + -
-

How it works:

- -
- - ${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'), }, }, },