fix: improve error handling, race condition and optimize page rendering logic in pdf multi tool

This commit is contained in:
alam00000
2026-02-25 13:17:30 +05:30
parent 518c18664d
commit 64153e698b
2 changed files with 449 additions and 160 deletions

View File

@@ -1,10 +1,15 @@
import { createIcons, icons } from 'lucide'; 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 * as pdfjsLib from 'pdfjs-dist';
import JSZip from 'jszip'; import JSZip from 'jszip';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { downloadFile, getPDFDocument } from '../utils/helpers'; 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 { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
import { repairPdfFile } from './repair-pdf.js'; import { repairPdfFile } from './repair-pdf.js';
@@ -41,13 +46,17 @@ let sortableInstance: Sortable | null = null;
const pageCanvasCache = new Map<string, HTMLCanvasElement>(); const pageCanvasCache = new Map<string, HTMLCanvasElement>();
type Snapshot = { allPages: PageData[]; selectedPages: number[]; splitMarkers: number[] }; type Snapshot = {
allPages: PageData[];
selectedPages: number[];
splitMarkers: number[];
};
const undoStack: Snapshot[] = []; const undoStack: Snapshot[] = [];
const redoStack: Snapshot[] = []; const redoStack: Snapshot[] = [];
function snapshot() { function snapshot() {
const snap: Snapshot = { const snap: Snapshot = {
allPages: allPages.map(p => ({ ...p, canvas: p.canvas })), allPages: allPages.map((p) => ({ ...p, canvas: p.canvas })),
selectedPages: Array.from(selectedPages), selectedPages: Array.from(selectedPages),
splitMarkers: Array.from(splitMarkers), splitMarkers: Array.from(splitMarkers),
}; };
@@ -56,16 +65,20 @@ function snapshot() {
} }
function restore(snap: Snapshot) { function restore(snap: Snapshot) {
allPages = snap.allPages.map(p => ({ allPages = snap.allPages.map((p) => ({
...p, ...p,
canvas: p.canvas canvas: p.canvas,
})); }));
selectedPages = new Set(snap.selectedPages); selectedPages = new Set(snap.selectedPages);
splitMarkers = new Set(snap.splitMarkers); splitMarkers = new Set(snap.splitMarkers);
updatePageDisplay(); 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 modal = document.getElementById('modal');
const modalTitle = document.getElementById('modal-title'); const modalTitle = document.getElementById('modal-title');
const modalMessage = document.getElementById('modal-message'); const modalMessage = document.getElementById('modal-message');
@@ -79,12 +92,12 @@ function showModal(title: string, message: string, type: 'info' | 'error' | 'suc
const iconMap = { const iconMap = {
info: 'info', info: 'info',
error: 'alert-circle', error: 'alert-circle',
success: 'check-circle' success: 'check-circle',
}; };
const colorMap = { const colorMap = {
info: 'text-blue-400', info: 'text-blue-400',
error: 'text-red-400', error: 'text-red-400',
success: 'text-green-400' success: 'text-green-400',
}; };
modalIcon.innerHTML = `<i data-lucide="${iconMap[type]}" class="w-12 h-12 ${colorMap[type]}"></i>`; modalIcon.innerHTML = `<i data-lucide="${iconMap[type]}" class="w-12 h-12 ${colorMap[type]}"></i>`;
@@ -112,7 +125,10 @@ function showLoading(current: number, total: number) {
text.textContent = t('multiTool.renderingPages'); text.textContent = t('multiTool.renderingPages');
} }
async function withButtonLoading(buttonId: string, action: () => Promise<void>) { async function withButtonLoading(
buttonId: string,
action: () => Promise<void>
) {
const button = document.getElementById(buttonId) as HTMLButtonElement; const button = document.getElementById(buttonId) as HTMLButtonElement;
if (!button) return; if (!button) return;
@@ -122,7 +138,8 @@ async function withButtonLoading(buttonId: string, action: () => Promise<void>)
try { try {
button.disabled = true; button.disabled = true;
button.style.pointerEvents = 'none'; button.style.pointerEvents = 'none';
button.innerHTML = '<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>'; button.innerHTML =
'<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
await action(); await action();
} finally { } finally {
@@ -143,7 +160,9 @@ if (document.readyState === 'loading') {
initializeTool(); initializeTool();
}); });
} else { } else {
console.log('PDF Multi Tool: DOMContentLoaded already fired, initializing immediately'); console.log(
'PDF Multi Tool: DOMContentLoaded already fired, initializing immediately'
);
initializeTool(); initializeTool();
} }
@@ -160,20 +179,30 @@ function initializeTool() {
document.getElementById('upload-pdfs-btn')?.addEventListener('click', () => { document.getElementById('upload-pdfs-btn')?.addEventListener('click', () => {
console.log('Upload button clicked, isRendering:', isRendering); console.log('Upload button clicked, isRendering:', isRendering);
if (isRendering) { if (isRendering) {
showModal(t('multiTool.pleaseWait'), t('multiTool.pagesRendering'), 'info'); showModal(
t('multiTool.pleaseWait'),
t('multiTool.pagesRendering'),
'info'
);
return; return;
} }
document.getElementById('pdf-file-input')?.click(); document.getElementById('pdf-file-input')?.click();
}); });
document.getElementById('pdf-file-input')?.addEventListener('change', handlePdfUpload); document
document.getElementById('insert-pdf-input')?.addEventListener('change', handleInsertPdf); .getElementById('pdf-file-input')
?.addEventListener('change', handlePdfUpload);
document
.getElementById('insert-pdf-input')
?.addEventListener('change', handleInsertPdf);
document.getElementById('bulk-rotate-left-btn')?.addEventListener('click', () => { document
if (isRendering) return; .getElementById('bulk-rotate-left-btn')
snapshot(); ?.addEventListener('click', () => {
bulkRotate(-90); if (isRendering) return;
}); snapshot();
bulkRotate(-90);
});
document.getElementById('bulk-rotate-btn')?.addEventListener('click', () => { document.getElementById('bulk-rotate-btn')?.addEventListener('click', () => {
if (isRendering) return; if (isRendering) return;
snapshot(); snapshot();
@@ -184,27 +213,38 @@ function initializeTool() {
snapshot(); snapshot();
bulkDelete(); bulkDelete();
}); });
document.getElementById('bulk-duplicate-btn')?.addEventListener('click', () => { document
if (isRendering) return; .getElementById('bulk-duplicate-btn')
snapshot(); ?.addEventListener('click', () => {
bulkDuplicate(); if (isRendering) return;
}); snapshot();
bulkDuplicate();
});
document.getElementById('bulk-split-btn')?.addEventListener('click', () => { document.getElementById('bulk-split-btn')?.addEventListener('click', () => {
if (isRendering) return; if (isRendering) return;
snapshot(); snapshot();
bulkSplit(); bulkSplit();
}); });
document.getElementById('bulk-download-btn')?.addEventListener('click', () => { document
if (isRendering) return; .getElementById('bulk-download-btn')
if (isRendering) return; ?.addEventListener('click', () => {
if (selectedPages.size === 0) { if (isRendering) return;
showModal(t('multiTool.noPagesSelected'), t('multiTool.selectOnePage'), 'info'); if (isRendering) return;
return; if (selectedPages.size === 0) {
} showModal(
withButtonLoading('bulk-download-btn', async () => { t('multiTool.noPagesSelected'),
await downloadPagesAsPdf(Array.from(selectedPages).sort((a, b) => a - b), 'selected-pages.pdf'); 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', () => { document.getElementById('select-all-btn')?.addEventListener('click', () => {
if (isRendering) return; if (isRendering) return;
@@ -229,17 +269,19 @@ function initializeTool() {
await downloadAll(); await downloadAll();
}); });
}); });
document.getElementById('add-blank-page-btn')?.addEventListener('click', () => { document
if (isRendering) return; .getElementById('add-blank-page-btn')
snapshot(); ?.addEventListener('click', () => {
addBlankPage(); if (isRendering) return;
}); snapshot();
addBlankPage();
});
document.getElementById('undo-btn')?.addEventListener('click', () => { document.getElementById('undo-btn')?.addEventListener('click', () => {
if (isRendering) return; if (isRendering) return;
const last = undoStack.pop(); const last = undoStack.pop();
if (last) { if (last) {
const current: Snapshot = { const current: Snapshot = {
allPages: allPages.map(p => ({ ...p })), allPages: allPages.map((p) => ({ ...p })),
selectedPages: Array.from(selectedPages), selectedPages: Array.from(selectedPages),
splitMarkers: Array.from(splitMarkers), splitMarkers: Array.from(splitMarkers),
}; };
@@ -252,7 +294,7 @@ function initializeTool() {
const next = redoStack.pop(); const next = redoStack.pop();
if (next) { if (next) {
const current: Snapshot = { const current: Snapshot = {
allPages: allPages.map(p => ({ ...p })), allPages: allPages.map((p) => ({ ...p })),
selectedPages: Array.from(selectedPages), selectedPages: Array.from(selectedPages),
splitMarkers: Array.from(splitMarkers), 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) => { document.getElementById('modal')?.addEventListener('click', (e) => {
if (e.target === document.getElementById('modal')) { if (e.target === document.getElementById('modal')) {
hideModal(); hideModal();
@@ -288,7 +332,9 @@ function initializeTool() {
uploadArea.addEventListener('drop', (e) => { uploadArea.addEventListener('drop', (e) => {
e.preventDefault(); e.preventDefault();
uploadArea.classList.remove('border-indigo-500'); 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) { if (files.length > 0) {
loadPdfs(files); loadPdfs(files);
} }
@@ -361,27 +407,38 @@ async function loadPdfs(files: File[]) {
try { try {
console.log(`Repairing ${file.name}...`); console.log(`Repairing ${file.name}...`);
const loadingText = document.getElementById('loading-text'); 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); const repairedData = await repairPdfFile(file);
if (repairedData) { if (repairedData) {
arrayBuffer = repairedData.buffer as ArrayBuffer; arrayBuffer = repairedData.buffer as ArrayBuffer;
console.log(`Successfully repaired ${file.name} before loading.`); console.log(`Successfully repaired ${file.name} before loading.`);
} else { } 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(); arrayBuffer = await file.arrayBuffer();
} }
} catch (repairError) { } 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(); 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); currentPdfDocs.push(pdfDoc);
const pdfIndex = currentPdfDocs.length - 1; const pdfIndex = currentPdfDocs.length - 1;
const pdfBytes = await pdfDoc.save(); 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 numPages = pdfjsDoc.numPages;
// Pre-fill allPages with placeholders to maintain order/state // Pre-fill allPages with placeholders to maintain order/state
@@ -425,10 +482,13 @@ async function loadPdfs(files: File[]) {
shouldCancel: () => renderCancelled, // Pass cancellation check shouldCancel: () => renderCancelled, // Pass cancellation check
} }
); );
} catch (e) { } catch (e) {
console.error(`Failed to load PDF ${file.name}:`, 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 { } finally {
hideLoading(); hideLoading();
isRendering = false; 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) { if (renderCancelled) {
renderCancelled = false; renderCancelled = false;
} }
@@ -464,7 +526,10 @@ function createPageCard(pageData: PageData, index: number) {
} }
// Modified to return the element instead of appending it // 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]; const pageData = allPages[index];
if (!pageData) { if (!pageData) {
console.error(`Page data not found for index ${index}`); 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'); 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.pageIndex = index.toString();
card.dataset.pageId = pageData.id; // Set ID for reconciliation 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'); 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) { if (canvas) {
const previewCanvas = canvas; const previewCanvas = canvas;
@@ -502,7 +569,8 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM
} else { } else {
// Show loading placeholder if canvas is null // Show loading placeholder if canvas is null
const loading = document.createElement('div'); 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 = ` loading.innerHTML = `
<i data-lucide="loader" class="w-8 h-8 animate-spin mb-2"></i> <i data-lucide="loader" class="w-8 h-8 animate-spin mb-2"></i>
<span class="text-xs">${t('common.loading')}</span> <span class="text-xs">${t('common.loading')}</span>
@@ -518,15 +586,18 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM
// Actions toolbar // Actions toolbar
const actions = document.createElement('div'); 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'); 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); actions.appendChild(actionsInner);
// Select checkbox // Select checkbox
const selectBtn = document.createElement('button'); 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) selectBtn.innerHTML = selectedPages.has(index)
? '<i data-lucide="check-square" class="w-4 h-4 text-indigo-400"></i>' ? '<i data-lucide="check-square" class="w-4 h-4 text-indigo-400"></i>'
: '<i data-lucide="square" class="w-4 h-4 text-gray-200"></i>'; : '<i data-lucide="square" class="w-4 h-4 text-gray-200"></i>';
@@ -538,14 +609,16 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM
// Rotate button // Rotate button
const rotateBtn = document.createElement('button'); const rotateBtn = document.createElement('button');
rotateBtn.className = 'p-1 rounded hover:bg-gray-700'; rotateBtn.className = 'p-1 rounded hover:bg-gray-700';
rotateBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-4 h-4 text-gray-300"></i>'; rotateBtn.innerHTML =
'<i data-lucide="rotate-cw" class="w-4 h-4 text-gray-300"></i>';
rotateBtn.onclick = (e) => { rotateBtn.onclick = (e) => {
e.stopPropagation(); e.stopPropagation();
rotatePage(index, 90); rotatePage(index, 90);
}; };
const rotateLeftBtn = document.createElement('button'); const rotateLeftBtn = document.createElement('button');
rotateLeftBtn.className = 'p-1 rounded hover:bg-gray-700'; rotateLeftBtn.className = 'p-1 rounded hover:bg-gray-700';
rotateLeftBtn.innerHTML = '<i data-lucide="rotate-ccw" class="w-4 h-4 text-gray-300"></i>'; rotateLeftBtn.innerHTML =
'<i data-lucide="rotate-ccw" class="w-4 h-4 text-gray-300"></i>';
rotateLeftBtn.onclick = (e) => { rotateLeftBtn.onclick = (e) => {
e.stopPropagation(); e.stopPropagation();
rotatePage(index, -90); rotatePage(index, -90);
@@ -554,7 +627,8 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM
// Duplicate button // Duplicate button
const duplicateBtn = document.createElement('button'); const duplicateBtn = document.createElement('button');
duplicateBtn.className = 'p-1 rounded hover:bg-gray-700'; duplicateBtn.className = 'p-1 rounded hover:bg-gray-700';
duplicateBtn.innerHTML = '<i data-lucide="copy" class="w-4 h-4 text-gray-300"></i>'; duplicateBtn.innerHTML =
'<i data-lucide="copy" class="w-4 h-4 text-gray-300"></i>';
duplicateBtn.title = t('multiTool.actions.duplicatePage'); duplicateBtn.title = t('multiTool.actions.duplicatePage');
duplicateBtn.onclick = (e) => { duplicateBtn.onclick = (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -565,7 +639,8 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM
// Delete button // Delete button
const deleteBtn = document.createElement('button'); const deleteBtn = document.createElement('button');
deleteBtn.className = 'p-1 rounded hover:bg-gray-700'; deleteBtn.className = 'p-1 rounded hover:bg-gray-700';
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4 text-red-400"></i>'; deleteBtn.innerHTML =
'<i data-lucide="trash-2" class="w-4 h-4 text-red-400"></i>';
deleteBtn.title = t('multiTool.actions.deletePage'); deleteBtn.title = t('multiTool.actions.deletePage');
deleteBtn.onclick = (e) => { deleteBtn.onclick = (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -576,7 +651,8 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM
// Insert PDF button // Insert PDF button
const insertBtn = document.createElement('button'); const insertBtn = document.createElement('button');
insertBtn.className = 'p-1 rounded hover:bg-gray-700'; insertBtn.className = 'p-1 rounded hover:bg-gray-700';
insertBtn.innerHTML = '<i data-lucide="file-plus" class="w-4 h-4 text-gray-300"></i>'; insertBtn.innerHTML =
'<i data-lucide="file-plus" class="w-4 h-4 text-gray-300"></i>';
insertBtn.title = t('multiTool.actions.insertPdf'); insertBtn.title = t('multiTool.actions.insertPdf');
insertBtn.onclick = (e) => { insertBtn.onclick = (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -587,7 +663,8 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM
// Split button // Split button
const splitBtn = document.createElement('button'); const splitBtn = document.createElement('button');
splitBtn.className = 'p-1 rounded hover:bg-gray-700'; splitBtn.className = 'p-1 rounded hover:bg-gray-700';
splitBtn.innerHTML = '<i data-lucide="scissors" class="w-4 h-4 text-gray-300"></i>'; splitBtn.innerHTML =
'<i data-lucide="scissors" class="w-4 h-4 text-gray-300"></i>';
splitBtn.title = t('multiTool.actions.toggleSplit'); splitBtn.title = t('multiTool.actions.toggleSplit');
splitBtn.onclick = (e) => { splitBtn.onclick = (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -596,14 +673,23 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM
renderSplitMarkers(); renderSplitMarkers();
}; };
actionsInner.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn); actionsInner.append(
rotateLeftBtn,
rotateBtn,
duplicateBtn,
insertBtn,
splitBtn,
deleteBtn
);
card.append(preview, info, actions, selectBtn); card.append(preview, info, actions, selectBtn);
// Check for split marker // Check for split marker
if (splitMarkers.has(index)) { if (splitMarkers.has(index)) {
const marker = document.createElement('div'); 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.className =
marker.innerHTML = '<div class="h-full w-0.5 border-l-2 border-dashed border-blue-400"></div>'; 'split-marker absolute -right-3 top-0 bottom-0 w-6 flex items-center justify-center z-20 pointer-events-none';
marker.innerHTML =
'<div class="h-full w-0.5 border-l-2 border-dashed border-blue-400"></div>';
card.appendChild(marker); card.appendChild(marker);
} }
@@ -655,15 +741,19 @@ function toggleSelectOptimized(index: number) {
const card = pagesContainer.children[index] as HTMLElement; const card = pagesContainer.children[index] as HTMLElement;
if (!card) return; 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 (!selectBtn) return;
if (selectedPages.has(index)) { if (selectedPages.has(index)) {
card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500'); card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500');
selectBtn.innerHTML = '<i data-lucide="check-square" class="w-4 h-4 text-indigo-400"></i>'; selectBtn.innerHTML =
'<i data-lucide="check-square" class="w-4 h-4 text-indigo-400"></i>';
} else { } else {
card.classList.remove('border-indigo-500', 'ring-2', 'ring-indigo-500'); card.classList.remove('border-indigo-500', 'ring-2', 'ring-indigo-500');
selectBtn.innerHTML = '<i data-lucide="square" class="w-4 h-4 text-gray-200"></i>'; selectBtn.innerHTML =
'<i data-lucide="square" class="w-4 h-4 text-gray-200"></i>';
} }
createIcons({ icons }); createIcons({ icons });
@@ -730,7 +820,7 @@ function deletePage(index: number) {
allPages.splice(index, 1); allPages.splice(index, 1);
selectedPages.delete(index); selectedPages.delete(index);
const newSelected = new Set<number>(); const newSelected = new Set<number>();
selectedPages.forEach(i => { selectedPages.forEach((i) => {
if (i > index) newSelected.add(i - 1); if (i > index) newSelected.add(i - 1);
else if (i < index) newSelected.add(i); else if (i < index) newSelected.add(i);
}); });
@@ -759,13 +849,17 @@ async function handleInsertPdf(e: Event) {
try { try {
const arrayBuffer = await file.arrayBuffer(); 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); currentPdfDocs.push(pdfDoc);
const pdfIndex = currentPdfDocs.length - 1; const pdfIndex = currentPdfDocs.length - 1;
// Load PDF.js document for rendering // Load PDF.js document for rendering
const pdfBytes = await pdfDoc.save(); 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 numPages = pdfjsDoc.numPages;
const newPages: PageData[] = []; const newPages: PageData[] = [];
@@ -802,13 +896,18 @@ async function handleInsertPdf(e: Event) {
// Update UI if card exists // Update UI if card exists
const pagesContainer = document.getElementById('pages-container'); 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) { 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) { if (preview) {
// Re-create the preview content // Re-create the preview content
preview.innerHTML = ''; 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; const previewCanvas = canvas;
previewCanvas.className = 'max-w-full max-h-full object-contain'; previewCanvas.className = 'max-w-full max-h-full object-contain';
@@ -819,10 +918,13 @@ async function handleInsertPdf(e: Event) {
} }
} }
} }
} catch (e) { } catch (e) {
console.error('Failed to insert PDF:', 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 = ''; input.value = '';
@@ -837,13 +939,15 @@ function renderSplitMarkers() {
const pagesContainer = document.getElementById('pages-container'); const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return; 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) => { Array.from(pagesContainer.children).forEach((cardEl, i) => {
if (splitMarkers.has(i)) { if (splitMarkers.has(i)) {
const marker = document.createElement('div'); 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.className =
marker.innerHTML = '<div class="h-full w-0.5 border-l-2 border-dashed border-blue-400"></div>'; 'split-marker absolute -right-3 top-0 bottom-0 w-6 flex items-center justify-center z-20 pointer-events-none';
marker.innerHTML =
'<div class="h-full w-0.5 border-l-2 border-dashed border-blue-400"></div>';
(cardEl as HTMLElement).appendChild(marker); (cardEl as HTMLElement).appendChild(marker);
} }
}); });
@@ -881,7 +985,7 @@ function bulkRotate(delta: number) {
return; return;
} }
selectedPages.forEach(index => { selectedPages.forEach((index) => {
const pageData = allPages[index]; const pageData = allPages[index];
if (pageData) { if (pageData) {
// Update state // Update state
@@ -890,13 +994,15 @@ function bulkRotate(delta: number) {
// Update DOM immediately if it exists // Update DOM immediately if it exists
const pagesContainer = document.getElementById('pages-container'); 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) { if (card) {
const canvas = card.querySelector('canvas'); const canvas = card.querySelector('canvas');
if (canvas) { if (canvas) {
canvas.style.transform = `rotate(${pageData.visualRotation}deg)`; 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. // When it eventually renders, createPageElement will use the new rotation.
} }
} }
@@ -911,7 +1017,7 @@ function bulkDelete() {
return; return;
} }
const indices = Array.from(selectedPages).sort((a, b) => b - a); 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(); selectedPages.clear();
if (allPages.length === 0) { if (allPages.length === 0) {
@@ -928,7 +1034,7 @@ function bulkDuplicate() {
return; return;
} }
const indices = Array.from(selectedPages).sort((a, b) => b - a); const indices = Array.from(selectedPages).sort((a, b) => b - a);
indices.forEach(index => { indices.forEach((index) => {
duplicatePage(index); duplicatePage(index);
}); });
selectedPages.clear(); selectedPages.clear();
@@ -937,11 +1043,15 @@ function bulkDuplicate() {
function bulkSplit() { function bulkSplit() {
if (selectedPages.size === 0) { 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; return;
} }
const indices = Array.from(selectedPages); const indices = Array.from(selectedPages);
indices.forEach(index => { indices.forEach((index) => {
if (!splitMarkers.has(index)) { if (!splitMarkers.has(index)) {
splitMarkers.add(index); splitMarkers.add(index);
} }
@@ -951,7 +1061,6 @@ function bulkSplit() {
updatePageDisplay(); updatePageDisplay();
} }
async function downloadAll() { async function downloadAll() {
if (allPages.length === 0) { if (allPages.length === 0) {
showModal('No Pages', 'Please upload PDFs first.', 'info'); showModal('No Pages', 'Please upload PDFs first.', 'info');
@@ -993,24 +1102,63 @@ async function downloadSplitPdfs() {
segments.push(currentSegment); segments.push(currentSegment);
} }
// Create PDFs for each segment
for (let segIndex = 0; segIndex < segments.length; segIndex++) { for (let segIndex = 0; segIndex < segments.length; segIndex++) {
const segment = segments[segIndex]; const segment = segments[segIndex];
const newPdf = await PDFLibDocument.create(); const newPdf = await PDFLibDocument.create();
const segSpecs: (
| {
type: 'pdf';
pdfDoc: PDFLibDocument;
originalPageIndex: number;
rotation: number;
}
| { type: 'blank' }
)[] = [];
for (const index of segment) { for (const index of segment) {
const pageData = allPages[index]; const pageData = allPages[index];
if (!pageData) { if (!pageData) continue;
console.warn(`Page data missing for index ${index}`);
continue;
}
if (pageData.pdfDoc && pageData.originalPageIndex >= 0) { if (pageData.pdfDoc && pageData.originalPageIndex >= 0) {
const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]); segSpecs.push({
const page = newPdf.addPage(copiedPage); type: 'pdf',
pdfDoc: pageData.pdfDoc,
originalPageIndex: pageData.originalPageIndex,
rotation: pageData.rotation,
});
} else {
segSpecs.push({ type: 'blank' });
}
}
if (pageData.rotation !== 0) { const docPageIndices = new Map<PDFLibDocument, number[]>();
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<PDFLibDocument, PDFPage[]>();
for (const [doc, pageIdxs] of Array.from(docPageIndices)) {
const copied = await newPdf.copyPages(doc, pageIdxs);
copiedPagesMap.set(doc, copied);
}
const docConsumeIndex = new Map<PDFLibDocument, number>();
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; const currentRotation = page.getRotation().angle;
page.setRotation(degrees(currentRotation + pageData.rotation)); page.setRotation(degrees(currentRotation + spec.rotation));
} }
} else { } else {
newPdf.addPage([595, 842]); newPdf.addPage([595, 842]);
@@ -1025,7 +1173,11 @@ async function downloadSplitPdfs() {
const zipBlob = await zip.generateAsync({ type: 'blob' }); const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'split-documents.zip'); 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) { } catch (e) {
console.error('Failed to create split PDFs:', e); console.error('Failed to create split PDFs:', e);
showModal('Error', 'Failed to create split PDFs.', 'error'); showModal('Error', 'Failed to create split PDFs.', 'error');
@@ -1038,20 +1190,59 @@ async function downloadPagesAsPdf(indices: number[], filename: string) {
try { try {
const newPdf = await PDFLibDocument.create(); const newPdf = await PDFLibDocument.create();
const pageSpecs: (
| {
type: 'pdf';
pdfDoc: PDFLibDocument;
originalPageIndex: number;
rotation: number;
}
| { type: 'blank' }
)[] = [];
for (const index of indices) { for (const index of indices) {
const pageData = allPages[index]; const pageData = allPages[index];
if (!pageData) { if (!pageData) continue;
console.warn(`Page data missing for index ${index}`);
continue;
}
if (pageData.pdfDoc && pageData.originalPageIndex >= 0) { if (pageData.pdfDoc && pageData.originalPageIndex >= 0) {
// Copy page from original PDF pageSpecs.push({
const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]); type: 'pdf',
const page = newPdf.addPage(copiedPage); pdfDoc: pageData.pdfDoc,
originalPageIndex: pageData.originalPageIndex,
rotation: pageData.rotation,
});
} else {
pageSpecs.push({ type: 'blank' });
}
}
if (pageData.rotation !== 0) { const docPageIndices = new Map<PDFLibDocument, number[]>();
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<PDFLibDocument, PDFPage[]>();
for (const [doc, pageIdxs] of Array.from(docPageIndices)) {
const copied = await newPdf.copyPages(doc, pageIdxs);
copiedPagesMap.set(doc, copied);
}
const docConsumeIndex = new Map<PDFLibDocument, number>();
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; const currentRotation = page.getRotation().angle;
page.setRotation(degrees(currentRotation + pageData.rotation)); page.setRotation(degrees(currentRotation + spec.rotation));
} }
} else { } else {
newPdf.addPage([595, 842]); newPdf.addPage([595, 842]);
@@ -1059,7 +1250,9 @@ async function downloadPagesAsPdf(indices: number[], filename: string) {
} }
const pdfBytes = await newPdf.save(); 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); downloadFile(blob, filename);
showModal('Success', 'PDF downloaded successfully.', 'success'); showModal('Success', 'PDF downloaded successfully.', 'success');
@@ -1101,18 +1294,28 @@ function updatePageDisplay() {
// Update index-dependent attributes // Update index-dependent attributes
card.dataset.pageIndex = index.toString(); 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} `; if (info) info.textContent = `Page ${index + 1} `;
// Update selection state // 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 (selectBtn) {
if (selectedPages.has(index)) { if (selectedPages.has(index)) {
card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500'); card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500');
selectBtn.innerHTML = '<i data-lucide="check-square" class="w-4 h-4 text-indigo-400"></i>'; selectBtn.innerHTML =
'<i data-lucide="check-square" class="w-4 h-4 text-indigo-400"></i>';
} else { } else {
card.classList.remove('border-indigo-500', 'ring-2', 'ring-indigo-500'); card.classList.remove(
selectBtn.innerHTML = '<i data-lucide="square" class="w-4 h-4 text-gray-200"></i>'; 'border-indigo-500',
'ring-2',
'ring-indigo-500'
);
selectBtn.innerHTML =
'<i data-lucide="square" class="w-4 h-4 text-gray-200"></i>';
} }
// Update click handler to use new index // Update click handler to use new index
(selectBtn as HTMLElement).onclick = (e) => { (selectBtn as HTMLElement).onclick = (e) => {
@@ -1128,17 +1331,47 @@ function updatePageDisplay() {
} }
// Update action buttons // 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) { if (actionsInner) {
const buttons = actionsInner.querySelectorAll('button'); const buttons = actionsInner.querySelectorAll('button');
if (buttons[0]) (buttons[0] as HTMLElement).onclick = (e) => { e.stopPropagation(); rotatePage(index, -90); }; if (buttons[0])
if (buttons[1]) (buttons[1] as HTMLElement).onclick = (e) => { e.stopPropagation(); rotatePage(index, 90); }; (buttons[0] as HTMLElement).onclick = (e) => {
if (buttons[2]) (buttons[2] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); duplicatePage(index); }; e.stopPropagation();
if (buttons[3]) (buttons[3] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); insertPdfAfter(index); }; rotatePage(index, -90);
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[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 { } else {
// Element doesn't exist, create it // Element doesn't exist, create it
card = createPageElement(pageData.canvas, index); card = createPageElement(pageData.canvas, index);
@@ -1179,7 +1412,9 @@ function updatePageNumbers() {
// We need to find the buttons and update their onclick handlers // We need to find the buttons and update their onclick handlers
// This is necessary because the original handlers captured the old index // 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) { if (selectBtn) {
selectBtn.onclick = (e) => { selectBtn.onclick = (e) => {
e.stopPropagation(); 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) { if (actionsInner) {
const buttons = actionsInner.querySelectorAll('button'); const buttons = actionsInner.querySelectorAll('button');
// Order: Rotate Left, Rotate Right, Duplicate, Insert, Split, Delete // Order: Rotate Left, Rotate Right, Duplicate, Insert, Split, Delete
if (buttons[0]) buttons[0].onclick = (e) => { e.stopPropagation(); rotatePage(index, -90); }; if (buttons[0])
if (buttons[1]) buttons[1].onclick = (e) => { e.stopPropagation(); rotatePage(index, 90); }; buttons[0].onclick = (e) => {
if (buttons[2]) buttons[2].onclick = (e) => { e.stopPropagation(); snapshot(); duplicatePage(index); }; e.stopPropagation();
if (buttons[3]) buttons[3].onclick = (e) => { e.stopPropagation(); snapshot(); insertPdfAfter(index); }; rotatePage(index, -90);
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[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);
};
} }
}); });
} }

View File

@@ -103,7 +103,10 @@ export async function renderPageToCanvas(
canvas.height = viewport.height; canvas.height = viewport.height;
canvas.width = viewport.width; 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({ await page.render({
canvasContext: context, 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( async function renderPageBatch(tasks: PageTask[]): Promise<void> {
tasks: PageTask[],
onProgress?: (current: number, total: number) => void
): Promise<void> {
for (const task of tasks) { for (const task of tasks) {
try { try {
const canvas = await renderPageToCanvas( const canvas = await renderPageToCanvas(
@@ -147,6 +147,13 @@ async function renderPageBatch(
parent.insertBefore(wrapper, placeholder); parent.insertBefore(wrapper, placeholder);
parent.removeChild(placeholder); parent.removeChild(placeholder);
} else { } else {
const existingRendered = task.container.querySelector(
`[data-page-number="${task.pageNumber}"]:not([data-lazy-load="true"])`
);
if (existingRendered) {
continue;
}
const allChildren = Array.from( const allChildren = Array.from(
task.container.children task.container.children
) as HTMLElement[]; ) as HTMLElement[];
@@ -207,7 +214,7 @@ function setupLazyRendering(
task.placeholderElement = placeholder; task.placeholderElement = placeholder;
// Render this page immediately (not waiting for isRendering flag) // Render this page immediately (not waiting for isRendering flag)
renderPageBatch([task], config.onProgress) renderPageBatch([task])
.then(() => { .then(() => {
// Trigger callback after lazy load batch // Trigger callback after lazy load batch
if (config.onBatchComplete) { if (config.onBatchComplete) {
@@ -263,21 +270,19 @@ export async function renderPagesProgressively(
config: RenderConfig = {} config: RenderConfig = {}
): Promise<void> { ): Promise<void> {
const { const {
batchSize = 8, // Increased from 5 to 8 for faster initial render batchSize = 8,
useLazyLoading = true, useLazyLoading = true,
eagerLoadBatches = 2, // Eagerly load 1 batch ahead by default eagerLoadBatches = 2,
onProgress, onProgress,
onBatchComplete, onBatchComplete,
} = config; } = config;
const totalPages = pdfjsDoc.numPages; const totalPages = pdfjsDoc.numPages;
// Render more pages initially to reduce lazy loading issues
const initialRenderCount = useLazyLoading const initialRenderCount = useLazyLoading
? Math.min(20, totalPages) // Increased from 12 to 20 pages ? Math.min(20, totalPages)
: totalPages; : totalPages;
// CRITICAL FIX: Create placeholders for ALL pages first to maintain order
const placeholders: HTMLElement[] = []; const placeholders: HTMLElement[] = [];
for (let i = 1; i <= totalPages; i++) { for (let i = 1; i <= totalPages; i++) {
const placeholder = createPlaceholder(i); const placeholder = createPlaceholder(i);
@@ -293,7 +298,7 @@ export async function renderPagesProgressively(
pageNumber: i, pageNumber: i,
pdfjsDoc, pdfjsDoc,
container, container,
scale: config.useLazyLoading ? 0.3 : 0.5, scale: useLazyLoading ? 0.3 : 0.5,
createWrapper, createWrapper,
placeholderElement: placeholders[i - 1], placeholderElement: placeholders[i - 1],
}); });
@@ -330,19 +335,24 @@ export async function renderPagesProgressively(
const batch = initialTasks.slice(i, i + batchSize); const batch = initialTasks.slice(i, i + batchSize);
await new Promise<void>((resolve) => { await new Promise<void>((resolve, reject) => {
requestIdleCallbackPolyfill(async () => { requestIdleCallbackPolyfill(() => {
await renderPageBatch(batch, onProgress); renderPageBatch(batch)
.then(() => {
if (onProgress) {
onProgress(
Math.min(i + batchSize, initialRenderCount),
totalPages
);
}
if (onProgress) { if (onBatchComplete) {
onProgress(Math.min(i + batchSize, initialRenderCount), totalPages); onBatchComplete();
} }
if (onBatchComplete) { resolve();
onBatchComplete(); })
} .catch(reject);
resolve();
}); });
}); });
} }
@@ -397,8 +407,11 @@ function renderEagerBatch(config: RenderConfig): void {
requestIdleCallbackPolyfill(async () => { requestIdleCallbackPolyfill(async () => {
if (config.shouldCancel?.()) return; if (config.shouldCancel?.()) return;
// Remove these tasks from pending since we're rendering them eagerly const tasksToRender = batch.filter((task) =>
batch.forEach((task) => { lazyLoadState.pendingTasksByPageNumber.has(task.pageNumber)
);
tasksToRender.forEach((task) => {
const placeholder = task.placeholderElement; const placeholder = task.placeholderElement;
if (placeholder && lazyLoadState.observer) { if (placeholder && lazyLoadState.observer) {
lazyLoadState.observer.unobserve(placeholder); 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) { if (config.onBatchComplete) {
config.onBatchComplete(); config.onBatchComplete();
@@ -421,7 +445,6 @@ function renderEagerBatch(config: RenderConfig): void {
(eagerLoadQueue.length - batchEnd) / batchSize (eagerLoadQueue.length - batchEnd) / batchSize
); );
if (remainingBatches > 0 && remainingBatches < eagerLoadBatches) { if (remainingBatches > 0 && remainingBatches < eagerLoadBatches) {
// Continue eager loading if we have more batches within the eager threshold
renderEagerBatch(config); renderEagerBatch(config);
} }
}); });