From 2aaea50031fd9dca7459009ef0d65694879eb541 Mon Sep 17 00:00:00 2001 From: alam00000 Date: Tue, 3 Mar 2026 23:34:55 +0530 Subject: [PATCH] feat: fix bug for remove blank pages tool. added i18n translations --- public/locales/ar/tools.json | 3 +- public/locales/be/tools.json | 3 +- public/locales/da/tools.json | 3 +- public/locales/de/tools.json | 3 +- public/locales/en/tools.json | 3 +- public/locales/es/tools.json | 3 +- public/locales/fr/tools.json | 3 +- public/locales/id/tools.json | 3 +- public/locales/it/tools.json | 3 +- public/locales/nl/tools.json | 3 +- public/locales/pt/tools.json | 3 +- public/locales/sv/tools.json | 3 +- public/locales/tr/tools.json | 3 +- public/locales/vi/tools.json | 3 +- public/locales/zh-TW/tools.json | 3 +- public/locales/zh/tools.json | 3 +- src/js/logic/remove-blank-pages-page.ts | 489 ++++++++++-------- .../workflow/nodes/remove-blank-pages-node.ts | 19 +- src/pages/remove-blank-pages.html | 10 +- 19 files changed, 314 insertions(+), 252 deletions(-) diff --git a/public/locales/ar/tools.json b/public/locales/ar/tools.json index 18a6f04..ae5082c 100644 --- a/public/locales/ar/tools.json +++ b/public/locales/ar/tools.json @@ -163,7 +163,8 @@ }, "removeBlankPages": { "name": "إزالة الصفحات الفارغة", - "subtitle": "اكتشاف وحذف الصفحات الفارغة تلقائيًا." + "subtitle": "اكتشاف وحذف الصفحات الفارغة تلقائيًا.", + "sensitivityHint": "أعلى = أكثر صرامة، فقط الصفحات الفارغة تمامًا. أقل = يسمح بصفحات تحتوي على بعض المحتوى." }, "imageToPdf": { "name": "صور إلى PDF", diff --git a/public/locales/be/tools.json b/public/locales/be/tools.json index 7026407..cd6f23a 100644 --- a/public/locales/be/tools.json +++ b/public/locales/be/tools.json @@ -163,7 +163,8 @@ }, "removeBlankPages": { "name": "Выдаліць пустыя старонкі", - "subtitle": "Аўтаматычна выявіць і выдаліць пустыя старонкі." + "subtitle": "Аўтаматычна выявіць і выдаліць пустыя старонкі.", + "sensitivityHint": "Вышэй = больш строга, толькі цалкам пустыя старонкі. Ніжэй = дапускае старонкі з нейкім змесцівам." }, "imageToPdf": { "name": "Відарысы ў PDF", diff --git a/public/locales/da/tools.json b/public/locales/da/tools.json index ffaa16b..2524b97 100644 --- a/public/locales/da/tools.json +++ b/public/locales/da/tools.json @@ -163,7 +163,8 @@ }, "removeBlankPages": { "name": "Fjern tomme sider", - "subtitle": "Find og fjern automatisk tomme sider." + "subtitle": "Find og fjern automatisk tomme sider.", + "sensitivityHint": "Højere = strengere, kun helt tomme sider. Lavere = tillader sider med noget indhold." }, "imageToPdf": { "name": "Billeder til PDF", diff --git a/public/locales/de/tools.json b/public/locales/de/tools.json index 8c5335f..ea93f3a 100644 --- a/public/locales/de/tools.json +++ b/public/locales/de/tools.json @@ -163,7 +163,8 @@ }, "removeBlankPages": { "name": "Leere Seiten entfernen", - "subtitle": "Leere Seiten automatisch erkennen und löschen." + "subtitle": "Leere Seiten automatisch erkennen und löschen.", + "sensitivityHint": "Höher = strenger, nur rein leere Seiten. Niedriger = erlaubt Seiten mit etwas Inhalt." }, "imageToPdf": { "name": "Bilder zu PDF", diff --git a/public/locales/en/tools.json b/public/locales/en/tools.json index 8c99513..9783bee 100644 --- a/public/locales/en/tools.json +++ b/public/locales/en/tools.json @@ -163,7 +163,8 @@ }, "removeBlankPages": { "name": "Remove Blank Pages", - "subtitle": "Automatically detect and delete blank pages." + "subtitle": "Automatically detect and delete blank pages.", + "sensitivityHint": "Higher = stricter, only purely blank pages. Lower = allows pages with some content." }, "imageToPdf": { "name": "Images to PDF", diff --git a/public/locales/es/tools.json b/public/locales/es/tools.json index be3ec61..1ca9a1f 100644 --- a/public/locales/es/tools.json +++ b/public/locales/es/tools.json @@ -163,7 +163,8 @@ }, "removeBlankPages": { "name": "Eliminar Páginas en Blanco", - "subtitle": "Detecta y elimina automáticamente páginas en blanco." + "subtitle": "Detecta y elimina automáticamente páginas en blanco.", + "sensitivityHint": "Mayor = más estricto, solo páginas completamente en blanco. Menor = permite páginas con algo de contenido." }, "imageToPdf": { "name": "Imágenes a PDF", diff --git a/public/locales/fr/tools.json b/public/locales/fr/tools.json index 8223229..4a208dd 100644 --- a/public/locales/fr/tools.json +++ b/public/locales/fr/tools.json @@ -163,7 +163,8 @@ }, "removeBlankPages": { "name": "Supprimer les pages blanches", - "subtitle": "Détecter et supprimer automatiquement les pages vides." + "subtitle": "Détecter et supprimer automatiquement les pages vides.", + "sensitivityHint": "Plus élevé = plus strict, uniquement les pages vierges. Plus bas = autorise les pages avec du contenu." }, "imageToPdf": { "name": "Images vers PDF", diff --git a/public/locales/id/tools.json b/public/locales/id/tools.json index 96f55a6..b75ea0f 100644 --- a/public/locales/id/tools.json +++ b/public/locales/id/tools.json @@ -163,7 +163,8 @@ }, "removeBlankPages": { "name": "Hapus Halaman Kosong", - "subtitle": "Deteksi dan hapus halaman kosong secara otomatis." + "subtitle": "Deteksi dan hapus halaman kosong secara otomatis.", + "sensitivityHint": "Lebih tinggi = lebih ketat, hanya halaman yang benar-benar kosong. Lebih rendah = mengizinkan halaman dengan sedikit konten." }, "imageToPdf": { "name": "Gambar ke PDF", diff --git a/public/locales/it/tools.json b/public/locales/it/tools.json index 3bb29a1..2736bc5 100644 --- a/public/locales/it/tools.json +++ b/public/locales/it/tools.json @@ -163,7 +163,8 @@ }, "removeBlankPages": { "name": "Rimuovi Pagine Vuote", - "subtitle": "Rileva e elimina automaticamente le pagine vuote." + "subtitle": "Rileva e elimina automaticamente le pagine vuote.", + "sensitivityHint": "Più alto = più rigoroso, solo pagine completamente vuote. Più basso = consente pagine con qualche contenuto." }, "imageToPdf": { "name": "Immagini in PDF", diff --git a/public/locales/nl/tools.json b/public/locales/nl/tools.json index 76cce16..c50eeca 100644 --- a/public/locales/nl/tools.json +++ b/public/locales/nl/tools.json @@ -163,7 +163,8 @@ }, "removeBlankPages": { "name": "Blanco pagina's Verwijderen", - "subtitle": "Automatisch blanco pagina's detecteren en verwijderen." + "subtitle": "Automatisch blanco pagina's detecteren en verwijderen.", + "sensitivityHint": "Hoger = strenger, alleen volledig lege pagina's. Lager = staat pagina's met enige inhoud toe." }, "imageToPdf": { "name": "Afbeelding naar PDF", diff --git a/public/locales/pt/tools.json b/public/locales/pt/tools.json index 05d96cd..1a16138 100644 --- a/public/locales/pt/tools.json +++ b/public/locales/pt/tools.json @@ -163,7 +163,8 @@ }, "removeBlankPages": { "name": "Remover Páginas em Branco", - "subtitle": "Detecte e exclua automaticamente páginas em branco." + "subtitle": "Detecte e exclua automaticamente páginas em branco.", + "sensitivityHint": "Maior = mais rigoroso, apenas páginas totalmente em branco. Menor = permite páginas com algum conteúdo." }, "imageToPdf": { "name": "Imagem para PDF", diff --git a/public/locales/sv/tools.json b/public/locales/sv/tools.json index aa3507f..d35aa9e 100644 --- a/public/locales/sv/tools.json +++ b/public/locales/sv/tools.json @@ -163,7 +163,8 @@ }, "removeBlankPages": { "name": "Ta bort tomma sidor", - "subtitle": "Automatiskt identifiera och ta bort tomma sidor." + "subtitle": "Automatiskt identifiera och ta bort tomma sidor.", + "sensitivityHint": "Högre = striktare, bara helt tomma sidor. Lägre = tillåter sidor med visst innehåll." }, "imageToPdf": { "name": "Bilder till PDF", diff --git a/public/locales/tr/tools.json b/public/locales/tr/tools.json index 2231fec..35a207a 100644 --- a/public/locales/tr/tools.json +++ b/public/locales/tr/tools.json @@ -163,7 +163,8 @@ }, "removeBlankPages": { "name": "Boş Sayfaları Kaldır", - "subtitle": "Boş sayfaları otomatik olarak tespit edin ve silin." + "subtitle": "Boş sayfaları otomatik olarak tespit edin ve silin.", + "sensitivityHint": "Yüksek = daha katı, yalnızca tamamen boş sayfalar. Düşük = biraz içerik olan sayfalara izin verir." }, "imageToPdf": { "name": "Görselden PDF'ye", diff --git a/public/locales/vi/tools.json b/public/locales/vi/tools.json index c415c69..fcdd960 100644 --- a/public/locales/vi/tools.json +++ b/public/locales/vi/tools.json @@ -163,7 +163,8 @@ }, "removeBlankPages": { "name": "Xóa trang trống", - "subtitle": "Tự động phát hiện và xóa trang trống." + "subtitle": "Tự động phát hiện và xóa trang trống.", + "sensitivityHint": "Cao hơn = nghiêm ngặt hơn, chỉ các trang hoàn toàn trống. Thấp hơn = cho phép trang có một số nội dung." }, "imageToPdf": { "name": "Hình ảnh sang PDF", diff --git a/public/locales/zh-TW/tools.json b/public/locales/zh-TW/tools.json index 6cc87e5..b129178 100644 --- a/public/locales/zh-TW/tools.json +++ b/public/locales/zh-TW/tools.json @@ -163,7 +163,8 @@ }, "removeBlankPages": { "name": "移除空白頁面", - "subtitle": "自動偵測並刪除空白頁面。" + "subtitle": "自動偵測並刪除空白頁面。", + "sensitivityHint": "越高 = 越嚴格,僅偵測純空白頁面。越低 = 允許包含少量內容的頁面。" }, "imageToPdf": { "name": "圖片轉 PDF", diff --git a/public/locales/zh/tools.json b/public/locales/zh/tools.json index fd04ada..d867a35 100644 --- a/public/locales/zh/tools.json +++ b/public/locales/zh/tools.json @@ -163,7 +163,8 @@ }, "removeBlankPages": { "name": "移除空白页", - "subtitle": "自动检测并删除空白页。" + "subtitle": "自动检测并删除空白页。", + "sensitivityHint": "越高 = 越严格,仅检测纯空白页。越低 = 允许包含少量内容的页面。" }, "imageToPdf": { "name": "图片转 PDF", diff --git a/src/js/logic/remove-blank-pages-page.ts b/src/js/logic/remove-blank-pages-page.ts index f320e63..d18dede 100644 --- a/src/js/logic/remove-blank-pages-page.ts +++ b/src/js/logic/remove-blank-pages-page.ts @@ -2,64 +2,77 @@ 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(); +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: PDFDocument | null; + file: File | null; + detectedBlankPages: number[]; + pageThumbnails: Map; } = { - pdfDoc: null, - file: null, - detectedBlankPages: [], - pageThumbnails: new 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; + 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 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 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); + 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 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(); + 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 = ` + area.innerHTML = `
@@ -72,144 +85,157 @@ function updateFileDisplay() {
`; - createIcons({ icons }); - document.getElementById('remove-file')?.addEventListener('click', resetState); + 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(); + 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 = ''; + 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 = ''; + const slider = document.getElementById( + 'sensitivity-slider' + ) as HTMLInputElement; + if (slider) slider.value = '80'; + const sliderLabel = document.getElementById('sensitivity-value'); + if (sliderLabel) sliderLabel.textContent = '80'; } 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(); - } + 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; +async function isPageBlank( + page: any, + maxNonWhitePercent = 0.5 +): Promise { + const viewport = page.getViewport({ scale: 0.5 }); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) return false; - canvas.width = viewport.width; - canvas.height = viewport.height; + canvas.width = viewport.width; + canvas.height = viewport.height; - await page.render({ canvasContext: ctx, viewport }).promise; + await page.render({ canvasContext: ctx, viewport }).promise; - const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - const data = imageData.data; + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + const totalPixels = data.length / 4; - 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; - } + let nonWhitePixels = 0; + for (let i = 0; i < data.length; i += 4) { + const brightness = (data[i] + data[i + 1] + data[i + 2]) / 3; + if (brightness < 240) nonWhitePixels++; + } - const avgBrightness = totalBrightness / (data.length / 4); - return avgBrightness > threshold; + const nonWhitePercent = (nonWhitePixels / totalPixels) * 100; + return nonWhitePercent <= maxNonWhitePercent; } 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 ''; + const viewport = page.getViewport({ scale: 1.5 }); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) return ''; - canvas.width = viewport.width; - canvas.height = viewport.height; + canvas.width = viewport.width; + canvas.height = viewport.height; - await page.render({ canvasContext: ctx, viewport }).promise; - return canvas.toDataURL('image/jpeg', 0.7); + 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.'); + 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)); + const sensitivitySlider = document.getElementById( + 'sensitivity-slider' + ) as HTMLInputElement; + const sensitivityPercent = parseInt(sensitivitySlider?.value || '80'); + const maxNonWhitePercent = 5 - (sensitivityPercent / 100) * 4.9; - showLoader('Detecting blank pages...'); - try { - const pdfData = await pageState.file.arrayBuffer(); - const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise; - const totalPages = pdfDoc.numPages; + 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(); + 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(); + for (let i = 1; i <= totalPages; i++) { + const page = await pdfDoc.getPage(i); + if (await isPageBlank(page, maxNonWhitePercent)) { + 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'); + const previewInfo = document.getElementById('preview-info'); + const previewContainer = document.getElementById('blank-pages-preview'); - if (!previewInfo || !previewContainer) return; + if (!previewInfo || !previewContainer) return; - previewInfo.textContent = `Found ${pageState.detectedBlankPages.length} blank page(s). Click on a page to deselect it.`; - previewContainer.innerHTML = ''; + 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'; + 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 = ` + div.innerHTML = `
Page ${pageIndex + 1}
@@ -221,108 +247,119 @@ function updatePreviewPanel() {
`; - div.addEventListener('click', () => togglePageSelection(div, pageIndex)); - previewContainer.appendChild(div); - }); + div.addEventListener('click', () => togglePageSelection(div, pageIndex)); + previewContainer.appendChild(div); + }); - createIcons({ icons }); + 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; + 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'); - } + 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.'); + 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); - }); + // 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; + 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); + } } - 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(); - } + 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'); + 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; - }); + 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); - }); + 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('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('border-indigo-500'); + }); - dropZone?.addEventListener('dragleave', () => { - dropZone.classList.remove('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); - }); + 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); + detectBtn?.addEventListener('click', detectBlankPages); + processBtn?.addEventListener('click', processRemoveBlankPages); - document.getElementById('back-to-tools')?.addEventListener('click', () => { - window.location.href = '../../index.html'; - }); + document.getElementById('back-to-tools')?.addEventListener('click', () => { + window.location.href = '../../index.html'; + }); }); diff --git a/src/js/workflow/nodes/remove-blank-pages-node.ts b/src/js/workflow/nodes/remove-blank-pages-node.ts index 902395e..692c9a9 100644 --- a/src/js/workflow/nodes/remove-blank-pages-node.ts +++ b/src/js/workflow/nodes/remove-blank-pages-node.ts @@ -23,7 +23,7 @@ export class RemoveBlankPagesNode extends BaseWorkflowNode { private async isPageBlank( page: pdfjsLib.PDFPageProxy, - threshold: number + maxNonWhitePercent: number ): Promise { const viewport = page.getViewport({ scale: 0.5 }); const canvas = document.createElement('canvas'); @@ -34,12 +34,14 @@ export class RemoveBlankPagesNode extends BaseWorkflowNode { const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; - let totalBrightness = 0; + const totalPixels = data.length / 4; + let nonWhitePixels = 0; for (let i = 0; i < data.length; i += 4) { - totalBrightness += (data[i] + data[i + 1] + data[i + 2]) / 3; + const brightness = (data[i] + data[i + 1] + data[i + 2]) / 3; + if (brightness < 240) nonWhitePixels++; } - const avgBrightness = totalBrightness / (data.length / 4); - return avgBrightness > threshold; + const nonWhitePercent = (nonWhitePixels / totalPixels) * 100; + return nonWhitePercent <= maxNonWhitePercent; } async data( @@ -50,7 +52,10 @@ export class RemoveBlankPagesNode extends BaseWorkflowNode { const threshCtrl = this.controls['threshold'] as | ClassicPreset.InputControl<'number'> | undefined; - const threshold = Math.max(200, Math.min(255, threshCtrl?.value ?? 250)); + const maxNonWhitePercent = Math.max( + 0.1, + Math.min(5, threshCtrl?.value ?? 0.5) + ); return { pdf: await processBatch(pdfInputs, async (input) => { @@ -61,7 +66,7 @@ export class RemoveBlankPagesNode extends BaseWorkflowNode { for (let i = 1; i <= pdfjsDoc.numPages; i++) { const page = await pdfjsDoc.getPage(i); - const blank = await this.isPageBlank(page, threshold); + const blank = await this.isPageBlank(page, maxNonWhitePercent); if (!blank) { nonBlankIndices.push(i - 1); } else { diff --git a/src/pages/remove-blank-pages.html b/src/pages/remove-blank-pages.html index b650a01..6f5aa61 100644 --- a/src/pages/remove-blank-pages.html +++ b/src/pages/remove-blank-pages.html @@ -193,8 +193,12 @@ step="5" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" /> -

- Higher values detect more pages as blank +

+ Higher = stricter, only purely blank pages. Lower = allows pages + with some content.