From 64153e698b8a36f6e4707ce7fb1375d58a49c5ee Mon Sep 17 00:00:00 2001 From: alam00000 Date: Wed, 25 Feb 2026 13:17:30 +0530 Subject: [PATCH] fix: improve error handling, race condition and optimize page rendering logic in pdf multi tool --- src/js/logic/pdf-multi-tool.ts | 530 +++++++++++++++++++++++++-------- src/js/utils/render-utils.ts | 79 +++-- 2 files changed, 449 insertions(+), 160 deletions(-) diff --git a/src/js/logic/pdf-multi-tool.ts b/src/js/logic/pdf-multi-tool.ts index 92c4e7d..4515f06 100644 --- a/src/js/logic/pdf-multi-tool.ts +++ b/src/js/logic/pdf-multi-tool.ts @@ -1,10 +1,15 @@ import { createIcons, icons } from 'lucide'; -import { degrees, PDFDocument as PDFLibDocument } from 'pdf-lib'; +import { degrees, PDFDocument as PDFLibDocument, PDFPage } from 'pdf-lib'; import * as pdfjsLib from 'pdfjs-dist'; import JSZip from 'jszip'; import Sortable from 'sortablejs'; import { downloadFile, getPDFDocument } from '../utils/helpers'; -import { renderPagesProgressively, cleanupLazyRendering, renderPageToCanvas, createPlaceholder } from '../utils/render-utils'; +import { + renderPagesProgressively, + cleanupLazyRendering, + renderPageToCanvas, + createPlaceholder, +} from '../utils/render-utils'; import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'; import { repairPdfFile } from './repair-pdf.js'; @@ -41,13 +46,17 @@ let sortableInstance: Sortable | null = null; const pageCanvasCache = new Map(); -type Snapshot = { allPages: PageData[]; selectedPages: number[]; splitMarkers: number[] }; +type Snapshot = { + allPages: PageData[]; + selectedPages: number[]; + splitMarkers: number[]; +}; const undoStack: Snapshot[] = []; const redoStack: Snapshot[] = []; function snapshot() { const snap: Snapshot = { - allPages: allPages.map(p => ({ ...p, canvas: p.canvas })), + allPages: allPages.map((p) => ({ ...p, canvas: p.canvas })), selectedPages: Array.from(selectedPages), splitMarkers: Array.from(splitMarkers), }; @@ -56,16 +65,20 @@ function snapshot() { } function restore(snap: Snapshot) { - allPages = snap.allPages.map(p => ({ + allPages = snap.allPages.map((p) => ({ ...p, - canvas: p.canvas + canvas: p.canvas, })); selectedPages = new Set(snap.selectedPages); splitMarkers = new Set(snap.splitMarkers); updatePageDisplay(); } -function showModal(title: string, message: string, type: 'info' | 'error' | 'success' = 'info') { +function showModal( + title: string, + message: string, + type: 'info' | 'error' | 'success' = 'info' +) { const modal = document.getElementById('modal'); const modalTitle = document.getElementById('modal-title'); const modalMessage = document.getElementById('modal-message'); @@ -79,12 +92,12 @@ function showModal(title: string, message: string, type: 'info' | 'error' | 'suc const iconMap = { info: 'info', error: 'alert-circle', - success: 'check-circle' + success: 'check-circle', }; const colorMap = { info: 'text-blue-400', error: 'text-red-400', - success: 'text-green-400' + success: 'text-green-400', }; modalIcon.innerHTML = ``; @@ -112,7 +125,10 @@ function showLoading(current: number, total: number) { text.textContent = t('multiTool.renderingPages'); } -async function withButtonLoading(buttonId: string, action: () => Promise) { +async function withButtonLoading( + buttonId: string, + action: () => Promise +) { const button = document.getElementById(buttonId) as HTMLButtonElement; if (!button) return; @@ -122,7 +138,8 @@ async function withButtonLoading(buttonId: string, action: () => Promise) try { button.disabled = true; button.style.pointerEvents = 'none'; - button.innerHTML = ''; + button.innerHTML = + ''; await action(); } finally { @@ -143,7 +160,9 @@ if (document.readyState === 'loading') { initializeTool(); }); } else { - console.log('PDF Multi Tool: DOMContentLoaded already fired, initializing immediately'); + console.log( + 'PDF Multi Tool: DOMContentLoaded already fired, initializing immediately' + ); initializeTool(); } @@ -160,20 +179,30 @@ function initializeTool() { document.getElementById('upload-pdfs-btn')?.addEventListener('click', () => { console.log('Upload button clicked, isRendering:', isRendering); if (isRendering) { - showModal(t('multiTool.pleaseWait'), t('multiTool.pagesRendering'), 'info'); + showModal( + t('multiTool.pleaseWait'), + t('multiTool.pagesRendering'), + 'info' + ); return; } document.getElementById('pdf-file-input')?.click(); }); - document.getElementById('pdf-file-input')?.addEventListener('change', handlePdfUpload); - document.getElementById('insert-pdf-input')?.addEventListener('change', handleInsertPdf); + document + .getElementById('pdf-file-input') + ?.addEventListener('change', handlePdfUpload); + document + .getElementById('insert-pdf-input') + ?.addEventListener('change', handleInsertPdf); - document.getElementById('bulk-rotate-left-btn')?.addEventListener('click', () => { - if (isRendering) return; - snapshot(); - bulkRotate(-90); - }); + document + .getElementById('bulk-rotate-left-btn') + ?.addEventListener('click', () => { + if (isRendering) return; + snapshot(); + bulkRotate(-90); + }); document.getElementById('bulk-rotate-btn')?.addEventListener('click', () => { if (isRendering) return; snapshot(); @@ -184,27 +213,38 @@ function initializeTool() { snapshot(); bulkDelete(); }); - document.getElementById('bulk-duplicate-btn')?.addEventListener('click', () => { - if (isRendering) return; - snapshot(); - bulkDuplicate(); - }); + document + .getElementById('bulk-duplicate-btn') + ?.addEventListener('click', () => { + if (isRendering) return; + snapshot(); + bulkDuplicate(); + }); document.getElementById('bulk-split-btn')?.addEventListener('click', () => { if (isRendering) return; snapshot(); bulkSplit(); }); - document.getElementById('bulk-download-btn')?.addEventListener('click', () => { - if (isRendering) return; - if (isRendering) return; - if (selectedPages.size === 0) { - showModal(t('multiTool.noPagesSelected'), t('multiTool.selectOnePage'), 'info'); - return; - } - withButtonLoading('bulk-download-btn', async () => { - await downloadPagesAsPdf(Array.from(selectedPages).sort((a, b) => a - b), 'selected-pages.pdf'); + document + .getElementById('bulk-download-btn') + ?.addEventListener('click', () => { + if (isRendering) return; + if (isRendering) return; + if (selectedPages.size === 0) { + showModal( + t('multiTool.noPagesSelected'), + t('multiTool.selectOnePage'), + 'info' + ); + return; + } + withButtonLoading('bulk-download-btn', async () => { + await downloadPagesAsPdf( + Array.from(selectedPages).sort((a, b) => a - b), + 'selected-pages.pdf' + ); + }); }); - }); document.getElementById('select-all-btn')?.addEventListener('click', () => { if (isRendering) return; @@ -229,17 +269,19 @@ function initializeTool() { await downloadAll(); }); }); - document.getElementById('add-blank-page-btn')?.addEventListener('click', () => { - if (isRendering) return; - snapshot(); - addBlankPage(); - }); + document + .getElementById('add-blank-page-btn') + ?.addEventListener('click', () => { + if (isRendering) return; + snapshot(); + addBlankPage(); + }); document.getElementById('undo-btn')?.addEventListener('click', () => { if (isRendering) return; const last = undoStack.pop(); if (last) { const current: Snapshot = { - allPages: allPages.map(p => ({ ...p })), + allPages: allPages.map((p) => ({ ...p })), selectedPages: Array.from(selectedPages), splitMarkers: Array.from(splitMarkers), }; @@ -252,7 +294,7 @@ function initializeTool() { const next = redoStack.pop(); if (next) { const current: Snapshot = { - allPages: allPages.map(p => ({ ...p })), + allPages: allPages.map((p) => ({ ...p })), selectedPages: Array.from(selectedPages), splitMarkers: Array.from(splitMarkers), }; @@ -269,7 +311,9 @@ function initializeTool() { } }); - document.getElementById('modal-close-btn')?.addEventListener('click', hideModal); + document + .getElementById('modal-close-btn') + ?.addEventListener('click', hideModal); document.getElementById('modal')?.addEventListener('click', (e) => { if (e.target === document.getElementById('modal')) { hideModal(); @@ -288,7 +332,9 @@ function initializeTool() { uploadArea.addEventListener('drop', (e) => { e.preventDefault(); uploadArea.classList.remove('border-indigo-500'); - const files = Array.from(e.dataTransfer?.files || []).filter(f => f.type === 'application/pdf'); + const files = Array.from(e.dataTransfer?.files || []).filter( + (f) => f.type === 'application/pdf' + ); if (files.length > 0) { loadPdfs(files); } @@ -361,27 +407,38 @@ async function loadPdfs(files: File[]) { try { console.log(`Repairing ${file.name}...`); const loadingText = document.getElementById('loading-text'); - if (loadingText) loadingText.textContent = `Repairing ${file.name}...`; + if (loadingText) + loadingText.textContent = `Repairing ${file.name}...`; const repairedData = await repairPdfFile(file); if (repairedData) { arrayBuffer = repairedData.buffer as ArrayBuffer; console.log(`Successfully repaired ${file.name} before loading.`); } else { - console.warn(`Repair returned null for ${file.name}, using original file.`); + console.warn( + `Repair returned null for ${file.name}, using original file.` + ); arrayBuffer = await file.arrayBuffer(); } } catch (repairError) { - console.warn(`Failed to repair ${file.name}, attempting to load original:`, repairError); + console.warn( + `Failed to repair ${file.name}, attempting to load original:`, + repairError + ); arrayBuffer = await file.arrayBuffer(); } - const pdfDoc = await PDFLibDocument.load(arrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false }); + const pdfDoc = await PDFLibDocument.load(arrayBuffer, { + ignoreEncryption: true, + throwOnInvalidObject: false, + }); currentPdfDocs.push(pdfDoc); const pdfIndex = currentPdfDocs.length - 1; const pdfBytes = await pdfDoc.save(); - const pdfjsDoc = await getPDFDocument({ data: new Uint8Array(pdfBytes) }).promise; + const pdfjsDoc = await getPDFDocument({ + data: new Uint8Array(pdfBytes), + }).promise; const numPages = pdfjsDoc.numPages; // Pre-fill allPages with placeholders to maintain order/state @@ -425,10 +482,13 @@ async function loadPdfs(files: File[]) { shouldCancel: () => renderCancelled, // Pass cancellation check } ); - } catch (e) { console.error(`Failed to load PDF ${file.name}:`, e); - showModal(t('multiTool.error'), `${t('multiTool.failedToLoad')} ${file.name}.`, 'error'); + showModal( + t('multiTool.error'), + `${t('multiTool.failedToLoad')} ${file.name}.`, + 'error' + ); } } @@ -439,7 +499,9 @@ async function loadPdfs(files: File[]) { } finally { hideLoading(); isRendering = false; - console.log('PDF Multi Tool: Render finished/cancelled, isRendering set to false'); + console.log( + 'PDF Multi Tool: Render finished/cancelled, isRendering set to false' + ); if (renderCancelled) { renderCancelled = false; } @@ -464,7 +526,10 @@ function createPageCard(pageData: PageData, index: number) { } // Modified to return the element instead of appending it -function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTMLElement { +function createPageElement( + canvas: HTMLCanvasElement | null, + index: number +): HTMLElement { const pageData = allPages[index]; if (!pageData) { console.error(`Page data not found for index ${index}`); @@ -472,7 +537,8 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM } const card = document.createElement('div'); - card.className = 'bg-gray-800 rounded-lg border-2 border-gray-700 p-2 relative group cursor-move'; + card.className = + 'bg-gray-800 rounded-lg border-2 border-gray-700 p-2 relative group cursor-move'; card.dataset.pageIndex = index.toString(); card.dataset.pageId = pageData.id; // Set ID for reconciliation @@ -490,7 +556,8 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM } const preview = document.createElement('div'); - preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative h-36 sm:h-64'; + preview.className = + 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative h-36 sm:h-64'; if (canvas) { const previewCanvas = canvas; @@ -502,7 +569,8 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM } else { // Show loading placeholder if canvas is null const loading = document.createElement('div'); - loading.className = 'flex flex-col items-center justify-center text-gray-400'; + loading.className = + 'flex flex-col items-center justify-center text-gray-400'; loading.innerHTML = ` ${t('common.loading')} @@ -518,15 +586,18 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM // Actions toolbar const actions = document.createElement('div'); - actions.className = 'flex items-center justify-center gap-1 sm:opacity-0 group-hover:opacity-100 transition-opacity absolute bottom-2 left-0 right-0'; + actions.className = + 'flex items-center justify-center gap-1 sm:opacity-0 group-hover:opacity-100 transition-opacity absolute bottom-2 left-0 right-0'; const actionsInner = document.createElement('div'); - actionsInner.className = 'flex items-center gap-1 bg-gray-900/90 rounded px-2 py-1'; + actionsInner.className = + 'flex items-center gap-1 bg-gray-900/90 rounded px-2 py-1'; actions.appendChild(actionsInner); // Select checkbox const selectBtn = document.createElement('button'); - selectBtn.className = 'absolute top-2 right-2 p-1 rounded bg-gray-900/70 hover:bg-gray-800 z-10'; + selectBtn.className = + 'absolute top-2 right-2 p-1 rounded bg-gray-900/70 hover:bg-gray-800 z-10'; selectBtn.innerHTML = selectedPages.has(index) ? '' : ''; @@ -538,14 +609,16 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM // Rotate button const rotateBtn = document.createElement('button'); rotateBtn.className = 'p-1 rounded hover:bg-gray-700'; - rotateBtn.innerHTML = ''; + rotateBtn.innerHTML = + ''; rotateBtn.onclick = (e) => { e.stopPropagation(); rotatePage(index, 90); }; const rotateLeftBtn = document.createElement('button'); rotateLeftBtn.className = 'p-1 rounded hover:bg-gray-700'; - rotateLeftBtn.innerHTML = ''; + rotateLeftBtn.innerHTML = + ''; rotateLeftBtn.onclick = (e) => { e.stopPropagation(); rotatePage(index, -90); @@ -554,7 +627,8 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM // Duplicate button const duplicateBtn = document.createElement('button'); duplicateBtn.className = 'p-1 rounded hover:bg-gray-700'; - duplicateBtn.innerHTML = ''; + duplicateBtn.innerHTML = + ''; duplicateBtn.title = t('multiTool.actions.duplicatePage'); duplicateBtn.onclick = (e) => { e.stopPropagation(); @@ -565,7 +639,8 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM // Delete button const deleteBtn = document.createElement('button'); deleteBtn.className = 'p-1 rounded hover:bg-gray-700'; - deleteBtn.innerHTML = ''; + deleteBtn.innerHTML = + ''; deleteBtn.title = t('multiTool.actions.deletePage'); deleteBtn.onclick = (e) => { e.stopPropagation(); @@ -576,7 +651,8 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM // Insert PDF button const insertBtn = document.createElement('button'); insertBtn.className = 'p-1 rounded hover:bg-gray-700'; - insertBtn.innerHTML = ''; + insertBtn.innerHTML = + ''; insertBtn.title = t('multiTool.actions.insertPdf'); insertBtn.onclick = (e) => { e.stopPropagation(); @@ -587,7 +663,8 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM // Split button const splitBtn = document.createElement('button'); splitBtn.className = 'p-1 rounded hover:bg-gray-700'; - splitBtn.innerHTML = ''; + splitBtn.innerHTML = + ''; splitBtn.title = t('multiTool.actions.toggleSplit'); splitBtn.onclick = (e) => { e.stopPropagation(); @@ -596,14 +673,23 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM renderSplitMarkers(); }; - actionsInner.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn); + actionsInner.append( + rotateLeftBtn, + rotateBtn, + duplicateBtn, + insertBtn, + splitBtn, + deleteBtn + ); card.append(preview, info, actions, selectBtn); // Check for split marker if (splitMarkers.has(index)) { const marker = document.createElement('div'); - marker.className = 'split-marker absolute -right-3 top-0 bottom-0 w-6 flex items-center justify-center z-20 pointer-events-none'; - marker.innerHTML = '
'; + marker.className = + 'split-marker absolute -right-3 top-0 bottom-0 w-6 flex items-center justify-center z-20 pointer-events-none'; + marker.innerHTML = + '
'; card.appendChild(marker); } @@ -655,15 +741,19 @@ function toggleSelectOptimized(index: number) { const card = pagesContainer.children[index] as HTMLElement; if (!card) return; - const selectBtn = card.querySelector('button[class*="absolute top-2 right-2"]'); + const selectBtn = card.querySelector( + 'button[class*="absolute top-2 right-2"]' + ); if (!selectBtn) return; if (selectedPages.has(index)) { card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500'); - selectBtn.innerHTML = ''; + selectBtn.innerHTML = + ''; } else { card.classList.remove('border-indigo-500', 'ring-2', 'ring-indigo-500'); - selectBtn.innerHTML = ''; + selectBtn.innerHTML = + ''; } createIcons({ icons }); @@ -730,7 +820,7 @@ function deletePage(index: number) { allPages.splice(index, 1); selectedPages.delete(index); const newSelected = new Set(); - selectedPages.forEach(i => { + selectedPages.forEach((i) => { if (i > index) newSelected.add(i - 1); else if (i < index) newSelected.add(i); }); @@ -759,13 +849,17 @@ async function handleInsertPdf(e: Event) { try { const arrayBuffer = await file.arrayBuffer(); - const pdfDoc = await PDFLibDocument.load(arrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false }); + const pdfDoc = await PDFLibDocument.load(arrayBuffer, { + ignoreEncryption: true, + throwOnInvalidObject: false, + }); currentPdfDocs.push(pdfDoc); const pdfIndex = currentPdfDocs.length - 1; // Load PDF.js document for rendering const pdfBytes = await pdfDoc.save(); - const pdfjsDoc = await getPDFDocument({ data: new Uint8Array(pdfBytes) }).promise; + const pdfjsDoc = await getPDFDocument({ data: new Uint8Array(pdfBytes) }) + .promise; const numPages = pdfjsDoc.numPages; const newPages: PageData[] = []; @@ -802,13 +896,18 @@ async function handleInsertPdf(e: Event) { // Update UI if card exists const pagesContainer = document.getElementById('pages-container'); - const card = pagesContainer?.querySelector(`div[data-page-index="${globalIndex}"]`); + const card = pagesContainer?.querySelector( + `div[data-page-index="${globalIndex}"]` + ); if (card) { - const preview = card.querySelector('.bg-gray-700') || card.querySelector('.bg-white'); + const preview = + card.querySelector('.bg-gray-700') || + card.querySelector('.bg-white'); if (preview) { // Re-create the preview content preview.innerHTML = ''; - preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative h-36 sm:h-64'; + preview.className = + 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative h-36 sm:h-64'; const previewCanvas = canvas; previewCanvas.className = 'max-w-full max-h-full object-contain'; @@ -819,10 +918,13 @@ async function handleInsertPdf(e: Event) { } } } - } catch (e) { console.error('Failed to insert PDF:', e); - showModal('Error', 'Failed to insert PDF. The file may be corrupted.', 'error'); + showModal( + 'Error', + 'Failed to insert PDF. The file may be corrupted.', + 'error' + ); } input.value = ''; @@ -837,13 +939,15 @@ function renderSplitMarkers() { const pagesContainer = document.getElementById('pages-container'); if (!pagesContainer) return; - pagesContainer.querySelectorAll('.split-marker').forEach(m => m.remove()); + pagesContainer.querySelectorAll('.split-marker').forEach((m) => m.remove()); Array.from(pagesContainer.children).forEach((cardEl, i) => { if (splitMarkers.has(i)) { const marker = document.createElement('div'); - marker.className = 'split-marker absolute -right-3 top-0 bottom-0 w-6 flex items-center justify-center z-20 pointer-events-none'; - marker.innerHTML = '
'; + marker.className = + 'split-marker absolute -right-3 top-0 bottom-0 w-6 flex items-center justify-center z-20 pointer-events-none'; + marker.innerHTML = + '
'; (cardEl as HTMLElement).appendChild(marker); } }); @@ -881,7 +985,7 @@ function bulkRotate(delta: number) { return; } - selectedPages.forEach(index => { + selectedPages.forEach((index) => { const pageData = allPages[index]; if (pageData) { // Update state @@ -890,13 +994,15 @@ function bulkRotate(delta: number) { // Update DOM immediately if it exists const pagesContainer = document.getElementById('pages-container'); - const card = pagesContainer?.querySelector(`div[data-page-index="${index}"]`); + const card = pagesContainer?.querySelector( + `div[data-page-index="${index}"]` + ); if (card) { const canvas = card.querySelector('canvas'); if (canvas) { canvas.style.transform = `rotate(${pageData.visualRotation}deg)`; } - // If no canvas (placeholder), the state update is enough. + // If no canvas (placeholder), the state update is enough. // When it eventually renders, createPageElement will use the new rotation. } } @@ -911,7 +1017,7 @@ function bulkDelete() { return; } const indices = Array.from(selectedPages).sort((a, b) => b - a); - indices.forEach(index => allPages.splice(index, 1)); + indices.forEach((index) => allPages.splice(index, 1)); selectedPages.clear(); if (allPages.length === 0) { @@ -928,7 +1034,7 @@ function bulkDuplicate() { return; } const indices = Array.from(selectedPages).sort((a, b) => b - a); - indices.forEach(index => { + indices.forEach((index) => { duplicatePage(index); }); selectedPages.clear(); @@ -937,11 +1043,15 @@ function bulkDuplicate() { function bulkSplit() { if (selectedPages.size === 0) { - showModal('No Selection', 'Please select pages to mark for splitting.', 'info'); + showModal( + 'No Selection', + 'Please select pages to mark for splitting.', + 'info' + ); return; } const indices = Array.from(selectedPages); - indices.forEach(index => { + indices.forEach((index) => { if (!splitMarkers.has(index)) { splitMarkers.add(index); } @@ -951,7 +1061,6 @@ function bulkSplit() { updatePageDisplay(); } - async function downloadAll() { if (allPages.length === 0) { showModal('No Pages', 'Please upload PDFs first.', 'info'); @@ -993,24 +1102,63 @@ async function downloadSplitPdfs() { segments.push(currentSegment); } - // Create PDFs for each segment for (let segIndex = 0; segIndex < segments.length; segIndex++) { const segment = segments[segIndex]; const newPdf = await PDFLibDocument.create(); + const segSpecs: ( + | { + type: 'pdf'; + pdfDoc: PDFLibDocument; + originalPageIndex: number; + rotation: number; + } + | { type: 'blank' } + )[] = []; for (const index of segment) { const pageData = allPages[index]; - if (!pageData) { - console.warn(`Page data missing for index ${index}`); - continue; - } + if (!pageData) continue; if (pageData.pdfDoc && pageData.originalPageIndex >= 0) { - const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]); - const page = newPdf.addPage(copiedPage); + segSpecs.push({ + type: 'pdf', + pdfDoc: pageData.pdfDoc, + originalPageIndex: pageData.originalPageIndex, + rotation: pageData.rotation, + }); + } else { + segSpecs.push({ type: 'blank' }); + } + } - if (pageData.rotation !== 0) { + const docPageIndices = new Map(); + for (const spec of segSpecs) { + if (spec.type === 'pdf') { + if (!docPageIndices.has(spec.pdfDoc)) { + docPageIndices.set(spec.pdfDoc, []); + } + docPageIndices.get(spec.pdfDoc)!.push(spec.originalPageIndex); + } + } + + const copiedPagesMap = new Map(); + for (const [doc, pageIdxs] of Array.from(docPageIndices)) { + const copied = await newPdf.copyPages(doc, pageIdxs); + copiedPagesMap.set(doc, copied); + } + + const docConsumeIndex = new Map(); + docPageIndices.forEach((_, doc) => docConsumeIndex.set(doc, 0)); + + for (const spec of segSpecs) { + if (spec.type === 'pdf') { + const idx = docConsumeIndex.get(spec.pdfDoc)!; + const copiedPage = copiedPagesMap.get(spec.pdfDoc)![idx]; + docConsumeIndex.set(spec.pdfDoc, idx + 1); + + const page = newPdf.addPage(copiedPage); + if (spec.rotation !== 0) { const currentRotation = page.getRotation().angle; - page.setRotation(degrees(currentRotation + pageData.rotation)); + page.setRotation(degrees(currentRotation + spec.rotation)); } } else { newPdf.addPage([595, 842]); @@ -1025,7 +1173,11 @@ async function downloadSplitPdfs() { const zipBlob = await zip.generateAsync({ type: 'blob' }); downloadFile(zipBlob, 'split-documents.zip'); - showModal('Success', `Downloaded ${segments.length} PDF files in a ZIP archive.`, 'success'); + showModal( + 'Success', + `Downloaded ${segments.length} PDF files in a ZIP archive.`, + 'success' + ); } catch (e) { console.error('Failed to create split PDFs:', e); showModal('Error', 'Failed to create split PDFs.', 'error'); @@ -1038,20 +1190,59 @@ async function downloadPagesAsPdf(indices: number[], filename: string) { try { const newPdf = await PDFLibDocument.create(); + const pageSpecs: ( + | { + type: 'pdf'; + pdfDoc: PDFLibDocument; + originalPageIndex: number; + rotation: number; + } + | { type: 'blank' } + )[] = []; for (const index of indices) { const pageData = allPages[index]; - if (!pageData) { - console.warn(`Page data missing for index ${index}`); - continue; - } + if (!pageData) continue; if (pageData.pdfDoc && pageData.originalPageIndex >= 0) { - // Copy page from original PDF - const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]); - const page = newPdf.addPage(copiedPage); + pageSpecs.push({ + type: 'pdf', + pdfDoc: pageData.pdfDoc, + originalPageIndex: pageData.originalPageIndex, + rotation: pageData.rotation, + }); + } else { + pageSpecs.push({ type: 'blank' }); + } + } - if (pageData.rotation !== 0) { + const docPageIndices = new Map(); + for (const spec of pageSpecs) { + if (spec.type === 'pdf') { + if (!docPageIndices.has(spec.pdfDoc)) { + docPageIndices.set(spec.pdfDoc, []); + } + docPageIndices.get(spec.pdfDoc)!.push(spec.originalPageIndex); + } + } + + const copiedPagesMap = new Map(); + for (const [doc, pageIdxs] of Array.from(docPageIndices)) { + const copied = await newPdf.copyPages(doc, pageIdxs); + copiedPagesMap.set(doc, copied); + } + + const docConsumeIndex = new Map(); + docPageIndices.forEach((_, doc) => docConsumeIndex.set(doc, 0)); + + for (const spec of pageSpecs) { + if (spec.type === 'pdf') { + const idx = docConsumeIndex.get(spec.pdfDoc)!; + const copiedPage = copiedPagesMap.get(spec.pdfDoc)![idx]; + docConsumeIndex.set(spec.pdfDoc, idx + 1); + + const page = newPdf.addPage(copiedPage); + if (spec.rotation !== 0) { const currentRotation = page.getRotation().angle; - page.setRotation(degrees(currentRotation + pageData.rotation)); + page.setRotation(degrees(currentRotation + spec.rotation)); } } else { newPdf.addPage([595, 842]); @@ -1059,7 +1250,9 @@ async function downloadPagesAsPdf(indices: number[], filename: string) { } const pdfBytes = await newPdf.save(); - const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }); + const blob = new Blob([new Uint8Array(pdfBytes)], { + type: 'application/pdf', + }); downloadFile(blob, filename); showModal('Success', 'PDF downloaded successfully.', 'success'); @@ -1101,18 +1294,28 @@ function updatePageDisplay() { // Update index-dependent attributes card.dataset.pageIndex = index.toString(); - const info = card.querySelector('.text-xs.text-gray-400.text-center.mb-2'); + const info = card.querySelector( + '.text-xs.text-gray-400.text-center.mb-2' + ); if (info) info.textContent = `Page ${index + 1} `; // Update selection state - const selectBtn = card.querySelector('button[class*="absolute top-2 right-2"]'); + const selectBtn = card.querySelector( + 'button[class*="absolute top-2 right-2"]' + ); if (selectBtn) { if (selectedPages.has(index)) { card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500'); - selectBtn.innerHTML = ''; + selectBtn.innerHTML = + ''; } else { - card.classList.remove('border-indigo-500', 'ring-2', 'ring-indigo-500'); - selectBtn.innerHTML = ''; + card.classList.remove( + 'border-indigo-500', + 'ring-2', + 'ring-indigo-500' + ); + selectBtn.innerHTML = + ''; } // Update click handler to use new index (selectBtn as HTMLElement).onclick = (e) => { @@ -1128,17 +1331,47 @@ function updatePageDisplay() { } // Update action buttons - const actionsInner = card.querySelector('.flex.items-center.gap-1.bg-gray-900\\/90'); + const actionsInner = card.querySelector( + '.flex.items-center.gap-1.bg-gray-900\\/90' + ); if (actionsInner) { const buttons = actionsInner.querySelectorAll('button'); - if (buttons[0]) (buttons[0] as HTMLElement).onclick = (e) => { e.stopPropagation(); rotatePage(index, -90); }; - if (buttons[1]) (buttons[1] as HTMLElement).onclick = (e) => { e.stopPropagation(); rotatePage(index, 90); }; - if (buttons[2]) (buttons[2] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); duplicatePage(index); }; - if (buttons[3]) (buttons[3] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); insertPdfAfter(index); }; - if (buttons[4]) (buttons[4] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); toggleSplitMarker(index); renderSplitMarkers(); }; - if (buttons[5]) (buttons[5] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); deletePage(index); }; + if (buttons[0]) + (buttons[0] as HTMLElement).onclick = (e) => { + e.stopPropagation(); + rotatePage(index, -90); + }; + if (buttons[1]) + (buttons[1] as HTMLElement).onclick = (e) => { + e.stopPropagation(); + rotatePage(index, 90); + }; + if (buttons[2]) + (buttons[2] as HTMLElement).onclick = (e) => { + e.stopPropagation(); + snapshot(); + duplicatePage(index); + }; + if (buttons[3]) + (buttons[3] as HTMLElement).onclick = (e) => { + e.stopPropagation(); + snapshot(); + insertPdfAfter(index); + }; + if (buttons[4]) + (buttons[4] as HTMLElement).onclick = (e) => { + e.stopPropagation(); + snapshot(); + toggleSplitMarker(index); + renderSplitMarkers(); + }; + if (buttons[5]) + (buttons[5] as HTMLElement).onclick = (e) => { + e.stopPropagation(); + snapshot(); + deletePage(index); + }; } - } else { // Element doesn't exist, create it card = createPageElement(pageData.canvas, index); @@ -1179,7 +1412,9 @@ function updatePageNumbers() { // We need to find the buttons and update their onclick handlers // This is necessary because the original handlers captured the old index - const selectBtn = card.querySelector('button[class*="absolute top-2 right-2"]') as HTMLButtonElement; + const selectBtn = card.querySelector( + 'button[class*="absolute top-2 right-2"]' + ) as HTMLButtonElement; if (selectBtn) { selectBtn.onclick = (e) => { e.stopPropagation(); @@ -1187,16 +1422,47 @@ function updatePageNumbers() { }; } - const actionsInner = card.querySelector('.flex.items-center.gap-1.bg-gray-900\\/90'); + const actionsInner = card.querySelector( + '.flex.items-center.gap-1.bg-gray-900\\/90' + ); if (actionsInner) { const buttons = actionsInner.querySelectorAll('button'); // Order: Rotate Left, Rotate Right, Duplicate, Insert, Split, Delete - if (buttons[0]) buttons[0].onclick = (e) => { e.stopPropagation(); rotatePage(index, -90); }; - if (buttons[1]) buttons[1].onclick = (e) => { e.stopPropagation(); rotatePage(index, 90); }; - if (buttons[2]) buttons[2].onclick = (e) => { e.stopPropagation(); snapshot(); duplicatePage(index); }; - if (buttons[3]) buttons[3].onclick = (e) => { e.stopPropagation(); snapshot(); insertPdfAfter(index); }; - if (buttons[4]) buttons[4].onclick = (e) => { e.stopPropagation(); snapshot(); toggleSplitMarker(index); renderSplitMarkers(); }; - if (buttons[5]) buttons[5].onclick = (e) => { e.stopPropagation(); snapshot(); deletePage(index); }; + if (buttons[0]) + buttons[0].onclick = (e) => { + e.stopPropagation(); + rotatePage(index, -90); + }; + if (buttons[1]) + buttons[1].onclick = (e) => { + e.stopPropagation(); + rotatePage(index, 90); + }; + if (buttons[2]) + buttons[2].onclick = (e) => { + e.stopPropagation(); + snapshot(); + duplicatePage(index); + }; + if (buttons[3]) + buttons[3].onclick = (e) => { + e.stopPropagation(); + snapshot(); + insertPdfAfter(index); + }; + if (buttons[4]) + buttons[4].onclick = (e) => { + e.stopPropagation(); + snapshot(); + toggleSplitMarker(index); + renderSplitMarkers(); + }; + if (buttons[5]) + buttons[5].onclick = (e) => { + e.stopPropagation(); + snapshot(); + deletePage(index); + }; } }); -} \ No newline at end of file +} diff --git a/src/js/utils/render-utils.ts b/src/js/utils/render-utils.ts index 7a0e807..df27264 100644 --- a/src/js/utils/render-utils.ts +++ b/src/js/utils/render-utils.ts @@ -103,7 +103,10 @@ export async function renderPageToCanvas( canvas.height = viewport.height; canvas.width = viewport.width; - const context = canvas.getContext('2d')!; + const context = canvas.getContext('2d'); + if (!context) { + throw new Error(`Failed to get 2D context for page ${pageNumber}`); + } await page.render({ canvasContext: context, @@ -115,12 +118,9 @@ export async function renderPageToCanvas( } /** - * Renders a batch of pages in parallel + * Renders a batch of pages */ -async function renderPageBatch( - tasks: PageTask[], - onProgress?: (current: number, total: number) => void -): Promise { +async function renderPageBatch(tasks: PageTask[]): Promise { for (const task of tasks) { try { const canvas = await renderPageToCanvas( @@ -147,6 +147,13 @@ async function renderPageBatch( parent.insertBefore(wrapper, placeholder); parent.removeChild(placeholder); } else { + const existingRendered = task.container.querySelector( + `[data-page-number="${task.pageNumber}"]:not([data-lazy-load="true"])` + ); + if (existingRendered) { + continue; + } + const allChildren = Array.from( task.container.children ) as HTMLElement[]; @@ -207,7 +214,7 @@ function setupLazyRendering( task.placeholderElement = placeholder; // Render this page immediately (not waiting for isRendering flag) - renderPageBatch([task], config.onProgress) + renderPageBatch([task]) .then(() => { // Trigger callback after lazy load batch if (config.onBatchComplete) { @@ -263,21 +270,19 @@ export async function renderPagesProgressively( config: RenderConfig = {} ): Promise { const { - batchSize = 8, // Increased from 5 to 8 for faster initial render + batchSize = 8, useLazyLoading = true, - eagerLoadBatches = 2, // Eagerly load 1 batch ahead by default + eagerLoadBatches = 2, onProgress, onBatchComplete, } = config; const totalPages = pdfjsDoc.numPages; - // Render more pages initially to reduce lazy loading issues const initialRenderCount = useLazyLoading - ? Math.min(20, totalPages) // Increased from 12 to 20 pages + ? Math.min(20, totalPages) : totalPages; - // CRITICAL FIX: Create placeholders for ALL pages first to maintain order const placeholders: HTMLElement[] = []; for (let i = 1; i <= totalPages; i++) { const placeholder = createPlaceholder(i); @@ -293,7 +298,7 @@ export async function renderPagesProgressively( pageNumber: i, pdfjsDoc, container, - scale: config.useLazyLoading ? 0.3 : 0.5, + scale: useLazyLoading ? 0.3 : 0.5, createWrapper, placeholderElement: placeholders[i - 1], }); @@ -330,19 +335,24 @@ export async function renderPagesProgressively( const batch = initialTasks.slice(i, i + batchSize); - await new Promise((resolve) => { - requestIdleCallbackPolyfill(async () => { - await renderPageBatch(batch, onProgress); + await new Promise((resolve, reject) => { + requestIdleCallbackPolyfill(() => { + renderPageBatch(batch) + .then(() => { + if (onProgress) { + onProgress( + Math.min(i + batchSize, initialRenderCount), + totalPages + ); + } - if (onProgress) { - onProgress(Math.min(i + batchSize, initialRenderCount), totalPages); - } + if (onBatchComplete) { + onBatchComplete(); + } - if (onBatchComplete) { - onBatchComplete(); - } - - resolve(); + resolve(); + }) + .catch(reject); }); }); } @@ -397,8 +407,11 @@ function renderEagerBatch(config: RenderConfig): void { requestIdleCallbackPolyfill(async () => { if (config.shouldCancel?.()) return; - // Remove these tasks from pending since we're rendering them eagerly - batch.forEach((task) => { + const tasksToRender = batch.filter((task) => + lazyLoadState.pendingTasksByPageNumber.has(task.pageNumber) + ); + + tasksToRender.forEach((task) => { const placeholder = task.placeholderElement; if (placeholder && lazyLoadState.observer) { lazyLoadState.observer.unobserve(placeholder); @@ -407,7 +420,18 @@ function renderEagerBatch(config: RenderConfig): void { } }); - await renderPageBatch(batch, config.onProgress); + if (tasksToRender.length === 0) { + lazyLoadState.nextEagerIndex = batchEnd; + const remainingBatches = Math.ceil( + (eagerLoadQueue.length - batchEnd) / batchSize + ); + if (remainingBatches > 0 && remainingBatches < eagerLoadBatches) { + renderEagerBatch(config); + } + return; + } + + await renderPageBatch(tasksToRender); if (config.onBatchComplete) { config.onBatchComplete(); @@ -421,7 +445,6 @@ function renderEagerBatch(config: RenderConfig): void { (eagerLoadQueue.length - batchEnd) / batchSize ); if (remainingBatches > 0 && remainingBatches < eagerLoadBatches) { - // Continue eager loading if we have more batches within the eager threshold renderEagerBatch(config); } });