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: () => ` -
Extract pages from a PDF using various methods.
- ${createFileInputHTML()} - -How it works:
-Total Pages:
- - -How it works:
-This will create a new PDF containing only the even or only the odd pages from your original document.
-How it works:
-Click on the page thumbnails below to select them. Click again to deselect. All selected pages will be extracted.
-How it works:
-This mode will create a separate PDF file for every single page in your document and download them together in one ZIP archive.
-How it works:
-Split the PDF at bookmark locations. Each bookmark will start a new PDF file.
-Select which bookmark nesting level to use for splitting
-How it works:
-Split the PDF into N equal parts. For example, a 40-page PDF with N=5 will create 8 PDFs with 5 pages each.
-Each resulting PDF will contain N pages (except possibly the last one)
-Note:
-Add 256-bit AES password protection to your PDF.
- ${createFileInputHTML()} - -Required to open and view the PDF
-Allows changing permissions and removing encryption
-Required to open and view the PDF
+Allows changing permissions and removing encryption
+Select which actions to disable:
-For strong security, set both passwords. Without an owner password, the security restrictions (printing, copying, etc.) can be easily bypassed.
-256-bit AES encryption without quality loss. Text remains selectable and searchable.
-Upload an encrypted PDF and provide its password to create an unlocked version.
- ${createFileInputHTML()} - -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() } +Rotate all or specific pages in a PDF document.
- ${createFileInputHTML()} - - -| Page # | -Dimensions (W x H) | -Standard Size | -Orientation | -Aspect Ratio | -Area | -Rotation | -
|---|
| Page # | + < 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 +
|---|
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() } +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() } +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() } +Convert all pages in your PDF to a uniform size. Choose a standard format or define a custom dimension.
- ${createFileInputHTML()} - - -Select a new background color for every page of your PDF.
- ${createFileInputHTML()} - -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()} - -Upload two files to visually compare them using either an overlay or a side-by-side view.
- -Upload Original PDF
-Upload Revised 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:
-Selected: None
-Only these characters will be recognized. Leave empty for all characters.
-Only these characters will be recognized. Leave empty for all characters.
-Initializing...
-+ 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.
+How it works:
+How it works:
+How it works:
+How it works:
+How it works:
+How it works:
++ Processing... +
+