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 /> + + +