From a8c3f9ffdd55ae9514b88d6087d579ec9e3a1316 Mon Sep 17 00:00:00 2001 From: Divy Sangwan Date: Wed, 22 Oct 2025 23:06:59 +0530 Subject: [PATCH] Feat : Allow bulk pdf compress (#67) * Feat : Allow bulk pdf compress * feat(pdf-tools): add sanitize-pdf tool for comprehensive document cleaning Implement a new PDF sanitization tool that allows users to remove various potentially sensitive elements from PDFs including metadata, annotations, JavaScript, embedded files, and more. The tool provides configurable options through checkboxes to selectively remove different types of content while preserving the core document structure. Extract reusable utility functions from existing tools (remove-metadata, remove-annotations, flatten) to support the new sanitization feature. The tool handles edge cases gracefully and provides feedback when no changes are made. * fix(sanitize-pdf): remove javascript actions, external links and font files - Remove JavaScript actions from annotations and form fields - Remove external links (URI, Launch, GoTo) from annotations - Remove embedded font files while preserving font descriptors - Add detailed error logging for each operation * fix(sanitize-pdf): improve link removal and add named destinations cleanup - Refactor link annotation removal logic to handle more action types (URI, Launch, GoTo, GoToR) - Add cleanup of named destinations in catalog and names dictionary - Improve error handling and logging throughout the sanitization process * chore: update config files, icons and readme - Update FUNDING.yml formatting - Remove debug log in sanitize-pdf.ts - Adjust test expectation for tool count - Standardize dependabot.yml quotes - Update tool icons in config - Expand and reorganize README features list - Fix markdown formatting in README * Release v1.1.0 --------- Co-authored-by: abdullahalam123 Co-authored-by: Alam <50314772+alam00000@users.noreply.github.com> --- src/js/config/pdf-tools.ts | 2 +- src/js/handlers/fileHandler.ts | 13 +- src/js/logic/compress.ts | 125 +++++++++++-------- src/js/ui.ts | 2 +- src/tests/bulk-compress-pdfs.test.ts | 172 +++++++++++++++++++++++++++ src/tests/pdf-tools.test.ts | 2 +- 6 files changed, 255 insertions(+), 61 deletions(-) create mode 100644 src/tests/bulk-compress-pdfs.test.ts diff --git a/src/js/config/pdf-tools.ts b/src/js/config/pdf-tools.ts index 56d0cd32..c45b2ae0 100644 --- a/src/js/config/pdf-tools.ts +++ b/src/js/config/pdf-tools.ts @@ -6,7 +6,6 @@ export const singlePdfLoadTools = [ 'pdf-to-jpg', 'pdf-to-png', 'pdf-to-webp', - 'compress', 'pdf-to-greyscale', 'edit-metadata', 'remove-metadata', @@ -63,4 +62,5 @@ export const multiFileTools = [ 'alternate-merge', 'linearize', 'reverse-pages', + 'compress' ]; diff --git a/src/js/handlers/fileHandler.ts b/src/js/handlers/fileHandler.ts index 3c044137..cde50ee5 100644 --- a/src/js/handlers/fileHandler.ts +++ b/src/js/handlers/fileHandler.ts @@ -440,11 +440,7 @@ async function handleSinglePdfUpload(toolId, file) { } async function handleMultiFileUpload(toolId) { - if ( - toolId === 'merge' || - toolId === 'alternate-merge' || - toolId === 'reverse-pages' - ) { + if (toolId === 'merge' || toolId === 'alternate-merge' || toolId === 'reverse-pages' || toolId === 'compress') { const pdfFilesUnloaded: File[] = []; state.files.forEach((file) => { @@ -496,7 +492,12 @@ async function handleMultiFileUpload(toolId) { processBtn.onclick = func; } } - + if (toolId === 'compress') { + const optionsDiv = document.getElementById('compress-options'); + if (optionsDiv) { + optionsDiv.classList.remove('hidden'); + } + } if (toolId === 'merge') { toolLogic.merge.setup(); } else if (toolId === 'alternate-merge') { diff --git a/src/js/logic/compress.ts b/src/js/logic/compress.ts index 7db64b9b..7fdf3958 100644 --- a/src/js/logic/compress.ts +++ b/src/js/logic/compress.ts @@ -6,7 +6,7 @@ import { } from '../utils/helpers.js'; import { state } from '../state.js'; import * as pdfjsLib from 'pdfjs-dist'; - +import JSZip from 'jszip'; import { PDFDocument, PDFName, PDFDict, PDFStream, PDFNumber } from 'pdf-lib'; function dataUrlToBytes(dataUrl: any) { @@ -270,68 +270,89 @@ export async function compress() { const smartSettings = { ...settings[level].smart, removeMetadata: true }; const legacySettings = settings[level].legacy; + const pdfFiles = state.files.filter((f) => f.type === 'application/pdf'); + if (!pdfFiles.length) { + showAlert('Error', 'No PDF files found to compress.'); + return; + } + showLoader(`Compressing ${pdfFiles.length} PDF(s)...`); + + const zip = new JSZip(); + let totalOriginal = 0; + let totalCompressed = 0; try { - const originalFile = state.files[0]; - const arrayBuffer = await readFileAsArrayBuffer(originalFile); + for (let i = 0; i < pdfFiles.length; i++) { + const originalFile = pdfFiles[i]; + const arrayBuffer = await readFileAsArrayBuffer(originalFile); - let resultBytes; - let usedMethod; + let resultBytes; + let usedMethod; - if (algorithm === 'vector') { - showLoader('Running Vector (Smart) compression...'); - resultBytes = await performSmartCompression(arrayBuffer, smartSettings); - usedMethod = 'Vector'; - } else if (algorithm === 'photon') { - showLoader('Running Photon (Rasterize) compression...'); - resultBytes = await performLegacyCompression(arrayBuffer, legacySettings); - usedMethod = 'Photon'; - } else { - showLoader('Running Automatic (Vector first)...'); - const vectorResultBytes = await performSmartCompression( - arrayBuffer, - smartSettings - ); - - if (vectorResultBytes.length < originalFile.size) { - resultBytes = vectorResultBytes; - usedMethod = 'Vector (Automatic)'; + if (algorithm === 'vector') { + showLoader('Running Vector (Smart) compression...'); + resultBytes = await performSmartCompression(arrayBuffer, smartSettings); + usedMethod = 'Vector'; + } else if (algorithm === 'photon') { + showLoader('Running Photon (Rasterize) compression...'); + resultBytes = await performLegacyCompression(arrayBuffer, legacySettings); + usedMethod = 'Photon'; } else { - showAlert('Vector failed to reduce size. Trying Photon...', 'info'); - showLoader('Running Automatic (Photon fallback)...'); - resultBytes = await performLegacyCompression( + showLoader('Running Automatic (Vector first)...'); + const vectorResultBytes = await performSmartCompression( arrayBuffer, - legacySettings + smartSettings ); - usedMethod = 'Photon (Automatic)'; + + if (vectorResultBytes.length < originalFile.size) { + resultBytes = vectorResultBytes; + usedMethod = 'Vector (Automatic)'; + } else { + showAlert('Vector failed to reduce size. Trying Photon...', 'info'); + showLoader('Running Automatic (Photon fallback)...'); + resultBytes = await performLegacyCompression( + arrayBuffer, + legacySettings + ); + usedMethod = 'Photon (Automatic)'; + } } + + const originalSize = formatBytes(originalFile.size); + const compressedSize = formatBytes(resultBytes.length); + const savings = originalFile.size - resultBytes.length; + const savingsPercent = + savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0; + + totalOriginal += originalFile.size; + totalCompressed += resultBytes.length; + + if (savings > 0) { + showAlert( + 'Compression Complete', + `Method: **${usedMethod}**. ` + + `File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).` + ); + } else { + showAlert( + 'Compression Finished', + `Method: **${usedMethod}**. ` + + `Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`, + // @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3. + 'warning' + ); + } + zip.file(`compressed-${originalFile.name}`, resultBytes); } + const zipBlob = await zip.generateAsync({ type: 'blob' }); + downloadFile(zipBlob, 'compressed_pdfs.zip'); - const originalSize = formatBytes(originalFile.size); - const compressedSize = formatBytes(resultBytes.length); - const savings = originalFile.size - resultBytes.length; - const savingsPercent = - savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0; + const totalSavings = totalOriginal - totalCompressed; + const totalPercent = ((totalSavings / totalOriginal) * 100).toFixed(1); - if (savings > 0) { - showAlert( - 'Compression Complete', - `Method: **${usedMethod}**. ` + - `File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).` - ); - } else { - showAlert( - 'Compression Finished', - `Method: **${usedMethod}**. ` + - `Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`, - // @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3. - 'warning' - ); - } - - downloadFile( - new Blob([resultBytes], { type: 'application/pdf' }), - 'compressed-final.pdf' + showAlert( + 'All Files Compressed', + `Total saved: ${totalPercent}% (${formatBytes(totalOriginal)} → ${formatBytes(totalCompressed)})` ); } catch (e) { showAlert( diff --git a/src/js/ui.ts b/src/js/ui.ts index c53d62da..400e7456 100644 --- a/src/js/ui.ts +++ b/src/js/ui.ts @@ -532,7 +532,7 @@ export const toolTemplates = { compress: () => `

Compress PDF

Reduce file size by choosing the compression method that best suits your document.

- ${createFileInputHTML()} + ${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })}