From d66287a55bac9436eb54a891de2a215a66ea5339 Mon Sep 17 00:00:00 2001
From: alam00000
Date: Sun, 1 Mar 2026 18:43:38 +0530
Subject: [PATCH] feat: support multiple PDF in PDF encryption tool
---
src/js/logic/decrypt-pdf-page.ts | 446 ++++++++++++++++++++-----------
src/js/types/decrypt-pdf-type.ts | 2 +-
src/pages/decrypt-pdf.html | 25 ++
3 files changed, 310 insertions(+), 163 deletions(-)
diff --git a/src/js/logic/decrypt-pdf-page.ts b/src/js/logic/decrypt-pdf-page.ts
index af291fd..4e93018 100644
--- a/src/js/logic/decrypt-pdf-page.ts
+++ b/src/js/logic/decrypt-pdf-page.ts
@@ -1,236 +1,358 @@
import { showAlert } from '../ui.js';
-import { downloadFile, formatBytes, initializeQpdf, readFileAsArrayBuffer } from '../utils/helpers.js';
+import {
+ downloadFile,
+ formatBytes,
+ initializeQpdf,
+ readFileAsArrayBuffer,
+} from '../utils/helpers.js';
import { icons, createIcons } from 'lucide';
+import JSZip from 'jszip';
import { DecryptPdfState } from '@/types';
const pageState: DecryptPdfState = {
- file: null,
+ files: [],
};
function resetState() {
- pageState.file = null;
+ pageState.files = [];
- const fileDisplayArea = document.getElementById('file-display-area');
- if (fileDisplayArea) fileDisplayArea.innerHTML = '';
+ const fileDisplayArea = document.getElementById('file-display-area');
+ if (fileDisplayArea) fileDisplayArea.innerHTML = '';
- const toolOptions = document.getElementById('tool-options');
- if (toolOptions) toolOptions.classList.add('hidden');
+ const toolOptions = document.getElementById('tool-options');
+ if (toolOptions) toolOptions.classList.add('hidden');
- const fileInput = document.getElementById('file-input') as HTMLInputElement;
- if (fileInput) fileInput.value = '';
+ const fileControls = document.getElementById('file-controls');
+ if (fileControls) fileControls.classList.add('hidden');
- const passwordInput = document.getElementById('password-input') as HTMLInputElement;
- if (passwordInput) passwordInput.value = '';
+ const fileInput = document.getElementById('file-input') as HTMLInputElement;
+ if (fileInput) fileInput.value = '';
+
+ const passwordInput = document.getElementById(
+ 'password-input'
+ ) as HTMLInputElement;
+ if (passwordInput) passwordInput.value = '';
}
async function updateUI() {
- const fileDisplayArea = document.getElementById('file-display-area');
- const toolOptions = document.getElementById('tool-options');
+ const fileDisplayArea = document.getElementById('file-display-area');
+ const toolOptions = document.getElementById('tool-options');
+ const fileControls = document.getElementById('file-controls');
- if (!fileDisplayArea) return;
+ if (!fileDisplayArea) return;
- fileDisplayArea.innerHTML = '';
+ fileDisplayArea.innerHTML = '';
- if (pageState.file) {
- const fileDiv = document.createElement('div');
- fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
+ if (pageState.files.length > 0) {
+ pageState.files.forEach((file, index) => {
+ const fileDiv = document.createElement('div');
+ fileDiv.className =
+ 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
- const infoContainer = document.createElement('div');
- infoContainer.className = 'flex flex-col overflow-hidden';
+ const infoContainer = document.createElement('div');
+ infoContainer.className = 'flex flex-col overflow-hidden';
- const nameSpan = document.createElement('div');
- nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
- nameSpan.textContent = pageState.file.name;
+ const nameSpan = document.createElement('div');
+ nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
+ nameSpan.textContent = file.name;
- const metaSpan = document.createElement('div');
- metaSpan.className = 'text-xs text-gray-400';
- metaSpan.textContent = formatBytes(pageState.file.size);
+ const metaSpan = document.createElement('div');
+ metaSpan.className = 'text-xs text-gray-400';
+ metaSpan.textContent = formatBytes(file.size);
- infoContainer.append(nameSpan, metaSpan);
+ infoContainer.append(nameSpan, metaSpan);
- const removeBtn = document.createElement('button');
- removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
- removeBtn.innerHTML = '';
- removeBtn.onclick = function () {
- resetState();
- };
+ const removeBtn = document.createElement('button');
+ removeBtn.className =
+ 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
+ removeBtn.innerHTML = '';
+ removeBtn.onclick = function () {
+ pageState.files.splice(index, 1);
+ updateUI();
+ };
- fileDiv.append(infoContainer, removeBtn);
- fileDisplayArea.appendChild(fileDiv);
- createIcons({ icons });
+ fileDiv.append(infoContainer, removeBtn);
+ fileDisplayArea.appendChild(fileDiv);
+ });
- if (toolOptions) toolOptions.classList.remove('hidden');
- } else {
- if (toolOptions) toolOptions.classList.add('hidden');
- }
+ createIcons({ icons });
+
+ if (toolOptions) toolOptions.classList.remove('hidden');
+ if (fileControls) fileControls.classList.remove('hidden');
+ } else {
+ if (toolOptions) toolOptions.classList.add('hidden');
+ if (fileControls) fileControls.classList.add('hidden');
+ }
}
function handleFileSelect(files: FileList | null) {
- if (files && files.length > 0) {
- const file = files[0];
- if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
- pageState.file = file;
- updateUI();
- }
+ if (files && files.length > 0) {
+ const pdfFiles = Array.from(files).filter(
+ (f) =>
+ f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
+ );
+ if (pdfFiles.length > 0) {
+ pageState.files.push(...pdfFiles);
+ updateUI();
}
+ }
}
async function decryptPdf() {
- if (!pageState.file) {
- showAlert('No File', 'Please upload a PDF file first.');
- return;
- }
+ if (pageState.files.length === 0) {
+ showAlert('No File', 'Please upload at least one PDF file.');
+ return;
+ }
- const password = (document.getElementById('password-input') as HTMLInputElement)?.value;
+ const password = (
+ document.getElementById('password-input') as HTMLInputElement
+ )?.value;
- if (!password) {
- showAlert('Input Required', 'Please enter the PDF password.');
- return;
- }
+ if (!password) {
+ showAlert('Input Required', 'Please enter the PDF password.');
+ return;
+ }
- const inputPath = '/input.pdf';
- const outputPath = '/output.pdf';
- let qpdf: any;
+ const loaderModal = document.getElementById('loader-modal');
+ const loaderText = document.getElementById('loader-text');
+ let qpdf: any;
- const loaderModal = document.getElementById('loader-modal');
- const loaderText = document.getElementById('loader-text');
+ try {
+ if (loaderModal) loaderModal.classList.remove('hidden');
+ if (loaderText) loaderText.textContent = 'Initializing decryption...';
- try {
- if (loaderModal) loaderModal.classList.remove('hidden');
- if (loaderText) loaderText.textContent = 'Initializing decryption...';
+ qpdf = await initializeQpdf();
- qpdf = await initializeQpdf();
+ if (pageState.files.length === 1) {
+ // Single file: decrypt and download directly
+ const file = pageState.files[0];
+ const inputPath = '/input.pdf';
+ const outputPath = '/output.pdf';
+ try {
if (loaderText) loaderText.textContent = 'Reading encrypted PDF...';
- const fileBuffer = await readFileAsArrayBuffer(pageState.file);
+ const fileBuffer = await readFileAsArrayBuffer(file);
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
-
qpdf.FS.writeFile(inputPath, uint8Array);
if (loaderText) loaderText.textContent = 'Decrypting PDF...';
-
- const args = [inputPath, '--password=' + password, '--decrypt', outputPath];
+ const args = [
+ inputPath,
+ '--password=' + password,
+ '--decrypt',
+ outputPath,
+ ];
try {
- qpdf.callMain(args);
+ qpdf.callMain(args);
} catch (qpdfError: any) {
- console.error('qpdf execution error:', qpdfError);
-
- if (
- qpdfError.message?.includes('invalid password') ||
- qpdfError.message?.includes('password')
- ) {
- throw new Error('INVALID_PASSWORD');
- }
- throw qpdfError;
+ if (
+ qpdfError.message?.includes('invalid password') ||
+ qpdfError.message?.includes('password')
+ ) {
+ throw new Error('INVALID_PASSWORD');
+ }
+ throw qpdfError;
}
if (loaderText) loaderText.textContent = 'Preparing download...';
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
if (outputFile.length === 0) {
- throw new Error('Decryption resulted in an empty file.');
+ throw new Error('Decryption resulted in an empty file.');
}
- const blob = new Blob([outputFile], { type: 'application/pdf' });
- downloadFile(blob, `unlocked-${pageState.file.name}`);
+ const blob = new Blob([new Uint8Array(outputFile)], {
+ type: 'application/pdf',
+ });
+ downloadFile(blob, `unlocked-${file.name}`);
if (loaderModal) loaderModal.classList.add('hidden');
showAlert(
- 'Success',
- 'PDF decrypted successfully! Your download has started.',
- 'success',
- () => { resetState(); }
+ 'Success',
+ 'PDF decrypted successfully! Your download has started.',
+ 'success',
+ () => {
+ resetState();
+ }
);
- } catch (error: any) {
- console.error('Error during PDF decryption:', error);
- if (loaderModal) loaderModal.classList.add('hidden');
-
- if (error.message === 'INVALID_PASSWORD') {
- showAlert(
- 'Incorrect Password',
- 'The password you entered is incorrect. Please try again.'
- );
- } else if (error.message?.includes('password')) {
- showAlert(
- 'Password Error',
- 'Unable to decrypt the PDF with the provided password.'
- );
- } else {
- showAlert(
- 'Decryption Failed',
- `An error occurred: ${error.message || 'The password you entered is wrong or the file is corrupted.'}`
- );
- }
- } finally {
+ } finally {
try {
- if (qpdf?.FS) {
- try {
- qpdf.FS.unlink(inputPath);
- } catch (e) {
- console.warn('Failed to unlink input file:', e);
- }
- try {
- qpdf.FS.unlink(outputPath);
- } catch (e) {
- console.warn('Failed to unlink output file:', e);
- }
- }
+ if (qpdf?.FS) {
+ if (qpdf.FS.analyzePath(inputPath).exists)
+ qpdf.FS.unlink(inputPath);
+ if (qpdf.FS.analyzePath(outputPath).exists)
+ qpdf.FS.unlink(outputPath);
+ }
} catch (cleanupError) {
- console.warn('Failed to cleanup WASM FS:', cleanupError);
+ console.warn('Failed to cleanup WASM FS:', cleanupError);
}
+ }
+ } else {
+ // Multiple files: decrypt all and download as ZIP
+ const zip = new JSZip();
+ let successCount = 0;
+ let errorCount = 0;
+
+ for (let i = 0; i < pageState.files.length; i++) {
+ const file = pageState.files[i];
+ const inputPath = `/input_${i}.pdf`;
+ const outputPath = `/output_${i}.pdf`;
+
+ if (loaderText)
+ loaderText.textContent = `Decrypting ${file.name} (${i + 1}/${pageState.files.length})...`;
+
+ try {
+ const fileBuffer = await readFileAsArrayBuffer(file);
+ const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
+ qpdf.FS.writeFile(inputPath, uint8Array);
+
+ const args = [
+ inputPath,
+ '--password=' + password,
+ '--decrypt',
+ outputPath,
+ ];
+
+ try {
+ qpdf.callMain(args);
+ } catch (qpdfError: any) {
+ if (
+ qpdfError.message?.includes('invalid password') ||
+ qpdfError.message?.includes('password')
+ ) {
+ throw new Error(`Invalid password for ${file.name}`);
+ }
+ throw qpdfError;
+ }
+
+ const outputFile = qpdf.FS.readFile(outputPath, {
+ encoding: 'binary',
+ });
+ if (!outputFile || outputFile.length === 0) {
+ throw new Error(
+ `Decryption resulted in an empty file for ${file.name}.`
+ );
+ }
+
+ zip.file(`unlocked-${file.name}`, outputFile, { binary: true });
+ successCount++;
+ } catch (fileError: any) {
+ errorCount++;
+ console.error(`Failed to decrypt ${file.name}:`, fileError);
+ } finally {
+ try {
+ if (qpdf?.FS) {
+ if (qpdf.FS.analyzePath(inputPath).exists)
+ qpdf.FS.unlink(inputPath);
+ if (qpdf.FS.analyzePath(outputPath).exists)
+ qpdf.FS.unlink(outputPath);
+ }
+ } catch (cleanupError) {
+ console.warn(
+ `Failed to cleanup WASM FS for ${file.name}:`,
+ cleanupError
+ );
+ }
+ }
+ }
+
+ if (successCount === 0) {
+ throw new Error(
+ 'No PDF files could be decrypted. The password may be incorrect.'
+ );
+ }
+
+ if (loaderText) loaderText.textContent = 'Generating ZIP file...';
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
+ downloadFile(zipBlob, 'decrypted-pdfs.zip');
+
+ let alertMessage = `${successCount} PDF(s) decrypted successfully.`;
+ if (errorCount > 0) {
+ alertMessage += ` ${errorCount} file(s) failed.`;
+ }
+ showAlert('Processing Complete', alertMessage, 'success', () => {
+ resetState();
+ });
}
+ } catch (error: any) {
+ console.error('Error during PDF decryption:', error);
+
+ if (error.message === 'INVALID_PASSWORD') {
+ showAlert(
+ 'Incorrect Password',
+ 'The password you entered is incorrect. Please try again.'
+ );
+ } else if (error.message?.includes('password')) {
+ showAlert(
+ 'Password Error',
+ 'Unable to decrypt the PDF with the provided password.'
+ );
+ } else {
+ showAlert(
+ 'Decryption Failed',
+ `An error occurred: ${error.message || 'The password you entered is wrong or the file is corrupted.'}`
+ );
+ }
+ } finally {
+ if (loaderModal) loaderModal.classList.add('hidden');
+ }
}
document.addEventListener('DOMContentLoaded', function () {
- const fileInput = document.getElementById('file-input') as HTMLInputElement;
- const dropZone = document.getElementById('drop-zone');
- const processBtn = document.getElementById('process-btn');
- const backBtn = document.getElementById('back-to-tools');
+ const fileInput = document.getElementById('file-input') as HTMLInputElement;
+ const dropZone = document.getElementById('drop-zone');
+ const processBtn = document.getElementById('process-btn');
+ const addMoreBtn = document.getElementById('add-more-btn');
+ const clearFilesBtn = document.getElementById('clear-files-btn');
+ const backBtn = document.getElementById('back-to-tools');
- if (backBtn) {
- backBtn.addEventListener('click', function () {
- window.location.href = import.meta.env.BASE_URL;
- });
- }
+ if (backBtn) {
+ backBtn.addEventListener('click', function () {
+ window.location.href = import.meta.env.BASE_URL;
+ });
+ }
- if (fileInput && dropZone) {
- fileInput.addEventListener('change', function (e) {
- handleFileSelect((e.target as HTMLInputElement).files);
- });
+ if (fileInput && dropZone) {
+ fileInput.addEventListener('change', function (e) {
+ handleFileSelect((e.target as HTMLInputElement).files);
+ });
- dropZone.addEventListener('dragover', function (e) {
- e.preventDefault();
- dropZone.classList.add('bg-gray-700');
- });
+ dropZone.addEventListener('dragover', function (e) {
+ e.preventDefault();
+ dropZone.classList.add('bg-gray-700');
+ });
- dropZone.addEventListener('dragleave', function (e) {
- e.preventDefault();
- dropZone.classList.remove('bg-gray-700');
- });
+ dropZone.addEventListener('dragleave', function (e) {
+ e.preventDefault();
+ dropZone.classList.remove('bg-gray-700');
+ });
- dropZone.addEventListener('drop', function (e) {
- e.preventDefault();
- dropZone.classList.remove('bg-gray-700');
- const files = e.dataTransfer?.files;
- if (files && files.length > 0) {
- const pdfFiles = Array.from(files).filter(function (f) {
- return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
- });
- if (pdfFiles.length > 0) {
- const dataTransfer = new DataTransfer();
- dataTransfer.items.add(pdfFiles[0]);
- handleFileSelect(dataTransfer.files);
- }
- }
- });
+ dropZone.addEventListener('drop', function (e) {
+ e.preventDefault();
+ dropZone.classList.remove('bg-gray-700');
+ handleFileSelect(e.dataTransfer?.files);
+ });
- fileInput.addEventListener('click', function () {
- fileInput.value = '';
- });
- }
+ fileInput.addEventListener('click', function () {
+ fileInput.value = '';
+ });
+ }
- if (processBtn) {
- processBtn.addEventListener('click', decryptPdf);
- }
+ if (processBtn) {
+ processBtn.addEventListener('click', decryptPdf);
+ }
+
+ if (addMoreBtn) {
+ addMoreBtn.addEventListener('click', function () {
+ fileInput.value = '';
+ fileInput.click();
+ });
+ }
+
+ if (clearFilesBtn) {
+ clearFilesBtn.addEventListener('click', function () {
+ resetState();
+ });
+ }
});
diff --git a/src/js/types/decrypt-pdf-type.ts b/src/js/types/decrypt-pdf-type.ts
index 15ec438..053c177 100644
--- a/src/js/types/decrypt-pdf-type.ts
+++ b/src/js/types/decrypt-pdf-type.ts
@@ -1,3 +1,3 @@
export interface DecryptPdfState {
- file: File | null;
+ files: File[];
}
diff --git a/src/pages/decrypt-pdf.html b/src/pages/decrypt-pdf.html
index 0444c76..6b4bc14 100644
--- a/src/pages/decrypt-pdf.html
+++ b/src/pages/decrypt-pdf.html
@@ -141,6 +141,12 @@
>
or drag and drop
+
+ Single or multiple PDF files supported
+
Your files never leave your device.
@@ -150,9 +156,28 @@
type="file"
class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
accept="application/pdf"
+ multiple
/>
+
+
+
+
+
+