feat: Reimplement PDF splitting functionality on a new dedicated page.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
547
src/js/logic/split-pdf-page.ts
Normal file
547
src/js/logic/split-pdf-page.ts
Normal file
@@ -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 = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user