feat: fix bug for remove blank pages tool. added i18n translations
This commit is contained in:
@@ -163,7 +163,8 @@
|
||||
},
|
||||
"removeBlankPages": {
|
||||
"name": "إزالة الصفحات الفارغة",
|
||||
"subtitle": "اكتشاف وحذف الصفحات الفارغة تلقائيًا."
|
||||
"subtitle": "اكتشاف وحذف الصفحات الفارغة تلقائيًا.",
|
||||
"sensitivityHint": "أعلى = أكثر صرامة، فقط الصفحات الفارغة تمامًا. أقل = يسمح بصفحات تحتوي على بعض المحتوى."
|
||||
},
|
||||
"imageToPdf": {
|
||||
"name": "صور إلى PDF",
|
||||
|
||||
@@ -163,7 +163,8 @@
|
||||
},
|
||||
"removeBlankPages": {
|
||||
"name": "Выдаліць пустыя старонкі",
|
||||
"subtitle": "Аўтаматычна выявіць і выдаліць пустыя старонкі."
|
||||
"subtitle": "Аўтаматычна выявіць і выдаліць пустыя старонкі.",
|
||||
"sensitivityHint": "Вышэй = больш строга, толькі цалкам пустыя старонкі. Ніжэй = дапускае старонкі з нейкім змесцівам."
|
||||
},
|
||||
"imageToPdf": {
|
||||
"name": "Відарысы ў PDF",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -163,7 +163,8 @@
|
||||
},
|
||||
"removeBlankPages": {
|
||||
"name": "移除空白頁面",
|
||||
"subtitle": "自動偵測並刪除空白頁面。"
|
||||
"subtitle": "自動偵測並刪除空白頁面。",
|
||||
"sensitivityHint": "越高 = 越嚴格,僅偵測純空白頁面。越低 = 允許包含少量內容的頁面。"
|
||||
},
|
||||
"imageToPdf": {
|
||||
"name": "圖片轉 PDF",
|
||||
|
||||
@@ -163,7 +163,8 @@
|
||||
},
|
||||
"removeBlankPages": {
|
||||
"name": "移除空白页",
|
||||
"subtitle": "自动检测并删除空白页。"
|
||||
"subtitle": "自动检测并删除空白页。",
|
||||
"sensitivityHint": "越高 = 越严格,仅检测纯空白页。越低 = 允许包含少量内容的页面。"
|
||||
},
|
||||
"imageToPdf": {
|
||||
"name": "图片转 PDF",
|
||||
|
||||
@@ -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<number, string>;
|
||||
pdfDoc: PDFDocument | null;
|
||||
file: File | null;
|
||||
detectedBlankPages: number[];
|
||||
pageThumbnails: Map<number, string>;
|
||||
} = {
|
||||
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 = `
|
||||
<div class="bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 min-w-0">
|
||||
@@ -72,144 +85,157 @@ function updateFileDisplay() {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<string> {
|
||||
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 = `
|
||||
<div class="relative border-2 border-red-500 rounded-lg overflow-hidden transition-all">
|
||||
<img src="${thumbnail}" alt="Page ${pageIndex + 1}" class="w-full h-auto">
|
||||
<div class="absolute bottom-0 left-0 right-0 bg-black/70 text-white text-xs text-center py-1">
|
||||
@@ -221,108 +247,119 @@ function updatePreviewPanel() {
|
||||
</div>
|
||||
`;
|
||||
|
||||
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';
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ export class RemoveBlankPagesNode extends BaseWorkflowNode {
|
||||
|
||||
private async isPageBlank(
|
||||
page: pdfjsLib.PDFPageProxy,
|
||||
threshold: number
|
||||
maxNonWhitePercent: number
|
||||
): Promise<boolean> {
|
||||
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 {
|
||||
|
||||
@@ -193,8 +193,12 @@
|
||||
step="5"
|
||||
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"
|
||||
/>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
Higher values detect more pages as blank
|
||||
<p
|
||||
class="text-xs text-gray-400 mt-1"
|
||||
data-i18n="tools:removeBlankPages.sensitivityHint"
|
||||
>
|
||||
Higher = stricter, only purely blank pages. Lower = allows pages
|
||||
with some content.
|
||||
</p>
|
||||
</div>
|
||||
<button id="detect-btn" class="btn-gradient w-full mt-4">
|
||||
@@ -210,7 +214,7 @@
|
||||
<p id="preview-info" class="text-gray-400 text-sm mb-4"></p>
|
||||
<div
|
||||
id="blank-pages-preview"
|
||||
class="grid grid-cols-3 md:grid-cols-4 gap-3 max-h-64 overflow-y-auto p-2 bg-gray-900 rounded-lg border border-gray-700"
|
||||
class="grid grid-cols-3 md:grid-cols-4 gap-3 p-2 bg-gray-900 rounded-lg border border-gray-700"
|
||||
></div>
|
||||
<button id="process-btn" class="btn-gradient w-full mt-4">
|
||||
Remove Selected Blank Pages
|
||||
|
||||
Reference in New Issue
Block a user