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()}
-
-
-
-
Split Mode
-
- Extract by Page Range (Default)
- Split by Even/Odd Pages
- Split All Pages into Separate Files
- Select Pages Visually
- Split by Bookmarks
- Split N Times
-
-
-
-
How it works:
-
- Enter page numbers separated by commas (e.g., 2, 8, 14).
- Enter page ranges using a hyphen (e.g., 5-10).
- Combine them for complex selections (e.g., 1-3, 7, 12-15).
-
-
-
Total Pages:
-
Enter page range:
-
-
-
-
-
-
How it works:
-
This will create a new PDF containing only the even or only the odd pages from your original document.
-
-
-
-
- Odd Pages Only
-
-
-
- Even Pages Only
-
-
-
-
-
-
-
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.
-
-
-
Bookmark Level
-
- Level 0 (Top level only)
- Level 1
- Level 2
- Level 3
- All Levels
-
-
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.
-
-
-
Number of Pages per Split (N)
-
-
Each resulting PDF will contain N pages (except possibly the last one)
-
-
-
-
-
-
-
- Download pages as individual files in a ZIP
-
-
-
-
Split PDF
-
-
-`,
encrypt: () => `
- Encrypt PDF
- Add 256-bit AES password protection to your PDF.
- ${createFileInputHTML()}
-
-
-
-
User Password
-
-
Required to open and view the PDF
-
-
-
Owner Password (Optional)
-
-
Allows changing permissions and removing encryption
-
+ < 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.
+ ${ createFileInputHTML() }
+
+ < div id = "encrypt-options" class="hidden space-y-4 mt-6" >
+
+
User Password
+ < input required type = "password" id = "user-password-input" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder = "Password to open the PDF" >
+
Required to open and view the PDF
+
+ < div >
+
Owner Password(Optional)
+ < input type = "password" id = "owner-password-input" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder = "Password for full permissions (recommended)" >
+
Allows changing permissions and removing encryption
+
-
-
+ < !--Restriction checkboxes(shown when owner password is entered)-- >
+
+
đź”’ Restrict PDF Permissions
+ < p class="text-sm text-gray-400 mb-3" > Select which actions to disable:
+ < div class="space-y-2" >
+
+
+ Disable all modifications(--modify=none)
+
+ < label class="flex items-center space-x-2" >
+
+ Disable text and image extraction(--extract=n)
+
+ < label class="flex items-center space-x-2" >
+
+ Disable all printing(--print=none)
+
+ < label class="flex items-center space-x-2" >
+
+ Disable accessibility text copying(--accessibility=n)
+
+ < label class="flex items-center space-x-2" >
+
+ Disable annotations(--annotate=n)
+
+ < label class="flex items-center space-x-2" >
+
+ Disable page assembly(--assemble=n)
+
+ < label class="flex items-center space-x-2" >
+
+ Disable form filling(--form=n)
+
+ < label class="flex items-center space-x-2" >
+
+ Disable other modifications(--modify - other=n)
+
+
+
-
-
⚠️ 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.
-
- Encrypt & Download
-
-`,
+ < 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()}
-
-
-
- Enter PDF Password
-
-
-
Decrypt & Download
-
-
- `,
+ < 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" >
+
+ Enter PDF Password
+ < 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()}
-
-
- Save Changes
- `,
+ < 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()}
-
-
-
-
-
BATCH ACTIONS
-
-
-
-
-
Rotate By 90 degrees
-
-
-
- Left
-
-
-
- Right
-
-
-
-
-
-
-
-
-
Rotate By Custom Degrees
-
-
-
-
+ < h2 class="text-2xl font-bold text-white mb-4" > Rotate PDF
+ < p class="mb-6 text-gray-400" > Rotate all or specific pages in a PDF document.
+ ${ createFileInputHTML() }
+
-
- Apply
-
-
-
+ < div id = "rotate-all-controls" class="hidden my-6" >
+
+
BATCH ACTIONS
+ < div class="flex flex-col md:flex-row justify-center gap-6 items-center" >
-
-
-
-
-
Save Rotations
- `,
+
-
-
-
-
-
- Font Size
-
-
-
- Page Size
-
-
- A4 (210 x 297 mm)
- A3 (297 x 420 mm)
- A5 (148 x 210 mm)
- A6 (105 x 148 mm)
-
-
- Letter (8.5 x 11 in)
- Legal (8.5 x 14 in)
- Tabloid (11 x 17 in)
- Executive (7.25 x 10.5 in)
-
-
- B4 (250 x 353 mm)
- B5 (176 x 250 mm)
-
- Custom Size
-
-
-
- Orientation
-
- Portrait
- Landscape
-
-
-
-
- Text Color
-
-
-
- Create PDF
- `,
+ Select Languages
+ < div class="relative" >
+
+ English(Default)
+ < i data - lucide="chevron-down" class="w-4 h-4" >
+
+ < 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" >
+
-
-
- Display Units:
-
- Points (pt)
- Inches (in)
- Millimeters (mm)
- Pixels (at 96 DPI)
-
-
-
-
+ < !--Controls Row-- >
+
+
+ Display Units:
+ < select id = "units-select" class="bg-gray-700 border border-gray-600 text-white rounded-lg p-2" >
+ Points(pt)
+ < option value = "in" > Inches(in)
+ < option value = "mm" > Millimeters(mm)
+ < option value = "px" > Pixels(at 96 DPI)
+
+
+ < button id = "export-csv-btn" class="btn bg-indigo-600 hover:bg-indigo-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2" >
+
Export to CSV
-
-
+
+
-
-
-
-
-
- Page #
- Dimensions (W x H)
- Standard Size
- Orientation
- Aspect Ratio
- Area
- Rotation
-
-
-
-
-
-
-
- `,
+ < !--Dimensions Table-- >
+
+
+
+
+ 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
+
+
+ < tbody id = "dimensions-table-body" class="divide-y divide-gray-700" >
+
+
+
+
+ `,
'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() }
+
-
-
-
- Pages Per Sheet
-
- 2-Up
- 4-Up (2x2)
- 9-Up (3x3)
- 16-Up (4x4)
-
-
-
- Output Page Size
-
- Letter (8.5 x 11 in)
- Legal (8.5 x 14 in)
- Tabloid (11 x 17 in)
- A4 (210 x 297 mm)
- A3 (297 x 420 mm)
-
-
-
+ < div id = "n-up-options" class="hidden mt-6 space-y-4" >
+
+
+ Pages Per Sheet
+ < select id = "pages-per-sheet" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" >
+ 2 - Up
+ < option value = "4" selected > 4 - Up(2x2)
+ < option value = "9" > 9 - Up(3x3)
+ < option value = "16" > 16 - Up(4x4)
+
+
+ < div >
+
Output Page Size
+ < select id = "output-page-size" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" >
+
Letter(8.5 x 11 in)
+ < option value = "Legal" > Legal(8.5 x 14 in)
+ < option value = "Tabloid" > Tabloid(11 x 17 in)
+ < option value = "A4" selected > A4(210 x 297 mm)
+ < option value = "A3" > A3(297 x 420 mm)
+
+
+
-
-
- Output Orientation
-
- Automatic
- Portrait
- Landscape
-
-
-
-
-
- Add Margins & Gutters
-
-
-
+ < div class="grid grid-cols-1 sm:grid-cols-2 gap-4" >
+
+ Output Orientation
+ < select id = "output-orientation" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" >
+ Automatic
+ < option value = "portrait" > Portrait
+ < option value = "landscape" > Landscape
+
+
+ < div class="flex items-end pb-1" >
+
+
+ Add Margins & Gutters
+
+
+
-
+ < div class="border-t border-gray-700 pt-4 grid grid-cols-1 sm:grid-cols-2 gap-4" >
+
+
+
+ Draw Border Around Each Page
+
+
+ < div id = "border-color-wrapper" class="hidden" >
+ Border Color
+ < 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" >
+
+
- Create N-Up PDF
-
- `,
+ < 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() }
+
-
-
+ < div id = "page-manager-options" class="hidden mt-6" >
+
+
+ < button id = "process-btn" class="btn-gradient w-full mt-6" > Save New PDF
-
Save New PDF
-
- `,
+ `,
'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() }
+
-
-
- Orientation
-
- Vertical (Stack pages top to bottom)
- Horizontal (Stack pages left to right)
-
-
-
-
-
-
-
-
- Draw a separator line between pages
-
-
-
-
-
-
Combine Pages
-
- `,
+ < div id = "combine-options" class="hidden mt-6 space-y-4" >
+
+ Orientation
+ < select id = "combine-orientation" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" >
+ Vertical(Stack pages top to bottom)
+ < option value = "horizontal" > Horizontal(Stack pages left to right)
+
+
+
+ < div class="grid grid-cols-1 sm:grid-cols-2 gap-4" >
+
+ Spacing Between Pages(in points)
+ < 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 >
+ Background Color
+ < 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 >
+
+
+ Draw a separator line between pages
+
+
+
+ < 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" >
+
+ Separator Line Thickness(in points)
+ < 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 >
+ Separator Line Color
+ < 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()}
-
-
-
-
-
- Target Size
-
- A4
- Letter
- Legal
- Tabloid
- A3
- A5
- Custom Size...
-
-
-
- Orientation
-
- Portrait
- Landscape
-
-
-
-
-
-
- Width
-
-
-
- Height
-
-
-
- Units
-
- Inches
- Millimeters
-
-
-
+ < h2 class="text-2xl font-bold text-white mb-4" > Standardize Page Dimensions
+ < p class="mb-6 text-gray-400" > Convert all pages in your PDF to a uniform size.Choose a standard format or define a custom dimension.
+ ${ createFileInputHTML() }
+
+ < div id = "fix-dimensions-options" class="hidden mt-6 space-y-4" >
+
-
Content Scaling Method
-
-
-
-
-
Fit
-
Preserves all content, may add white bars.
-
-
-
-
-
-
Fill
-
Covers the page, may crop content.
-
-
-
-
+
Target Size
+ < select id = "target-size" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" >
+
A4
+ < option value = "Letter" > Letter
+ < option value = "Legal" > Legal
+ < option value = "Tabloid" > Tabloid
+ < option value = "A3" > A3
+ < option value = "A5" > A5
+ < option value = "Custom" > Custom Size...
+
+
+ < div >
+
Orientation
+ < select id = "orientation" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" >
+
Portrait
+ < option value = "landscape" > Landscape
+
+
+
-
- Background Color (for 'Fit' mode)
-
-
+ < div id = "custom-size-wrapper" class="hidden p-4 rounded-lg bg-gray-900 border border-gray-700 grid grid-cols-3 gap-3" >
+
+ Width
+ < 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 >
+ Height
+ < input type = "number" id = "custom-height" value = "11" class="w-full bg-gray-700 border-gray-600 text-white rounded-lg p-2" >
+
+ < div >
+ Units
+ < select id = "custom-units" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2" >
+ Inches
+ < option value = "mm" > Millimeters
+
+
+
- Standardize Pages
-
- `,
+ < div >
+ Content Scaling Method
+ < div class="flex gap-4 p-2 rounded-lg bg-gray-900" >
+
+
+
+ Fit
+ < p class="text-xs text-gray-400" > Preserves all content, may add white bars.
+
+
+ < 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 >
+ Background Color(for 'Fit' mode)
+ < 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()}
-
-
- Choose Background Color
-
- Apply Color & Download
-
- `,
+ < 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" >
+ Choose Background Color
+ < 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" >
+ Apply Color & Download
+
+ `,
'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()}
-
-
-
- Select Text Color
-
-
-
-
-
Original
-
+ < h2 class="text-2xl font-bold text-white mb-4" > Change Text Color
+ < p class="mb-6 text-gray-400" > 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() }
+
+ < div id = "text-color-options" class="hidden mt-6 space-y-4" >
+
+ Select Text Color
+ < input type = "color" id = "text-color-input" value = "#FF0000" class="w-full h-[42px] bg-gray-700 border border-gray-600 rounded-lg p-1 cursor-pointer" >
-
-
Preview
-
-
-
-
Apply Color & Download
-
- `,
+ < div class="grid grid-cols-2 gap-4" >
+
+
Original
+ < canvas id = "original-canvas" class="w-full h-auto rounded-lg border-2 border-gray-600" >
+
+ < div class="text-center" >
+
Preview
+ < canvas id = "text-color-canvas" class="w-full h-auto rounded-lg border-2 border-gray-600" >
+
+
+ < 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
-
-
-
-
-
+ < 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.
-
-
-
-
Page 1 of 1
-
-
-
- Overlay
- Side-by-Side
-
-
-
- Flicker
- Opacity:
-
-
-
-
-
- Sync Scrolling
-
-
-
-
-
- `,
+ < 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" >
+
Overlay
+ < 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" >
+ Flicker
+ < 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" >
+
+
+ Sync Scrolling
+
+
+
+ < 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()}
-
-
-
+ ${ createFileInputHTML() }
+
+
+ < div id = "ocr-options" class="hidden mt-6 space-y-4" >
-
Languages in Document
-
-
-
-
-
- Advanced Settings (Recommended to improve accuracy)
-
-
-
-
-
- Resolution
-
- Standard (192 DPI)
- High (288 DPI)
- Ultra (384 DPI)
-
-
-
-
-
- Binarize Image (Enhance Contrast for Clean Scans)
-
-
-
-
-
Character Whitelist Preset
-
- None (All characters)
- Alphanumeric + Basic Punctuation
- Numbers + Currency Symbols
- Letters Only (A-Z, a-z)
- Numbers Only (0-9)
- Invoice/Receipt (Numbers, $, ., -, /)
- Forms (Alphanumeric + Common Symbols)
- Custom...
-
-
Only these characters will be recognized. Leave empty for all characters.
-
-
-
-
-
Character Whitelist (Optional)
-
-
Only these characters will be recognized. Leave empty for all characters.
-
-
-
-
-
Start OCR
+ )
+ .join('')
+}
+
+ < 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" >
+
-
-
-
-
-
- Flatten PDF (use the Save button below)
-
-
+ < 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" >
- Save & Download Signed PDF
-
-`,
+ < div id = "signature-editor" class="hidden mt-6" >
+
+
-
- Save & Download Filled Form
-
-`,
+ ${ createFileInputHTML() }
+
+ < div id = "form-filler-options" class="hidden mt-6" >
+
+
+
+
+
+
+
+
+
+
+
+ Back to Tools
+
+
+
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.
+
+
+
+
+
+
+
+
+
Split Mode
+
+ Extract by Page Range (Default)
+ Split by Even/Odd Pages
+ Split All Pages into Separate Files
+ Select Pages Visually
+ Split by Bookmarks
+ Split N Times
+
+
+
+
+
How it works:
+
+ Enter page numbers separated by commas (e.g., 2, 8, 14).
+ Enter page ranges using a hyphen (e.g., 5-10).
+ Combine them for complex selections (e.g., 1-3, 7, 12-15).
+
+
+
Page Range
+
+
+
+
+
+
How it works:
+
+ Extract all even pages (2, 4, 6...) or all odd pages (1, 3, 5...) into a new PDF.
+
+
+
+
+ Even Pages
+
+
+
+ Odd Pages
+
+
+
+
+
+
How it works:
+
+ Every single page of the PDF will be saved as a separate PDF file.
+ The result will be downloaded as a ZIP file containing all the pages.
+
+
+
+
+
+
+
How it works:
+
+ Click on the page thumbnails below to select the pages you want to extract.
+ Selected pages will be highlighted.
+
+
+
+
+
+
+
+
+
+
How it works:
+
+ Split the PDF based on its bookmarks (outline).
+ Select the bookmark level to split at.
+
+
+
Bookmark
+ Level
+
+ All Levels
+ Level 0 (Top Level Only)
+ Level 1
+ Level 2
+ Level 3
+
+
+
+
+
+
How it works:
+
+ Split the PDF into multiple files, each containing N pages.
+
+
+
Pages per file
+ (N)
+
+
+
+
+
+
+
+
+
+ Download as ZIP (for
+ multiple files)
+
+
+
Split PDF
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
BentoPDF
+
+
+ © 2025 BentoPDF. All rights reserved.
+
+
+ Version
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+