import { PDFDocument } from 'pdf-lib'; import * as pdfjsLib from 'pdfjs-dist'; import { createIcons, icons } from 'lucide'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); // State const pageState: { pdfDoc: PDFDocument | null; file: File | null; detectedBlankPages: number[]; pageThumbnails: Map; } = { pdfDoc: null, file: null, detectedBlankPages: [], pageThumbnails: new Map() }; function showLoader(msg = 'Processing...') { document.getElementById('loader-modal')?.classList.remove('hidden'); const txt = document.getElementById('loader-text'); if (txt) txt.textContent = msg; } function hideLoader() { document.getElementById('loader-modal')?.classList.add('hidden'); } function showAlert(title: string, msg: string, type = 'error', cb?: () => void) { const modal = document.getElementById('alert-modal'); const t = document.getElementById('alert-title'); const m = document.getElementById('alert-message'); if (t) t.textContent = title; if (m) m.textContent = msg; modal?.classList.remove('hidden'); const okBtn = document.getElementById('alert-ok'); if (okBtn) { const newBtn = okBtn.cloneNode(true) as HTMLElement; okBtn.replaceWith(newBtn); newBtn.addEventListener('click', () => { modal?.classList.add('hidden'); if (cb) cb(); }); } } function downloadFile(blob: Blob, filename: string) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } function updateFileDisplay() { const area = document.getElementById('file-display-area'); if (!area || !pageState.file || !pageState.pdfDoc) return; const fileSize = pageState.file.size < 1024 * 1024 ? `${(pageState.file.size / 1024).toFixed(1)} KB` : `${(pageState.file.size / 1024 / 1024).toFixed(2)} MB`; const pageCount = pageState.pdfDoc.getPageCount(); area.innerHTML = `

${pageState.file.name}

${fileSize} • ${pageCount} page${pageCount !== 1 ? 's' : ''}

