fix: improve error handling, race condition and optimize page rendering logic in pdf multi tool
This commit is contained in:
@@ -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,16 +179,26 @@ 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
|
||||||
|
.getElementById('bulk-rotate-left-btn')
|
||||||
|
?.addEventListener('click', () => {
|
||||||
if (isRendering) return;
|
if (isRendering) return;
|
||||||
snapshot();
|
snapshot();
|
||||||
bulkRotate(-90);
|
bulkRotate(-90);
|
||||||
@@ -184,7 +213,9 @@ function initializeTool() {
|
|||||||
snapshot();
|
snapshot();
|
||||||
bulkDelete();
|
bulkDelete();
|
||||||
});
|
});
|
||||||
document.getElementById('bulk-duplicate-btn')?.addEventListener('click', () => {
|
document
|
||||||
|
.getElementById('bulk-duplicate-btn')
|
||||||
|
?.addEventListener('click', () => {
|
||||||
if (isRendering) return;
|
if (isRendering) return;
|
||||||
snapshot();
|
snapshot();
|
||||||
bulkDuplicate();
|
bulkDuplicate();
|
||||||
@@ -194,15 +225,24 @@ function initializeTool() {
|
|||||||
snapshot();
|
snapshot();
|
||||||
bulkSplit();
|
bulkSplit();
|
||||||
});
|
});
|
||||||
document.getElementById('bulk-download-btn')?.addEventListener('click', () => {
|
document
|
||||||
|
.getElementById('bulk-download-btn')
|
||||||
|
?.addEventListener('click', () => {
|
||||||
if (isRendering) return;
|
if (isRendering) return;
|
||||||
if (isRendering) return;
|
if (isRendering) return;
|
||||||
if (selectedPages.size === 0) {
|
if (selectedPages.size === 0) {
|
||||||
showModal(t('multiTool.noPagesSelected'), t('multiTool.selectOnePage'), 'info');
|
showModal(
|
||||||
|
t('multiTool.noPagesSelected'),
|
||||||
|
t('multiTool.selectOnePage'),
|
||||||
|
'info'
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
withButtonLoading('bulk-download-btn', async () => {
|
withButtonLoading('bulk-download-btn', async () => {
|
||||||
await downloadPagesAsPdf(Array.from(selectedPages).sort((a, b) => a - b), 'selected-pages.pdf');
|
await downloadPagesAsPdf(
|
||||||
|
Array.from(selectedPages).sort((a, b) => a - b),
|
||||||
|
'selected-pages.pdf'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -229,7 +269,9 @@ function initializeTool() {
|
|||||||
await downloadAll();
|
await downloadAll();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
document.getElementById('add-blank-page-btn')?.addEventListener('click', () => {
|
document
|
||||||
|
.getElementById('add-blank-page-btn')
|
||||||
|
?.addEventListener('click', () => {
|
||||||
if (isRendering) return;
|
if (isRendering) return;
|
||||||
snapshot();
|
snapshot();
|
||||||
addBlankPage();
|
addBlankPage();
|
||||||
@@ -239,7 +281,7 @@ function initializeTool() {
|
|||||||
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,7 +994,9 @@ 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) {
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -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,12 +335,15 @@ 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) {
|
if (onProgress) {
|
||||||
onProgress(Math.min(i + batchSize, initialRenderCount), totalPages);
|
onProgress(
|
||||||
|
Math.min(i + batchSize, initialRenderCount),
|
||||||
|
totalPages
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onBatchComplete) {
|
if (onBatchComplete) {
|
||||||
@@ -343,6 +351,8 @@ export async function renderPagesProgressively(
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user