diff --git a/src/js/config/tools.ts b/src/js/config/tools.ts index 19d16e9..080a4c9 100644 --- a/src/js/config/tools.ts +++ b/src/js/config/tools.ts @@ -176,7 +176,7 @@ export const categories = [ id: 'image-to-pdf', name: 'Image to PDF', icon: 'images', - subtitle: 'Combine various images into one PDF.', + subtitle: 'Convert JPG, PNG, WebP, BMP, TIFF, SVG, HEIC to PDF.', }, { id: 'jpg-to-pdf', diff --git a/src/js/handlers/fileHandler.ts b/src/js/handlers/fileHandler.ts index 4a21fa4..f042b9c 100644 --- a/src/js/handlers/fileHandler.ts +++ b/src/js/handlers/fileHandler.ts @@ -21,6 +21,16 @@ import { } from '../config/pdf-tools.js'; import * as pdfjsLib from 'pdfjs-dist'; +// Global state for rotation tracking (used by Rotate tool) +const rotationState: number[] = []; +let imageSortableInstance: Sortable | null = null; +const activeImageUrls = new Map(); + +// Export getter for rotation state (used by ui.ts) +export function getRotationState(): readonly number[] { + return rotationState; +} + async function handleSinglePdfUpload(toolId, file) { showLoader('Loading PDF...'); try { @@ -101,6 +111,12 @@ async function handleSinglePdfUpload(toolId, file) { await renderPageThumbnails(toolId, state.pdfDoc); if (toolId === 'rotate') { + // Initialize rotation state for all pages + rotationState.length = 0; + for (let i = 0; i < state.pdfDoc.getPageCount(); i++) { + rotationState.push(0); + } + const rotateAllControls = document.getElementById( 'rotate-all-controls' ); @@ -113,11 +129,15 @@ async function handleSinglePdfUpload(toolId, file) { createIcons({ icons }); const rotateAll = (direction) => { + // Update rotation state for ALL pages (including unrendered ones) + for (let i = 0; i < rotationState.length; i++) { + rotationState[i] = (rotationState[i] + direction * 90 + 360) % 360; + } + + // Update DOM for currently rendered pages document.querySelectorAll('.page-rotator-item').forEach((item) => { - const currentRotation = parseInt( - (item as HTMLElement).dataset.rotation || '0' - ); - const newRotation = (currentRotation + direction * 90 + 360) % 360; + const pageIndex = parseInt((item as HTMLElement).dataset.pageIndex || '0'); + const newRotation = rotationState[pageIndex]; (item as HTMLElement).dataset.rotation = newRotation.toString(); const thumbnail = item.querySelector('canvas, img'); if (thumbnail) { @@ -468,6 +488,8 @@ async function handleMultiFileUpload(toolId) { toolId === 'alternate-merge' || toolId === 'reverse-pages' ) { + showLoader('Loading PDF documents...'); + const pdfFilesUnloaded: File[] = []; state.files.forEach((file) => { @@ -502,6 +524,7 @@ async function handleMultiFileUpload(toolId) { const errorMessage = `PDFs found that are password-protected\n\nPlease use the Decrypt or Change Permissions tool on these files first:\n\n${encryptedPDFFileNames.join('\n')}`; + hideLoader(); // Hide loader before showing alert showAlert('Protected PDFs', errorMessage); switchView('grid'); @@ -526,9 +549,27 @@ async function handleMultiFileUpload(toolId) { toolLogic['alternate-merge'].setup(); } else if (toolId === 'image-to-pdf') { const imageList = document.getElementById('image-list'); - imageList.textContent = ''; + + const renderedFiles = new Set( + Array.from(imageList.querySelectorAll('li')).map(li => li.dataset.fileName) + ); + state.files.forEach((file) => { - const url = URL.createObjectURL(file); + if (!file) { + console.error('Invalid file encountered in state.files'); + return; + } + + if (renderedFiles.has(file.name)) { + return; + } + + let url = activeImageUrls.get(file); + if (!url) { + url = URL.createObjectURL(file); + activeImageUrls.set(file, url); + } + const li = document.createElement('li'); li.className = 'relative group cursor-move'; li.dataset.fileName = file.name; @@ -550,10 +591,28 @@ async function handleMultiFileUpload(toolId) { imageList.appendChild(li); }); - Sortable.create(imageList); - // Show image-to-pdf options and wire up slider text + const syncStateWithDOM = () => { + const domOrder = Array.from(imageList.querySelectorAll('li')).map(li => li.dataset.fileName); + state.files.sort((a, b) => { + const aIndex = domOrder.indexOf(a.name); + const bIndex = domOrder.indexOf(b.name); + return aIndex - bIndex; + }); + }; + + if (!imageSortableInstance) { + imageSortableInstance = Sortable.create(imageList, { + animation: 150, + onEnd: () => { + syncStateWithDOM(); + } + }); + } + + syncStateWithDOM(); + const opts = document.getElementById('image-to-pdf-options'); - if (opts) { + if (opts && opts.classList.contains('hidden')) { opts.classList.remove('hidden'); const slider = document.getElementById('image-pdf-quality') as HTMLInputElement; const value = document.getElementById('image-pdf-quality-value'); @@ -617,6 +676,18 @@ export function setupFileInputHandler(toolId) { const processFiles = async (newFiles) => { if (newFiles.length === 0) return; + if (toolId === 'image-to-pdf') { + const validTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/bmp', 'image/tiff']; + const validFiles = newFiles.filter(file => validTypes.includes(file.type)); + + if (validFiles.length < newFiles.length) { + showAlert('Invalid Files', 'Some files were skipped because they are not supported images.'); + } + + newFiles = validFiles; + if (newFiles.length === 0) return; + } + if (!isMultiFileTool || isFirstUpload) { state.files = newFiles; } else { @@ -731,6 +802,9 @@ export function setupFileInputHandler(toolId) { const clearBtn = document.getElementById('clear-files-btn'); if (clearBtn) { clearBtn.addEventListener('click', () => { + activeImageUrls.forEach(url => URL.revokeObjectURL(url)); + activeImageUrls.clear(); + state.files = []; isFirstUpload = true; (fileInput as HTMLInputElement).value = ''; diff --git a/src/js/logic/duplicate-organize.ts b/src/js/logic/duplicate-organize.ts index aa828e3..66d87f9 100644 --- a/src/js/logic/duplicate-organize.ts +++ b/src/js/logic/duplicate-organize.ts @@ -1,6 +1,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile } from '../utils/helpers.js'; import { state } from '../state.js'; +import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js'; import Sortable from 'sortablejs'; import { icons, createIcons } from 'lucide'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; @@ -82,6 +83,9 @@ export async function renderDuplicateOrganizeThumbnails() { const grid = document.getElementById('page-grid'); if (!grid) return; + // Cleanup any previous lazy loading observers + cleanupLazyRendering(); + showLoader('Rendering page previews...'); const pdfData = await state.pdfDoc.save(); // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'. @@ -89,20 +93,13 @@ export async function renderDuplicateOrganizeThumbnails() { grid.textContent = ''; - for (let i = 1; i <= pdfjsDoc.numPages; i++) { - const page = await pdfjsDoc.getPage(i); - const viewport = page.getViewport({ scale: 0.5 }); - const canvas = document.createElement('canvas'); - canvas.height = viewport.height; - canvas.width = viewport.width; - await page.render({ canvasContext: canvas.getContext('2d'), viewport }) - .promise; - + // Function to create wrapper element for each page + const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => { const wrapper = document.createElement('div'); wrapper.className = 'page-thumbnail relative cursor-move flex flex-col items-center gap-2'; // @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'. - wrapper.dataset.originalPageIndex = i - 1; + wrapper.dataset.originalPageIndex = pageNumber - 1; const imgContainer = document.createElement('div'); imgContainer.className = @@ -116,7 +113,7 @@ export async function renderDuplicateOrganizeThumbnails() { const pageNumberSpan = document.createElement('span'); pageNumberSpan.className = 'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1'; - pageNumberSpan.textContent = i.toString(); + pageNumberSpan.textContent = pageNumber.toString(); const controlsDiv = document.createElement('div'); controlsDiv.className = 'flex items-center justify-center gap-4'; @@ -141,13 +138,38 @@ export async function renderDuplicateOrganizeThumbnails() { controlsDiv.append(duplicateBtn, deleteBtn); wrapper.append(imgContainer, pageNumberSpan, controlsDiv); - grid.appendChild(wrapper); - attachEventListeners(wrapper); - } - initializePageGridSortable(); - createIcons({ icons }); - hideLoader(); + attachEventListeners(wrapper); + + return wrapper; + }; + + try { + // Render pages progressively with lazy loading + await renderPagesProgressively( + pdfjsDoc, + grid, + createWrapper, + { + batchSize: 8, + useLazyLoading: true, + lazyLoadMargin: '400px', + onProgress: (current, total) => { + showLoader(`Rendering page previews: ${current}/${total}`); + }, + onBatchComplete: () => { + createIcons({ icons }); + } + } + ); + + initializePageGridSortable(); + } catch (error) { + console.error('Error rendering thumbnails:', error); + showAlert('Error', 'Failed to render page previews'); + } finally { + hideLoader(); + } } export async function processAndSave() { @@ -156,11 +178,28 @@ export async function processAndSave() { const grid = document.getElementById('page-grid'); const finalPageElements = grid.querySelectorAll('.page-thumbnail'); - const finalIndices = Array.from(finalPageElements).map((el) => - parseInt((el as HTMLElement).dataset.originalPageIndex) - ); + const finalIndices = Array.from(finalPageElements) + .map((el) => parseInt((el as HTMLElement).dataset.originalPageIndex || '', 10)) + .filter(index => !isNaN(index) && index >= 0); + + console.log('Saving PDF with indices:', finalIndices); + console.log('Original PDF Page Count:', state.pdfDoc?.getPageCount()); + + if (finalIndices.length === 0) { + showAlert('Error', 'No valid pages to save.'); + return; + } const newPdfDoc = await PDFLibDocument.create(); + + const totalPages = state.pdfDoc.getPageCount(); + const invalidIndices = finalIndices.filter(i => i >= totalPages); + if (invalidIndices.length > 0) { + console.error('Found invalid indices:', invalidIndices); + showAlert('Error', 'Some pages could not be processed. Please try again.'); + return; + } + const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices); copiedPages.forEach((page: any) => newPdfDoc.addPage(page)); @@ -170,8 +209,8 @@ export async function processAndSave() { 'organized.pdf' ); } catch (e) { - console.error(e); - showAlert('Error', 'Failed to save the new PDF.'); + console.error('Save error:', e); + showAlert('Error', 'Failed to save the new PDF. Check console for details.'); } finally { hideLoader(); } diff --git a/src/js/logic/merge.ts b/src/js/logic/merge.ts index 08b9e22..0da2448 100644 --- a/src/js/logic/merge.ts +++ b/src/js/logic/merge.ts @@ -1,7 +1,9 @@ import { showLoader, hideLoader, showAlert } from '../ui.ts'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.ts'; import { state } from '../state.ts'; +import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.ts'; +import { createIcons, icons } from 'lucide'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import * as pdfjsLib from 'pdfjs-dist'; import Sortable from 'sortablejs'; @@ -129,15 +131,53 @@ async function renderPageMergeThumbnails() { mergeState.isRendering = true; container.textContent = ''; - let currentPageNumber = 0; + // Cleanup any previous lazy loading observers + cleanupLazyRendering(); + let totalPages = state.files.reduce((sum, file) => { const pdfDoc = mergeState.pdfDocs[file.name]; return sum + (pdfDoc ? pdfDoc.getPageCount() : 0); }, 0); try { - const thumbnailsHTML = []; + let currentPageNumber = 0; + // Function to create wrapper element for each page + const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number, fileName?: string) => { + const wrapper = document.createElement('div'); + wrapper.className = + 'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors'; + wrapper.dataset.fileName = fileName || ''; + wrapper.dataset.pageIndex = (pageNumber - 1).toString(); + + const imgContainer = document.createElement('div'); + imgContainer.className = 'relative'; + + const img = document.createElement('img'); + img.src = canvas.toDataURL(); + img.className = 'rounded-md shadow-md max-w-full h-auto'; + + const pageNumDiv = document.createElement('div'); + pageNumDiv.className = + 'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg'; + pageNumDiv.textContent = pageNumber.toString(); + + imgContainer.append(img, pageNumDiv); + + const fileNamePara = document.createElement('p'); + fileNamePara.className = + 'text-xs text-gray-400 truncate w-full text-center'; + const fullTitle = fileName ? `${fileName} (page ${pageNumber})` : `Page ${pageNumber}`; + fileNamePara.title = fullTitle; + fileNamePara.textContent = fileName + ? `${fileName.substring(0, 10)}... (p${pageNumber})` + : `Page ${pageNumber}`; + + wrapper.append(imgContainer, fileNamePara); + return wrapper; + }; + + // Render pages from all files progressively for (const file of state.files) { const pdfDoc = mergeState.pdfDocs[file.name]; if (!pdfDoc) continue; @@ -145,55 +185,35 @@ async function renderPageMergeThumbnails() { const pdfData = await pdfDoc.save(); const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise; - for (let i = 1; i <= pdfjsDoc.numPages; i++) { - currentPageNumber++; - showLoader( - `Rendering page previews: ${currentPageNumber}/${totalPages}` - ); - const page = await pdfjsDoc.getPage(i); - const viewport = page.getViewport({ scale: 0.3 }); - const canvas = document.createElement('canvas'); - canvas.height = viewport.height; - canvas.width = viewport.width; - const context = canvas.getContext('2d')!; - await page.render({ - canvasContext: context, - canvas: canvas, - viewport, - }).promise; + // Create a wrapper function that includes the file name + const createWrapperWithFileName = (canvas: HTMLCanvasElement, pageNumber: number) => { + return createWrapper(canvas, pageNumber, file.name); + }; - const wrapper = document.createElement('div'); - wrapper.className = - 'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors'; - wrapper.dataset.fileName = file.name; - wrapper.dataset.pageIndex = (i - 1).toString(); + // Render pages progressively with lazy loading + await renderPagesProgressively( + pdfjsDoc, + container, + createWrapperWithFileName, + { + batchSize: 6, + useLazyLoading: true, + lazyLoadMargin: '300px', + onProgress: (current, total) => { + currentPageNumber++; + showLoader( + `Rendering page previews: ${currentPageNumber}/${totalPages}` + ); + }, + onBatchComplete: () => { + createIcons({ icons }); + } + } + ); - const imgContainer = document.createElement('div'); - imgContainer.className = 'relative'; - - const img = document.createElement('img'); - img.src = canvas.toDataURL(); - img.className = 'rounded-md shadow-md max-w-full h-auto'; - - const pageNumDiv = document.createElement('div'); - pageNumDiv.className = - 'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg'; - pageNumDiv.textContent = i.toString(); - - imgContainer.append(img, pageNumDiv); - - const fileNamePara = document.createElement('p'); - fileNamePara.className = - 'text-xs text-gray-400 truncate w-full text-center'; - const fullTitle = `${file.name} (page ${i})`; - fileNamePara.title = fullTitle; - fileNamePara.textContent = `${file.name.substring(0, 10)}... (p${i})`; - - wrapper.append(imgContainer, fileNamePara); - container.appendChild(wrapper); - } - - pdfjsDoc.destroy(); + // TODO@ALAM - DON'T destroy the PDF.js document here - lazy loading still needs it! + // It will be garbage collected automatically when no longer referenced + // pdfjsDoc.destroy(); // REMOVED } mergeState.cachedThumbnails = true; diff --git a/src/js/logic/pdf-multi-tool.ts b/src/js/logic/pdf-multi-tool.ts index d28c6b4..c92036e 100644 --- a/src/js/logic/pdf-multi-tool.ts +++ b/src/js/logic/pdf-multi-tool.ts @@ -1,12 +1,10 @@ -// @TODO:@ALAM- sometimes I think... and then I forget... -// - import { createIcons, icons } from 'lucide'; import { degrees, PDFDocument as PDFLibDocument } from 'pdf-lib'; import * as pdfjsLib from 'pdfjs-dist'; import JSZip from 'jszip'; import Sortable from 'sortablejs'; import { downloadFile } from '../utils/helpers'; +import { renderPagesProgressively, cleanupLazyRendering, renderPageToCanvas, createPlaceholder } from '../utils/render-utils'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -14,15 +12,20 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( ).toString(); interface PageData { + id: string; // Unique ID for DOM reconciliation pdfIndex: number; pageIndex: number; rotation: number; visualRotation: number; - canvas: HTMLCanvasElement; + canvas: HTMLCanvasElement | null; pdfDoc: PDFLibDocument; originalPageIndex: number; } +function generateId(): string { + return Math.random().toString(36).substr(2, 9) + Date.now().toString(36); +} + let allPages: PageData[] = []; let selectedPages: Set = new Set(); let currentPdfDocs: PDFLibDocument[] = []; @@ -90,6 +93,8 @@ function hideModal() { } function showLoading(current: number, total: number) { + // renderPagesProgressively handles loading UI via onProgress callback + // but we can keep this for compatibility if needed const loader = document.getElementById('loading-overlay'); const progress = document.getElementById('loading-progress'); const text = document.getElementById('loading-text'); @@ -102,16 +107,43 @@ function showLoading(current: number, total: number) { text.textContent = `Rendering pages... ${current} of ${total}`; } +async function withButtonLoading(buttonId: string, action: () => Promise) { + const button = document.getElementById(buttonId) as HTMLButtonElement; + if (!button) return; + + const originalContent = button.innerHTML; + const originalPointerEvents = button.style.pointerEvents; + + try { + button.disabled = true; + button.style.pointerEvents = 'none'; + button.innerHTML = ''; + + await action(); + } finally { + button.disabled = false; + button.style.pointerEvents = originalPointerEvents; + button.innerHTML = originalContent; + } +} + function hideLoading() { const loader = document.getElementById('loading-overlay'); if (loader) loader.classList.add('hidden'); } -document.addEventListener('DOMContentLoaded', () => { +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + console.log('PDF Multi Tool: DOMContentLoaded'); + initializeTool(); + }); +} else { + console.log('PDF Multi Tool: DOMContentLoaded already fired, initializing immediately'); initializeTool(); -}); +} function initializeTool() { + console.log('PDF Multi Tool: Initializing...'); createIcons({ icons }); document.getElementById('close-tool-btn')?.addEventListener('click', () => { @@ -119,6 +151,7 @@ function initializeTool() { }); document.getElementById('upload-pdfs-btn')?.addEventListener('click', () => { + console.log('Upload button clicked, isRendering:', isRendering); if (isRendering) { showModal('Please Wait', 'Pages are still being rendered. Please wait...', 'info'); return; @@ -156,19 +189,24 @@ function initializeTool() { }); document.getElementById('bulk-download-btn')?.addEventListener('click', () => { if (isRendering) return; - bulkDownload(); - }); - document.getElementById('select-all-btn')?.addEventListener('click', () => { - if (isRendering) return; - selectAll(); - }); - document.getElementById('deselect-all-btn')?.addEventListener('click', () => { - if (isRendering) return; - deselectAll(); + if (selectedPages.size === 0) { + showModal('No Pages Selected', 'Please select at least one page to download.', 'info'); + return; + } + withButtonLoading('bulk-download-btn', async () => { + await downloadPagesAsPdf(Array.from(selectedPages).sort((a, b) => a - b), 'selected-pages.pdf'); + }); }); + document.getElementById('export-pdf-btn')?.addEventListener('click', () => { if (isRendering) return; - downloadAll(); + if (allPages.length === 0) { + showModal('No Pages', 'There are no pages to export.', 'info'); + return; + } + withButtonLoading('export-pdf-btn', async () => { + await downloadAll(); + }); }); document.getElementById('add-blank-page-btn')?.addEventListener('click', () => { if (isRendering) return; @@ -240,21 +278,25 @@ function initializeTool() { } function resetAll() { + renderCancelled = true; + isRendering = false; snapshot(); allPages = []; selectedPages.clear(); splitMarkers.clear(); currentPdfDocs = []; pageCanvasCache.clear(); - renderCancelled = false; - isRendering = false; + cleanupLazyRendering(); - // Destroy sortable instance if (sortableInstance) { sortableInstance.destroy(); sortableInstance = null; } + // Force clear DOM to prevent ghost pages + const pagesContainer = document.getElementById('pages-container'); + if (pagesContainer) pagesContainer.innerHTML = ''; + updatePageDisplay(); document.getElementById('upload-area')?.classList.remove('hidden'); } @@ -276,43 +318,79 @@ async function loadPdfs(files: File[]) { const uploadArea = document.getElementById('upload-area'); if (uploadArea) uploadArea.classList.add('hidden'); + const pagesContainer = document.getElementById('pages-container'); + if (!pagesContainer) return; isRendering = true; + console.log('PDF Multi Tool: Starting render, isRendering set to true'); renderCancelled = false; - let totalPages = 0; - let currentPage = 0; + + // Cleanup previous observers + cleanupLazyRendering(); + + showLoading(0, 100); try { - // First pass: count total pages - const pdfDocs: PDFLibDocument[] = []; for (const file of files) { + if (renderCancelled) break; + try { const arrayBuffer = await file.arrayBuffer(); const pdfDoc = await PDFLibDocument.load(arrayBuffer); - pdfDocs.push(pdfDoc); - totalPages += pdfDoc.getPageCount(); + currentPdfDocs.push(pdfDoc); + const pdfIndex = currentPdfDocs.length - 1; + + const pdfBytes = await pdfDoc.save(); + const pdfjsDoc = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise; + const numPages = pdfjsDoc.numPages; + + // Pre-fill allPages with placeholders to maintain order/state + const startIndex = allPages.length; + for (let i = 0; i < numPages; i++) { + allPages.push({ + id: generateId(), + pdfIndex, + pageIndex: i, + rotation: 0, + visualRotation: 0, + canvas: null, // Will be filled when rendered + pdfDoc, + originalPageIndex: i, + }); + } + + await renderPagesProgressively( + pdfjsDoc, + pagesContainer, + (canvas, pageNumber) => { + const globalIndex = startIndex + pageNumber - 1; + + if (allPages[globalIndex]) { + allPages[globalIndex].canvas = canvas; + } + + return createPageElement(canvas, globalIndex); + }, + { + batchSize: 8, + useLazyLoading: true, + lazyLoadMargin: '400px', + onProgress: (current, total) => { + showLoading(current, total); + }, + onBatchComplete: () => { + createIcons({ icons }); + }, + shouldCancel: () => renderCancelled, // Pass cancellation check + } + ); + } catch (e) { console.error(`Failed to load PDF ${file.name}:`, e); showModal('Error', `Failed to load ${file.name}. The file may be corrupted.`, 'error'); } } - // Second pass: render pages - for (const pdfDoc of pdfDocs) { - if (renderCancelled) break; - - currentPdfDocs.push(pdfDoc); - const numPages = pdfDoc.getPageCount(); - - for (let i = 0; i < numPages; i++) { - if (renderCancelled) break; - - currentPage++; - showLoading(currentPage, totalPages); - await renderPage(pdfDoc, i, currentPdfDocs.length - 1); - } - } - if (!renderCancelled) { setupSortable(); createIcons({ icons }); @@ -320,6 +398,7 @@ async function loadPdfs(files: File[]) { } finally { hideLoading(); isRendering = false; + console.log('PDF Multi Tool: Render finished/cancelled, isRendering set to false'); if (renderCancelled) { renderCancelled = false; } @@ -330,79 +409,55 @@ function getCacheKey(pdfIndex: number, pageIndex: number): string { return `${pdfIndex}-${pageIndex}`; } -async function renderPage(pdfDoc: PDFLibDocument, pageIndex: number, pdfIndex: number) { - const pagesContainer = document.getElementById('pages-container'); - if (!pagesContainer) return; - - // Check cache first - const cacheKey = getCacheKey(pdfIndex, pageIndex); - let canvas: HTMLCanvasElement; - - if (pageCanvasCache.has(cacheKey)) { - canvas = pageCanvasCache.get(cacheKey)!; - } else { - const pdfBytes = await pdfDoc.save(); - const pdf = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise; - const page = await pdf.getPage(pageIndex + 1); - - const viewport = page.getViewport({ scale: 0.5, rotation: 0 }); - - canvas = document.createElement('canvas'); - canvas.width = viewport.width; - canvas.height = viewport.height; - const context = canvas.getContext('2d'); - if (!context) return; - - await page.render({ - canvasContext: context, - viewport, - background: 'white', - canvas - }).promise; - - // Cache the canvas - pageCanvasCache.set(cacheKey, canvas); - } - - const pageData: PageData = { - pdfIndex, - pageIndex, - rotation: 0, // Actual rotation to apply when saving PDF - visualRotation: 0, // Visual rotation for display only - canvas, - pdfDoc, - originalPageIndex: pageIndex, - }; - - allPages.push(pageData); - createPageCard(pageData, allPages.length - 1); -} - +// Wrapper for compatibility with updatePageDisplay function createPageCard(pageData: PageData, index: number) { const pagesContainer = document.getElementById('pages-container'); if (!pagesContainer) return; + const card = createPageElement(pageData.canvas, index); + pagesContainer.appendChild(card); + createIcons({ icons }); +} + +// Modified to return the element instead of appending it +function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTMLElement { + const pageData = allPages[index]; + if (!pageData) { + console.error(`Page data not found for index ${index}`); + return 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.dataset.pageIndex = index.toString(); + card.dataset.pageId = pageData.id; // Set ID for reconciliation if (selectedPages.has(index)) { card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500'); } - // Page preview const preview = document.createElement('div'); preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative'; preview.style.minHeight = '160px'; preview.style.height = '250px'; - const previewCanvas = pageData.canvas; - previewCanvas.className = 'max-w-full max-h-full object-contain'; + if (canvas) { + const previewCanvas = canvas; + previewCanvas.className = 'max-w-full max-h-full object-contain'; - // Apply visual rotation using CSS transform - previewCanvas.style.transform = `rotate(${pageData.visualRotation}deg)`; - previewCanvas.style.transition = 'transform 0.2s ease'; - - preview.appendChild(previewCanvas); + previewCanvas.style.transform = `rotate(${pageData.visualRotation}deg)`; + previewCanvas.style.transition = 'transform 0.2s ease'; + preview.appendChild(previewCanvas); + } else { + // Show loading placeholder if canvas is null + const loading = document.createElement('div'); + loading.className = 'flex flex-col items-center justify-center text-gray-400'; + loading.innerHTML = ` + + Loading... + `; + preview.appendChild(loading); + preview.classList.add('bg-gray-700'); // Darker background for loading + } // Page info const info = document.createElement('div'); @@ -491,9 +546,16 @@ function createPageCard(pageData: PageData, index: number) { actionsInner.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn); card.append(preview, info, actions, selectBtn); - pagesContainer.appendChild(card); - createIcons({ icons }); + // Check for split marker + if (splitMarkers.has(index)) { + const marker = document.createElement('div'); + marker.className = 'split-marker absolute -right-3 top-0 bottom-0 w-6 flex items-center justify-center z-20 pointer-events-none'; + marker.innerHTML = '
'; + card.appendChild(marker); + } + + return card; } function setupSortable() { @@ -600,6 +662,7 @@ function duplicatePage(index: number) { const newPageData: PageData = { ...originalPageData, + id: generateId(), // New ID for the duplicate canvas: newCanvas, }; @@ -643,18 +706,66 @@ async function handleInsertPdf(e: Event) { const arrayBuffer = await file.arrayBuffer(); const pdfDoc = await PDFLibDocument.load(arrayBuffer); currentPdfDocs.push(pdfDoc); + const pdfIndex = currentPdfDocs.length - 1; + + // Load PDF.js document for rendering + const pdfBytes = await pdfDoc.save(); + const pdfjsDoc = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise; + const numPages = pdfjsDoc.numPages; - const numPages = pdfDoc.getPageCount(); const newPages: PageData[] = []; for (let i = 0; i < numPages; i++) { - // Use the existing renderPage function, which adds to allPages - await renderPage(pdfDoc, i, currentPdfDocs.length - 1); - // Move the newly added page data to the temporary array - newPages.push(allPages.pop()!); + newPages.push({ + id: generateId(), + pdfIndex, + pageIndex: i, + rotation: 0, + visualRotation: 0, + canvas: null, // Placeholder + pdfDoc, + originalPageIndex: i, + }); } + // Insert new pages into allPages allPages.splice(insertAfterIndex + 1, 0, ...newPages); + + // Update display to show placeholders immediately updatePageDisplay(); + + // Render pages progressively + for (let i = 0; i < numPages; i++) { + const globalIndex = insertAfterIndex + 1 + i; + + // Render page + const canvas = await renderPageToCanvas(pdfjsDoc, i + 1); + + // Update data + if (allPages[globalIndex]) { + allPages[globalIndex].canvas = canvas; + + // Update UI if card exists + const pagesContainer = document.getElementById('pages-container'); + const card = pagesContainer?.querySelector(`div[data-page-index="${globalIndex}"]`); + if (card) { + const preview = card.querySelector('.bg-gray-700') || card.querySelector('.bg-white'); + if (preview) { + // Re-create the preview content + preview.innerHTML = ''; + preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative'; + (preview as HTMLElement).style.minHeight = '160px'; + (preview as HTMLElement).style.height = '250px'; + + const previewCanvas = canvas; + previewCanvas.className = 'max-w-full max-h-full object-contain'; + previewCanvas.style.transform = `rotate(${allPages[globalIndex].visualRotation}deg)`; + previewCanvas.style.transition = 'transform 0.2s ease'; + preview.appendChild(previewCanvas); + } + } + } + } + } catch (e) { console.error('Failed to insert PDF:', e); showModal('Error', 'Failed to insert PDF. The file may be corrupted.', 'error'); @@ -695,6 +806,7 @@ function addBlankPage() { } const blankPageData: PageData = { + id: generateId(), pdfIndex: -1, pageIndex: -1, rotation: 0, @@ -716,11 +828,26 @@ function bulkRotate(delta: number) { selectedPages.forEach(index => { const pageData = allPages[index]; - pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360; - pageData.rotation = (pageData.rotation + delta + 360) % 360; + if (pageData) { + // Update state + pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360; + pageData.rotation = (pageData.rotation + delta + 360) % 360; + + // Update DOM immediately if it exists + const pagesContainer = document.getElementById('pages-container'); + const card = pagesContainer?.querySelector(`div[data-page-index="${index}"]`); + if (card) { + const canvas = card.querySelector('canvas'); + if (canvas) { + canvas.style.transform = `rotate(${pageData.visualRotation}deg)`; + } + // If no canvas (placeholder), the state update is enough. + // When it eventually renders, createPageElement will use the new rotation. + } + } }); - updatePageDisplay(); + // TODO@ALAM - Do NOT call updatePageDisplay() as it destroys lazy loading observers } function bulkDelete() { @@ -769,14 +896,6 @@ function bulkSplit() { updatePageDisplay(); } -async function bulkDownload() { - if (selectedPages.size === 0) { - showModal('No Selection', 'Please select pages to download.', 'info'); - return; - } - const indices = Array.from(selectedPages); - await downloadPagesAsPdf(indices, 'selected-pages.pdf'); -} async function downloadAll() { if (allPages.length === 0) { @@ -826,6 +945,10 @@ async function downloadSplitPdfs() { for (const index of segment) { const pageData = allPages[index]; + if (!pageData) { + console.warn(`Page data missing for index ${index}`); + continue; + } if (pageData.pdfDoc && pageData.originalPageIndex >= 0) { const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]); const page = newPdf.addPage(copiedPage); @@ -840,7 +963,7 @@ async function downloadSplitPdfs() { } const pdfBytes = await newPdf.save(); - zip.file(`document-${segIndex + 1}.pdf`, pdfBytes); + zip.file(`document - ${segIndex + 1}.pdf`, pdfBytes); } // Generate and download ZIP @@ -851,6 +974,8 @@ async function downloadSplitPdfs() { } catch (e) { console.error('Failed to create split PDFs:', e); showModal('Error', 'Failed to create split PDFs.', 'error'); + } finally { + hideLoading(); // Ensure loader is hidden if we used it (though showModal replaces it) } } @@ -860,6 +985,10 @@ async function downloadPagesAsPdf(indices: number[], filename: string) { for (const index of indices) { const pageData = allPages[index]; + if (!pageData) { + console.warn(`Page data missing for index ${index}`); + continue; + } if (pageData.pdfDoc && pageData.originalPageIndex >= 0) { // Copy page from original PDF const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]); @@ -878,6 +1007,7 @@ async function downloadPagesAsPdf(indices: number[], filename: string) { const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }); downloadFile(blob, filename); + showModal('Success', 'PDF downloaded successfully.', 'success'); } catch (e) { console.error('Failed to create PDF:', e); showModal('Error', 'Failed to create PDF.', 'error'); @@ -888,15 +1018,124 @@ function updatePageDisplay() { const pagesContainer = document.getElementById('pages-container'); if (!pagesContainer) return; - pagesContainer.innerHTML = ''; - allPages.forEach((pageData, index) => { - createPageCard(pageData, index); + // 1. Create a map of existing elements by ID + const existingElements = new Map(); + Array.from(pagesContainer.children).forEach((child) => { + const el = child as HTMLElement; + if (el.dataset.pageId) { + existingElements.set(el.dataset.pageId, el); + } }); + + // 2. Iterate through allPages and reconcile DOM + allPages.forEach((pageData, index) => { + let card = existingElements.get(pageData.id); + + if (card) { + // Element exists, update it + existingElements.delete(pageData.id); // Remove from map so we know it's still in use + + // Check if position changed + if (pagesContainer.children[index] !== card) { + if (index < pagesContainer.children.length) { + pagesContainer.insertBefore(card, pagesContainer.children[index]); + } else { + pagesContainer.appendChild(card); + } + } + + // Update index-dependent attributes + card.dataset.pageIndex = index.toString(); + const info = card.querySelector('.text-xs.text-gray-400.text-center.mb-2'); + if (info) info.textContent = `Page ${index + 1} `; + + // Update selection state + const selectBtn = card.querySelector('button[class*="absolute top-2 right-2"]'); + if (selectBtn) { + if (selectedPages.has(index)) { + card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500'); + selectBtn.innerHTML = ''; + } else { + card.classList.remove('border-indigo-500', 'ring-2', 'ring-indigo-500'); + selectBtn.innerHTML = ''; + } + // Update click handler to use new index + (selectBtn as HTMLElement).onclick = (e) => { + e.stopPropagation(); + toggleSelectOptimized(index); + }; + } + + // Update action buttons + const actionsInner = card.querySelector('.flex.items-center.gap-1.bg-gray-900\\/90'); + if (actionsInner) { + const buttons = actionsInner.querySelectorAll('button'); + if (buttons[0]) (buttons[0] as HTMLElement).onclick = (e) => { e.stopPropagation(); rotatePage(index, -90); }; + if (buttons[1]) (buttons[1] as HTMLElement).onclick = (e) => { e.stopPropagation(); rotatePage(index, 90); }; + if (buttons[2]) (buttons[2] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); duplicatePage(index); }; + if (buttons[3]) (buttons[3] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); insertPdfAfter(index); }; + if (buttons[4]) (buttons[4] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); toggleSplitMarker(index); renderSplitMarkers(); }; + if (buttons[5]) (buttons[5] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); deletePage(index); }; + } + + } else { + // Element doesn't exist, create it + card = createPageElement(pageData.canvas, index); + card.dataset.pageId = pageData.id; // IMPORTANT: Set the ID + + if (index < pagesContainer.children.length) { + pagesContainer.insertBefore(card, pagesContainer.children[index]); + } else { + pagesContainer.appendChild(card); + } + } + }); + + // 3. Remove remaining elements (deleted pages) + existingElements.forEach((el) => el.remove()); + setupSortable(); renderSplitMarkers(); createIcons({ icons }); } function updatePageNumbers() { - updatePageDisplay(); + const pagesContainer = document.getElementById('pages-container'); + if (!pagesContainer) return; + + const cards = Array.from(pagesContainer.children) as HTMLElement[]; + cards.forEach((card, index) => { + // Update data attribute + card.dataset.pageIndex = index.toString(); + + // Update visible page number text + const info = card.querySelector('.text-xs.text-gray-400.text-center.mb-2'); + if (info) { + info.textContent = `Page ${index + 1} `; + } + + // Re-attach event listeners for buttons + // We need to find the buttons and update their onclick handlers + // This is necessary because the original handlers captured the old index + + const selectBtn = card.querySelector('button[class*="absolute top-2 right-2"]') as HTMLButtonElement; + if (selectBtn) { + selectBtn.onclick = (e) => { + e.stopPropagation(); + toggleSelectOptimized(index); + }; + } + + const actionsInner = card.querySelector('.flex.items-center.gap-1.bg-gray-900\\/90'); + if (actionsInner) { + const buttons = actionsInner.querySelectorAll('button'); + // Order: Rotate Left, Rotate Right, Duplicate, Insert, Split, Delete + if (buttons[0]) buttons[0].onclick = (e) => { e.stopPropagation(); rotatePage(index, -90); }; + if (buttons[1]) buttons[1].onclick = (e) => { e.stopPropagation(); rotatePage(index, 90); }; + if (buttons[2]) buttons[2].onclick = (e) => { e.stopPropagation(); snapshot(); duplicatePage(index); }; + if (buttons[3]) buttons[3].onclick = (e) => { e.stopPropagation(); snapshot(); insertPdfAfter(index); }; + if (buttons[4]) buttons[4].onclick = (e) => { e.stopPropagation(); snapshot(); toggleSplitMarker(index); renderSplitMarkers(); }; + if (buttons[5]) buttons[5].onclick = (e) => { e.stopPropagation(); snapshot(); deletePage(index); }; + } + }); } \ No newline at end of file diff --git a/src/js/logic/rotate.ts b/src/js/logic/rotate.ts index f2b9787..ba9c10e 100644 --- a/src/js/logic/rotate.ts +++ b/src/js/logic/rotate.ts @@ -1,6 +1,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile } from '../utils/helpers.js'; import { state } from '../state.js'; +import { getRotationState } from '../handlers/fileHandler.js'; import { degrees } from 'pdf-lib'; @@ -8,12 +9,11 @@ export async function rotate() { showLoader('Applying rotations...'); try { const pages = state.pdfDoc.getPages(); - document.querySelectorAll('.page-rotator-item').forEach((item) => { - // @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message - const pageIndex = parseInt(item.dataset.pageIndex); - // @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message - const rotation = parseInt(item.dataset.rotation || '0'); - if (rotation !== 0) { + const rotationStateArray = getRotationState(); + + // Apply rotations from state (not DOM) to ensure all pages including lazy-loaded ones are rotated + rotationStateArray.forEach((rotation, pageIndex) => { + if (rotation !== 0 && pages[pageIndex]) { const currentRotation = pages[pageIndex].getRotation().angle; pages[pageIndex].setRotation(degrees(currentRotation + rotation)); } diff --git a/src/js/logic/split.ts b/src/js/logic/split.ts index f1b2721..4ab3e44 100644 --- a/src/js/logic/split.ts +++ b/src/js/logic/split.ts @@ -1,6 +1,9 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; +import { createIcons, icons } from 'lucide'; +import * as pdfjsLib from 'pdfjs-dist'; import { downloadFile } from '../utils/helpers.js'; import { state } from '../state.js'; +import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js'; import JSZip from 'jszip'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; @@ -17,35 +20,29 @@ async function renderVisualSelector() { container.textContent = ''; + // Cleanup any previous lazy loading observers + cleanupLazyRendering(); + showLoader('Rendering page previews...'); + try { const pdfData = await state.pdfDoc.save(); - // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'. const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise; - for (let i = 1; i <= pdf.numPages; i++) { - const page = await pdf.getPage(i); - const viewport = page.getViewport({ scale: 0.4 }); - const canvas = document.createElement('canvas'); - canvas.height = viewport.height; - canvas.width = viewport.width; - await page.render({ - canvasContext: canvas.getContext('2d'), - viewport: viewport, - }).promise; - + // Function to create wrapper element for each page + const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => { const wrapper = document.createElement('div'); wrapper.className = 'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500'; // @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'. - wrapper.dataset.pageIndex = i - 1; + wrapper.dataset.pageIndex = pageNumber - 1; const img = document.createElement('img'); img.src = canvas.toDataURL(); img.className = 'rounded-md w-full h-auto'; const p = document.createElement('p'); p.className = 'text-center text-xs mt-1 text-gray-300'; - p.textContent = `Page ${i}`; + p.textContent = `Page ${pageNumber}`; wrapper.append(img, p); const handleSelection = (e: any) => { @@ -69,12 +66,31 @@ async function renderVisualSelector() { wrapper.addEventListener('touchstart', (e) => { e.preventDefault(); }); - container.appendChild(wrapper); - } + + return wrapper; + }; + + // Render pages progressively with lazy loading + await renderPagesProgressively( + pdf, + container, + createWrapper, + { + batchSize: 8, + useLazyLoading: true, + lazyLoadMargin: '400px', + onProgress: (current, total) => { + showLoader(`Rendering page previews: ${current}/${total}`); + }, + onBatchComplete: () => { + createIcons({ icons }); + } + } + ); } catch (error) { console.error('Error rendering visual selector:', error); showAlert('Error', 'Failed to render page previews.'); - // 4. ADDED: Reset the flag on error so the user can try again. + // Reset the flag on error so the user can try again. visualSelectorRendered = false; } finally { hideLoader(); diff --git a/src/js/ui.ts b/src/js/ui.ts index 19a475b..437435e 100644 --- a/src/js/ui.ts +++ b/src/js/ui.ts @@ -1,8 +1,10 @@ import { resetState } from './state.js'; import { formatBytes } from './utils/helpers.js'; import { tesseractLanguages } from './config/tesseract-languages.js'; +import { renderPagesProgressively, cleanupLazyRendering } from './utils/render-utils.js'; import { icons, createIcons } from 'lucide'; import Sortable from 'sortablejs'; +import { getRotationState } from './handlers/fileHandler.js'; // Centralizing DOM element selection export const dom = { @@ -111,25 +113,21 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => { if (!container) return; container.innerHTML = ''; + + // Cleanup any previous lazy loading observers + cleanupLazyRendering(); + showLoader('Rendering page previews...'); const pdfData = await pdfDoc.save(); // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'. const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise; - for (let i = 1; i <= pdf.numPages; i++) { - const page = await pdf.getPage(i); - const viewport = page.getViewport({ scale: 0.5 }); - const canvas = document.createElement('canvas'); - canvas.height = viewport.height; - canvas.width = viewport.width; - const context = canvas.getContext('2d'); - await page.render({ canvasContext: context, viewport: viewport }).promise; - + // Function to create wrapper element for each page + const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => { const wrapper = document.createElement('div'); - wrapper.className = 'page-thumbnail relative group'; // @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'. - wrapper.dataset.pageIndex = i - 1; + wrapper.dataset.pageIndex = pageNumber - 1; const imgContainer = document.createElement('div'); imgContainer.className = @@ -148,7 +146,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => { const pageNumSpan = document.createElement('span'); pageNumSpan.className = 'absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1'; - pageNumSpan.textContent = i.toString(); + pageNumSpan.textContent = pageNumber.toString(); const deleteBtn = document.createElement('button'); deleteBtn.className = @@ -156,14 +154,36 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => { deleteBtn.innerHTML = '×'; deleteBtn.addEventListener('click', (e) => { (e.currentTarget as HTMLElement).parentElement.remove(); + + // Renumber remaining pages + const pages = container.querySelectorAll('.page-thumbnail'); + pages.forEach((page, index) => { + const numSpan = page.querySelector('span'); + if (numSpan) { + numSpan.textContent = (index + 1).toString(); + } + }); + initializeOrganizeSortable(containerId); }); wrapper.append(pageNumSpan, deleteBtn); } else if (toolId === 'rotate') { wrapper.className = 'page-rotator-item flex flex-col items-center gap-2'; - wrapper.dataset.rotation = '0'; + + // Read rotation from state (handles "Rotate All" on lazy-loaded pages) + const rotationStateArray = getRotationState(); + const pageIndex = pageNumber - 1; + const initialRotation = rotationStateArray[pageIndex] || 0; + + wrapper.dataset.rotation = initialRotation.toString(); img.classList.add('transition-transform', 'duration-300'); + + // Apply initial rotation if any + if (initialRotation !== 0) { + img.style.transform = `rotate(${initialRotation}deg)`; + } + wrapper.appendChild(imgContainer); const controlsDiv = document.createElement('div'); @@ -171,7 +191,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => { const pageNumSpan = document.createElement('span'); pageNumSpan.className = 'font-medium text-sm text-white'; - pageNumSpan.textContent = i.toString(); + pageNumSpan.textContent = pageNumber.toString(); const rotateBtn = document.createElement('button'); rotateBtn.className = @@ -194,15 +214,40 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => { wrapper.appendChild(controlsDiv); } - container.appendChild(wrapper); + return wrapper; + }; + + try { + // Render pages progressively with lazy loading + await renderPagesProgressively( + pdf, + container, + createWrapper, + { + batchSize: 6, + useLazyLoading: true, + lazyLoadMargin: '300px', + onProgress: (current, total) => { + showLoader(`Rendering page previews: ${current}/${total}`); + }, + onBatchComplete: () => { + createIcons({ icons }); + } + } + ); + + if (toolId === 'organize') { + initializeOrganizeSortable(containerId); + } + + // Reinitialize lucide icons for dynamically added elements createIcons({ icons }); + } catch (error) { + console.error('Error rendering page thumbnails:', error); + showAlert('Error', 'Failed to render page thumbnails'); + } finally { + hideLoader(); } - - if (toolId === 'organize') { - initializeOrganizeSortable(containerId); - } - - hideLoader(); }; /** @@ -987,8 +1032,14 @@ export const toolTemplates = { 'image-to-pdf': () => `

Image to PDF Converter

-

Combine multiple images into a single PDF. Drag and drop to reorder.

- ${createFileInputHTML({ multiple: true, accept: 'image/jpeg,image/png,image/webp', showControls: true })} +

Combine multiple images into a single PDF. Drag and drop to reorder.

+ +
+

Supported Formats:

+

JPG, PNG, WebP, BMP, TIFF, SVG, HEIC/HEIF

+
+ + ${createFileInputHTML({ multiple: true, accept: 'image/jpeg,image/png,image/webp,image/bmp,image/tiff,image/svg+xml', showControls: true })}