`; createIcons({ icons }); document.getElementById('remove-file')?.addEventListener('click', resetState); } function resetState() { pageState.pdfDoc = null; pageState.file = null; pageState.detectedBlankPages = []; pageState.pageThumbnails.forEach(url => URL.revokeObjectURL(url)); pageState.pageThumbnails.clear(); const area = document.getElementById('file-display-area'); if (area) area.innerHTML = ''; document.getElementById('options-panel')?.classList.add('hidden'); document.getElementById('preview-panel')?.classList.add('hidden'); const inp = document.getElementById('file-input') as HTMLInputElement; if (inp) inp.value = ''; } async function handleFileUpload(file: File) { if (!file || file.type !== 'application/pdf') { showAlert('Error', 'Please upload a valid PDF file.'); return; } showLoader('Loading PDF...'); try { const buf = await file.arrayBuffer(); pageState.pdfDoc = await PDFDocument.load(buf); pageState.file = file; pageState.detectedBlankPages = []; updateFileDisplay(); document.getElementById('options-panel')?.classList.remove('hidden'); document.getElementById('preview-panel')?.classList.add('hidden'); } catch (e) { console.error(e); showAlert('Error', 'Failed to load PDF file.'); } finally { hideLoader(); } } async function isPageBlank(page: any, threshold = 250): Promise { const viewport = page.getViewport({ scale: 0.5 }); // Lower scale for faster processing const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) return false; canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: ctx, viewport }).promise; const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; let totalBrightness = 0; for (let i = 0; i < data.length; i += 4) { const r = data[i], g = data[i + 1], b = data[i + 2]; totalBrightness += (r + g + b) / 3; } const avgBrightness = totalBrightness / (data.length / 4); return avgBrightness > threshold; } async function generateThumbnail(page: any): Promise { const viewport = page.getViewport({ scale: 0.3 }); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) return ''; canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: ctx, viewport }).promise; return canvas.toDataURL('image/jpeg', 0.7); } async function detectBlankPages() { if (!pageState.pdfDoc || !pageState.file) return showAlert('Error', 'Please upload a PDF first.'); const sensitivitySlider = document.getElementById('sensitivity-slider') as HTMLInputElement; const sensitivityPercent = parseInt(sensitivitySlider?.value || '80'); const threshold = Math.round(255 - (sensitivityPercent * 2.55)); showLoader('Detecting blank pages...'); try { const pdfData = await pageState.file.arrayBuffer(); const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise; const totalPages = pdfDoc.numPages; pageState.detectedBlankPages = []; pageState.pageThumbnails.forEach(url => URL.revokeObjectURL(url)); pageState.pageThumbnails.clear(); for (let i = 1; i <= totalPages; i++) { const page = await pdfDoc.getPage(i); if (await isPageBlank(page, threshold)) { pageState.detectedBlankPages.push(i - 1); // 0-indexed const thumbnail = await generateThumbnail(page); pageState.pageThumbnails.set(i - 1, thumbnail); } } if (pageState.detectedBlankPages.length === 0) { showAlert('Info', 'No blank pages detected in this PDF.'); hideLoader(); return; } // Show preview panel updatePreviewPanel(); document.getElementById('preview-panel')?.classList.remove('hidden'); hideLoader(); } catch (e) { console.error(e); showAlert('Error', 'Could not detect blank pages.'); hideLoader(); } } function updatePreviewPanel() { const previewInfo = document.getElementById('preview-info'); const previewContainer = document.getElementById('blank-pages-preview'); if (!previewInfo || !previewContainer) return; previewInfo.textContent = `Found ${pageState.detectedBlankPages.length} blank page(s). Click on a page to deselect it.`; previewContainer.innerHTML = ''; pageState.detectedBlankPages.forEach((pageIndex) => { const thumbnail = pageState.pageThumbnails.get(pageIndex) || ''; const div = document.createElement('div'); div.className = 'relative cursor-pointer group'; div.dataset.pageIndex = String(pageIndex); div.dataset.selected = 'true'; div.innerHTML = `
Page ${pageIndex + 1}
Page ${pageIndex + 1}
`; div.addEventListener('click', () => togglePageSelection(div, pageIndex)); previewContainer.appendChild(div); }); createIcons({ icons }); } function togglePageSelection(div: HTMLElement, pageIndex: number) { const isSelected = div.dataset.selected === 'true'; const border = div.querySelector('.border-2') as HTMLElement; const checkMark = div.querySelector('.check-mark') as HTMLElement; if (isSelected) { div.dataset.selected = 'false'; border?.classList.remove('border-red-500'); border?.classList.add('border-gray-500', 'opacity-50'); checkMark?.classList.add('hidden'); } else { div.dataset.selected = 'true'; border?.classList.add('border-red-500'); border?.classList.remove('border-gray-500', 'opacity-50'); checkMark?.classList.remove('hidden'); } } async function processRemoveBlankPages() { if (!pageState.pdfDoc || !pageState.file) return showAlert('Error', 'Please upload a PDF first.'); // Get selected pages to remove const previewContainer = document.getElementById('blank-pages-preview'); const selectedPages: number[] = []; previewContainer?.querySelectorAll('[data-selected="true"]').forEach(el => { const pageIndex = parseInt((el as HTMLElement).dataset.pageIndex || '-1'); if (pageIndex >= 0) selectedPages.push(pageIndex); }); if (selectedPages.length === 0) { showAlert('Info', 'No pages selected for removal.'); return; } showLoader(`Removing ${selectedPages.length} blank page(s)...`); try { const newPdf = await PDFDocument.create(); const pages = pageState.pdfDoc.getPages(); for (let i = 0; i < pages.length; i++) { if (!selectedPages.includes(i)) { const [copiedPage] = await newPdf.copyPages(pageState.pdfDoc, [i]); newPdf.addPage(copiedPage); } } const newPdfBytes = await newPdf.save(); downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'blank-pages-removed.pdf'); showAlert('Success', `Removed ${selectedPages.length} blank page(s) successfully!`, 'success', resetState); } catch (e) { console.error(e); showAlert('Error', 'Could not remove blank pages.'); } finally { hideLoader(); } } document.addEventListener('DOMContentLoaded', () => { const fileInput = document.getElementById('file-input') as HTMLInputElement; const dropZone = document.getElementById('drop-zone'); const detectBtn = document.getElementById('detect-btn'); const processBtn = document.getElementById('process-btn'); const sensitivitySlider = document.getElementById('sensitivity-slider') as HTMLInputElement; const sensitivityValue = document.getElementById('sensitivity-value'); sensitivitySlider?.addEventListener('input', (e) => { const value = (e.target as HTMLInputElement).value; if (sensitivityValue) sensitivityValue.textContent = value; }); fileInput?.addEventListener('change', (e) => { const f = (e.target as HTMLInputElement).files?.[0]; if (f) handleFileUpload(f); }); dropZone?.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-indigo-500'); }); dropZone?.addEventListener('dragleave', () => { dropZone.classList.remove('border-indigo-500'); }); dropZone?.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('border-indigo-500'); const f = e.dataTransfer?.files[0]; if (f) handleFileUpload(f); }); detectBtn?.addEventListener('click', detectBlankPages); processBtn?.addEventListener('click', processRemoveBlankPages); document.getElementById('back-to-tools')?.addEventListener('click', () => { window.location.href = '../../index.html'; }); });