diff --git a/index.html b/index.html index 366e593..7f1a128 100644 --- a/index.html +++ b/index.html @@ -863,6 +863,73 @@ + +
diff --git a/src/js/handlers/fileHandler.ts b/src/js/handlers/fileHandler.ts index 6be0dce..db1a75f 100644 --- a/src/js/handlers/fileHandler.ts +++ b/src/js/handlers/fileHandler.ts @@ -19,6 +19,10 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { icons, createIcons } from 'lucide'; import Sortable from 'sortablejs'; import { makeUniqueFileKey } from '../utils/deduplicate-filename.js'; +import { + promptAndDecryptFile, + handleEncryptedFiles, +} from '../utils/password-prompt.js'; import { multiFileTools, simpleTools, @@ -31,7 +35,6 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( import.meta.url ).toString(); -// Re-export rotation state utilities export { getRotationState, updateRotationState, @@ -43,10 +46,9 @@ const rotationState: number[] = []; let imageSortableInstance: Sortable | null = null; const activeImageUrls = new Map(); -async function handleSinglePdfUpload(toolId, file) { +async function handleSinglePdfUpload(toolId: string, file: File) { showLoader('Loading PDF...'); try { - // For form-filler, bypass pdf-lib (can't handle XFA) and use PDF.js if (toolId === 'form-filler') { hideLoader(); @@ -80,12 +82,14 @@ async function handleSinglePdfUpload(toolId, file) { toolId !== 'change-permissions' && toolId !== 'remove-restrictions' ) { - showAlert( - 'Protected PDF', - 'This PDF is password-protected. Please use the Decrypt or Change Permissions tool first.' - ); - switchView('grid'); - return; + const decryptedFile = await promptAndDecryptFile(file); + if (!decryptedFile) { + switchView('grid'); + return; + } + const decryptedBytes = await readFileAsArrayBuffer(decryptedFile); + state.pdfDoc = await PDFLibDocument.load(decryptedBytes as ArrayBuffer); + state.files = [decryptedFile]; } const optionsDiv = document.querySelector( @@ -127,7 +131,6 @@ async function handleSinglePdfUpload(toolId, file) { await renderPageThumbnails(toolId, state.pdfDoc); if (toolId === 'rotate') { - // Initialize rotation state for all pages rotationState.length = 0; for (let i = 0; i < state.pdfDoc.getPageCount(); i++) { rotationState.push(0); @@ -157,12 +160,10 @@ async function handleSinglePdfUpload(toolId, file) { createIcons({ icons }); const rotateAll = (angle: number) => { - // Update rotation state for ALL pages (including unrendered ones) for (let i = 0; i < rotationState.length; i++) { rotationState[i] = rotationState[i] + angle; } - // Update DOM for currently rendered pages document.querySelectorAll('.page-rotator-item').forEach((item) => { const pageIndex = parseInt( (item as HTMLElement).dataset.pageIndex || '0' @@ -236,7 +237,7 @@ async function handleSinglePdfUpload(toolId, file) { resultsDiv.textContent = ''; // Clear safely - const createSection = (title) => { + const createSection = (title: string) => { const wrapper = document.createElement('div'); wrapper.className = 'mb-4'; const h3 = document.createElement('h3'); @@ -249,7 +250,7 @@ async function handleSinglePdfUpload(toolId, file) { return { wrapper, ul }; }; - const createListItem = (key, value) => { + const createListItem = (key: string, value: string) => { const li = document.createElement('li'); li.className = 'flex flex-col sm:flex-row'; const strong = document.createElement('strong'); @@ -262,13 +263,8 @@ async function handleSinglePdfUpload(toolId, file) { return li; }; - const parsePdfDate = (pdfDate) => { - if ( - !pdfDate || - typeof pdfDate !== 'string' || - !pdfDate.startsWith('D:') - ) - return pdfDate; + const parsePdfDate = (pdfDate: string): string => { + if (!pdfDate || !pdfDate.startsWith('D:')) return pdfDate; try { const year = pdfDate.substring(2, 6); const month = pdfDate.substring(6, 8); @@ -319,8 +315,8 @@ async function handleSinglePdfUpload(toolId, file) { const fieldsSection = createSection('Interactive Form Fields'); if (fieldObjects && Object.keys(fieldObjects).length > 0) { for (const fieldName in fieldObjects) { - const field = fieldObjects[fieldName][0]; - const value = (field as any).fieldValue || '- Not Set -'; + const field = fieldObjects[fieldName][0] as Record; + const value = field.fieldValue || '- Not Set -'; fieldsSection.ul.appendChild( createListItem(fieldName, String(value)) ); @@ -330,7 +326,7 @@ async function handleSinglePdfUpload(toolId, file) { } resultsDiv.appendChild(fieldsSection.wrapper); - const createXmpListItem = (key, value, indent = 0) => { + const createXmpListItem = (key: string, value: string, indent = 0) => { const li = document.createElement('li'); li.className = 'flex flex-col sm:flex-row'; @@ -347,7 +343,7 @@ async function handleSinglePdfUpload(toolId, file) { return li; }; - const createXmpHeaderItem = (key, indent = 0) => { + const createXmpHeaderItem = (key: string, indent = 0) => { const li = document.createElement('li'); li.className = 'flex pt-2'; const strong = document.createElement('strong'); @@ -358,7 +354,11 @@ async function handleSinglePdfUpload(toolId, file) { return li; }; - const appendXmpNodes = (xmlNode, ulElement, indentLevel) => { + const appendXmpNodes = ( + xmlNode: Element, + ulElement: HTMLUListElement, + indentLevel: number + ) => { const xmpDateKeys = [ 'xap:CreateDate', 'xap:ModifyDate', @@ -368,12 +368,12 @@ async function handleSinglePdfUpload(toolId, file) { const childNodes = Array.from(xmlNode.children); for (const child of childNodes) { - if ((child as Element).nodeType !== 1) continue; + if (child.nodeType !== 1) continue; - let key = (child as Element).tagName; - const elementChildren = Array.from( - (child as Element).children - ).filter((c) => c.nodeType === 1); + let key = child.tagName; + const elementChildren = Array.from(child.children).filter( + (c) => c.nodeType === 1 + ); if (key === 'rdf:li') { appendXmpNodes(child, ulElement, indentLevel); @@ -384,7 +384,7 @@ async function handleSinglePdfUpload(toolId, file) { } if ( - (child as Element).getAttribute('rdf:parseType') === 'Resource' && + child.getAttribute('rdf:parseType') === 'Resource' && elementChildren.length === 0 ) { ulElement.appendChild( @@ -397,7 +397,7 @@ async function handleSinglePdfUpload(toolId, file) { ulElement.appendChild(createXmpHeaderItem(key, indentLevel)); appendXmpNodes(child, ulElement, indentLevel + 1); } else { - let value = (child as Element).textContent.trim(); + let value = (child.textContent ?? '').trim(); if (value) { if (xmpDateKeys.includes(key)) { value = formatIsoDate(value); @@ -462,9 +462,9 @@ async function handleSinglePdfUpload(toolId, file) { const container = document.getElementById('custom-metadata-container'); const addBtn = document.getElementById('add-custom-meta-btn'); - const formatDateForInput = (date) => { + const formatDateForInput = (date: Date | undefined) => { if (!date) return ''; - const pad = (num) => num.toString().padStart(2, '0'); + const pad = (num: number) => num.toString().padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; }; @@ -529,7 +529,6 @@ async function handleSinglePdfUpload(toolId, file) { toolLogic['page-dimensions'](); } - // Setup quality sliders for image conversion tools if (toolId === 'pdf-to-jpg') { const qualitySlider = document.getElementById( 'jpg-quality' @@ -585,7 +584,7 @@ async function handleSinglePdfUpload(toolId, file) { } } -async function handleMultiFileUpload(toolId) { +async function handleMultiFileUpload(toolId: string) { if ( toolId === 'merge' || toolId === 'alternate-merge' || @@ -615,24 +614,44 @@ async function handleMultiFileUpload(toolId) { }) ); - const foundEncryptedPDFs = pdfFilesLoaded.filter( - (pdf) => pdf.pdfDoc.isEncrypted - ); + const encryptedIndices: number[] = []; + pdfFilesLoaded.forEach((pdf, index) => { + if (pdf.pdfDoc.isEncrypted) { + encryptedIndices.push(index); + } + }); - if (foundEncryptedPDFs.length > 0) { - const encryptedPDFFileNames = []; - foundEncryptedPDFs.forEach((encryptedPDF) => { - encryptedPDFFileNames.push(encryptedPDF.file.name); - }); + if (encryptedIndices.length > 0) { + hideLoader(); + const decryptedFiles = await handleEncryptedFiles( + pdfFilesUnloaded, + encryptedIndices + ); - const errorMessage = `PDFs found that are password-protected\n\nPlease use the Decrypt or Change Permissions tool on these files first:\n\n${encryptedPDFFileNames.join('\n')}`; + for (const [index, decryptedFile] of decryptedFiles) { + const originalIndex = state.files.indexOf(pdfFilesUnloaded[index]); + if (originalIndex !== -1) { + state.files[originalIndex] = decryptedFile; + } + } - hideLoader(); // Hide loader before showing alert - showAlert('Protected PDFs', errorMessage); + const skippedFiles = new Set( + encryptedIndices + .filter((i) => !decryptedFiles.has(i)) + .map((i) => pdfFilesUnloaded[i]) + ); + if (skippedFiles.size > 0) { + state.files = state.files.filter((f) => !skippedFiles.has(f)); + } - switchView('grid'); + if ( + state.files.filter((f) => f.type === 'application/pdf').length === 0 + ) { + switchView('grid'); + return; + } - return; + showLoader('Loading PDF documents...'); } } @@ -646,10 +665,6 @@ async function handleMultiFileUpload(toolId) { } } - // if (toolId === 'merge') { - // toolLogic.merge.setup(); - // } - if (toolId === 'alternate-merge') { toolLogic['alternate-merge'].setup(); } else if (toolId === 'image-to-pdf') { @@ -791,12 +806,12 @@ async function handleMultiFileUpload(toolId) { } } -export function setupFileInputHandler(toolId) { +export function setupFileInputHandler(toolId: string) { const fileInput = document.getElementById('file-input'); const isMultiFileTool = multiFileTools.includes(toolId); let isFirstUpload = true; - const processFiles = async (newFiles) => { + const processFiles = async (newFiles: File[]) => { if (newFiles.length === 0) return; if (toolId === 'image-to-pdf') { diff --git a/src/js/logic/add-attachments-page.ts b/src/js/logic/add-attachments-page.ts index fbec042..0bf01e8 100644 --- a/src/js/logic/add-attachments-page.ts +++ b/src/js/logic/add-attachments-page.ts @@ -8,6 +8,7 @@ import { showWasmRequiredDialog, WasmProvider, } from '../utils/wasm-provider.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; const worker = new Worker( import.meta.env.BASE_URL + 'workers/add-attachments.worker.js' @@ -129,13 +130,16 @@ async function updateUI() { createIcons({ icons }); try { + const result = await loadPdfWithPasswordPrompt(pageState.file); + if (!result) { + resetState(); + return; + } + result.pdf.destroy(); + pageState.file = result.file; showLoader('Loading PDF...'); - const arrayBuffer = await pageState.file.arrayBuffer(); - pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, { - ignoreEncryption: true, - throwOnInvalidObject: false, - }); + pageState.pdfDoc = await PDFLibDocument.load(result.bytes); const pageCount = pageState.pdfDoc.getPageCount(); metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`; @@ -269,10 +273,13 @@ async function addAttachments() { const transferables = [pdfBuffer, ...attachmentBuffers]; worker.postMessage(message, transferables); - } catch (error: any) { + } catch (error) { console.error('Error attaching files:', error); hideLoader(); - showAlert('Error', `Failed to attach files: ${error.message}`); + showAlert( + 'Error', + `Failed to attach files: ${error instanceof Error ? error.message : 'Unknown error'}` + ); } } diff --git a/src/js/logic/add-blank-page-page.ts b/src/js/logic/add-blank-page-page.ts index ee9387e..439b6b6 100644 --- a/src/js/logic/add-blank-page-page.ts +++ b/src/js/logic/add-blank-page-page.ts @@ -2,230 +2,274 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { AddBlankPageState } from '@/types'; - const pageState: AddBlankPageState = { - file: null, - pdfDoc: null, + file: null, + pdfDoc: null, }; function resetState() { - pageState.file = null; - pageState.pdfDoc = null; + pageState.file = null; + pageState.pdfDoc = null; - 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 fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; - const pagePositionInput = document.getElementById('page-position') as HTMLInputElement; - if (pagePositionInput) pagePositionInput.value = '0'; + const pagePositionInput = document.getElementById( + 'page-position' + ) as HTMLInputElement; + if (pagePositionInput) pagePositionInput.value = '0'; - const pageCountInput = document.getElementById('page-count') as HTMLInputElement; - if (pageCountInput) pageCountInput.value = '1'; + const pageCountInput = document.getElementById( + 'page-count' + ) as HTMLInputElement; + if (pageCountInput) pageCountInput.value = '1'; } async function updateUI() { - const fileDisplayArea = document.getElementById('file-display-area'); - const toolOptions = document.getElementById('tool-options'); - const pagePositionHint = document.getElementById('page-position-hint'); - const pagePositionInput = document.getElementById('page-position') as HTMLInputElement; + const fileDisplayArea = document.getElementById('file-display-area'); + const toolOptions = document.getElementById('tool-options'); + const pagePositionHint = document.getElementById('page-position-hint'); + const pagePositionInput = document.getElementById( + 'page-position' + ) as HTMLInputElement; - 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.file) { + 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 = pageState.file.name; - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`; + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`; - 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 () { + resetState(); + }; - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); - // Load PDF document - try { - showLoader('Loading PDF...'); - const arrayBuffer = await pageState.file.arrayBuffer(); - pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, { - ignoreEncryption: true, - throwOnInvalidObject: false - }); - hideLoader(); + // Load PDF document + try { + const result = await loadPdfWithPasswordPrompt(pageState.file); + if (!result) { + resetState(); + return; + } + showLoader('Loading PDF...'); + pageState.file = result.file; + pageState.pdfDoc = await PDFLibDocument.load(result.bytes, { + throwOnInvalidObject: false, + }); + result.pdf.destroy(); + hideLoader(); - const pageCount = pageState.pdfDoc.getPageCount(); - metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`; + const pageCount = pageState.pdfDoc.getPageCount(); + metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`; - if (pagePositionHint) { - pagePositionHint.textContent = `Enter 0 to insert at the beginning, or ${pageCount} to insert at the end.`; - } - if (pagePositionInput) { - pagePositionInput.max = pageCount.toString(); - } + if (pagePositionHint) { + pagePositionHint.textContent = `Enter 0 to insert at the beginning, or ${pageCount} to insert at the end.`; + } + if (pagePositionInput) { + pagePositionInput.max = pageCount.toString(); + } - if (toolOptions) toolOptions.classList.remove('hidden'); - } catch (error) { - console.error('Error loading PDF:', error); - hideLoader(); - showAlert('Error', 'Failed to load PDF file.'); - resetState(); - } - } else { - if (toolOptions) toolOptions.classList.add('hidden'); + if (toolOptions) toolOptions.classList.remove('hidden'); + } catch (error) { + console.error('Error loading PDF:', error); + hideLoader(); + showAlert('Error', 'Failed to load PDF file.'); + resetState(); } + } else { + if (toolOptions) toolOptions.classList.add('hidden'); + } } async function addBlankPages() { - if (!pageState.pdfDoc || !pageState.file) { - showAlert('Error', 'Please upload a PDF first.'); - return; + if (!pageState.pdfDoc || !pageState.file) { + showAlert('Error', 'Please upload a PDF first.'); + return; + } + + const pagePositionInput = document.getElementById( + 'page-position' + ) as HTMLInputElement; + const pageCountInput = document.getElementById( + 'page-count' + ) as HTMLInputElement; + + const position = parseInt(pagePositionInput.value); + const insertCount = parseInt(pageCountInput.value); + const totalPages = pageState.pdfDoc.getPageCount(); + + if (isNaN(position) || position < 0 || position > totalPages) { + showAlert( + 'Invalid Input', + `Please enter a number between 0 and ${totalPages}.` + ); + return; + } + + if (isNaN(insertCount) || insertCount < 1) { + showAlert( + 'Invalid Input', + 'Please enter a valid number of pages (1 or more).' + ); + return; + } + + showLoader( + `Adding ${insertCount} blank page${insertCount > 1 ? 's' : ''}...` + ); + + try { + const newPdf = await PDFLibDocument.create(); + const { width, height } = pageState.pdfDoc.getPage(0).getSize(); + const allIndices = Array.from({ length: totalPages }, function (_, i) { + return i; + }); + + const indicesBefore = allIndices.slice(0, position); + const indicesAfter = allIndices.slice(position); + + if (indicesBefore.length > 0) { + const copied = await newPdf.copyPages(pageState.pdfDoc, indicesBefore); + copied.forEach(function (p) { + newPdf.addPage(p); + }); } - const pagePositionInput = document.getElementById('page-position') as HTMLInputElement; - const pageCountInput = document.getElementById('page-count') as HTMLInputElement; - - const position = parseInt(pagePositionInput.value); - const insertCount = parseInt(pageCountInput.value); - const totalPages = pageState.pdfDoc.getPageCount(); - - if (isNaN(position) || position < 0 || position > totalPages) { - showAlert('Invalid Input', `Please enter a number between 0 and ${totalPages}.`); - return; + // Add the specified number of blank pages + for (let i = 0; i < insertCount; i++) { + newPdf.addPage([width, height]); } - if (isNaN(insertCount) || insertCount < 1) { - showAlert('Invalid Input', 'Please enter a valid number of pages (1 or more).'); - return; + if (indicesAfter.length > 0) { + const copied = await newPdf.copyPages(pageState.pdfDoc, indicesAfter); + copied.forEach(function (p) { + newPdf.addPage(p); + }); } - showLoader(`Adding ${insertCount} blank page${insertCount > 1 ? 's' : ''}...`); + const newPdfBytes = await newPdf.save(); + const originalName = pageState.file.name.replace(/\.pdf$/i, ''); - try { - const newPdf = await PDFLibDocument.create(); - const { width, height } = pageState.pdfDoc.getPage(0).getSize(); - const allIndices = Array.from({ length: totalPages }, function (_, i) { return i; }); + downloadFile( + new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), + `${originalName}_blank-pages-added.pdf` + ); - const indicesBefore = allIndices.slice(0, position); - const indicesAfter = allIndices.slice(position); - - if (indicesBefore.length > 0) { - const copied = await newPdf.copyPages(pageState.pdfDoc, indicesBefore); - copied.forEach(function (p) { newPdf.addPage(p); }); - } - - // Add the specified number of blank pages - for (let i = 0; i < insertCount; i++) { - newPdf.addPage([width, height]); - } - - if (indicesAfter.length > 0) { - const copied = await newPdf.copyPages(pageState.pdfDoc, indicesAfter); - copied.forEach(function (p) { newPdf.addPage(p); }); - } - - const newPdfBytes = await newPdf.save(); - const originalName = pageState.file.name.replace(/\.pdf$/i, ''); - - downloadFile( - new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), - `${originalName}_blank-pages-added.pdf` - ); - - showAlert('Success', `Added ${insertCount} blank page${insertCount > 1 ? 's' : ''} successfully!`, 'success', function () { - resetState(); - }); - } catch (e) { - console.error(e); - showAlert('Error', `Could not add blank page${insertCount > 1 ? 's' : ''}.`); - } finally { - hideLoader(); - } + showAlert( + 'Success', + `Added ${insertCount} blank page${insertCount > 1 ? 's' : ''} successfully!`, + 'success', + function () { + resetState(); + } + ); + } catch (e) { + console.error(e); + showAlert( + 'Error', + `Could not add blank page${insertCount > 1 ? 's' : ''}.` + ); + } finally { + hideLoader(); + } } 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 file = files[0]; + if ( + file.type === 'application/pdf' || + file.name.toLowerCase().endsWith('.pdf') + ) { + pageState.file = file; + updateUI(); } + } } 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 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); + }); + + 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('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); + } + } + }); - if (fileInput && dropZone) { - fileInput.addEventListener('change', function (e) { - handleFileSelect((e.target as HTMLInputElement).files); - }); + fileInput.addEventListener('click', function () { + fileInput.value = ''; + }); + } - 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('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); - } - } - }); - - fileInput.addEventListener('click', function () { - fileInput.value = ''; - }); - } - - if (processBtn) { - processBtn.addEventListener('click', addBlankPages); - } + if (processBtn) { + processBtn.addEventListener('click', addBlankPages); + } }); diff --git a/src/js/logic/add-stamps.ts b/src/js/logic/add-stamps.ts index a34c7f5..92e15ea 100644 --- a/src/js/logic/add-stamps.ts +++ b/src/js/logic/add-stamps.ts @@ -1,290 +1,342 @@ -import { formatBytes, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers' -import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js' -import { createIcons, icons } from 'lucide' +import { + formatBytes, + readFileAsArrayBuffer, + getPDFDocument, +} from '../utils/helpers'; +import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'; +import { createIcons, icons } from 'lucide'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; -let selectedFile: File | null = null -let viewerIframe: HTMLIFrameElement | null = null -let viewerReady = false -let currentBlobUrl: string | null = null +let selectedFile: File | null = null; +let viewerIframe: HTMLIFrameElement | null = null; +let viewerReady = false; +let currentBlobUrl: string | null = null; -const pdfInput = document.getElementById('pdfFile') as HTMLInputElement -const fileListDiv = document.getElementById('fileList') as HTMLDivElement -const viewerContainer = document.getElementById('stamp-viewer-container') as HTMLDivElement -const viewerCard = document.getElementById('viewer-card') as HTMLDivElement | null -const saveStampedBtn = document.getElementById('save-stamped-btn') as HTMLButtonElement -const backToToolsBtn = document.getElementById('back-to-tools') as HTMLButtonElement | null -const toolUploader = document.getElementById('tool-uploader') as HTMLDivElement | null -const usernameInput = document.getElementById('stamp-username') as HTMLInputElement | null +const pdfInput = document.getElementById('pdfFile') as HTMLInputElement; +const fileListDiv = document.getElementById('fileList') as HTMLDivElement; +const viewerContainer = document.getElementById( + 'stamp-viewer-container' +) as HTMLDivElement; +const viewerCard = document.getElementById( + 'viewer-card' +) as HTMLDivElement | null; +const saveStampedBtn = document.getElementById( + 'save-stamped-btn' +) as HTMLButtonElement; +const backToToolsBtn = document.getElementById( + 'back-to-tools' +) as HTMLButtonElement | null; +const toolUploader = document.getElementById( + 'tool-uploader' +) as HTMLDivElement | null; +const usernameInput = document.getElementById( + 'stamp-username' +) as HTMLInputElement | null; function resetState() { - selectedFile = null + selectedFile = null; if (currentBlobUrl) { - URL.revokeObjectURL(currentBlobUrl) - currentBlobUrl = null + URL.revokeObjectURL(currentBlobUrl); + currentBlobUrl = null; } - if (viewerIframe && viewerContainer && viewerIframe.parentElement === viewerContainer) { - viewerContainer.removeChild(viewerIframe) + if ( + viewerIframe && + viewerContainer && + viewerIframe.parentElement === viewerContainer + ) { + viewerContainer.removeChild(viewerIframe); } - viewerIframe = null - viewerReady = false - if (viewerCard) viewerCard.classList.add('hidden') - if (saveStampedBtn) saveStampedBtn.classList.add('hidden') + viewerIframe = null; + viewerReady = false; + if (viewerCard) viewerCard.classList.add('hidden'); + if (saveStampedBtn) saveStampedBtn.classList.add('hidden'); if (viewerContainer) { - viewerContainer.style.height = '' - viewerContainer.style.aspectRatio = '' + viewerContainer.style.height = ''; + viewerContainer.style.aspectRatio = ''; } - const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false' + const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false'; if (toolUploader && !isFullWidth) { - toolUploader.classList.remove('max-w-6xl') - toolUploader.classList.add('max-w-2xl') + toolUploader.classList.remove('max-w-6xl'); + toolUploader.classList.add('max-w-2xl'); } - updateFileList() - if (pdfInput) pdfInput.value = '' + updateFileList(); + if (pdfInput) pdfInput.value = ''; } function updateFileList() { if (!selectedFile) { - fileListDiv.classList.add('hidden') - fileListDiv.innerHTML = '' - return + fileListDiv.classList.add('hidden'); + fileListDiv.innerHTML = ''; + return; } - fileListDiv.classList.remove('hidden') - fileListDiv.innerHTML = '' + fileListDiv.classList.remove('hidden'); + fileListDiv.innerHTML = ''; // Expand container width for viewer if NOT in full width mode (default to true if not set) - const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false' + const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false'; if (toolUploader && !isFullWidth) { - toolUploader.classList.remove('max-w-2xl') - toolUploader.classList.add('max-w-6xl') + toolUploader.classList.remove('max-w-2xl'); + toolUploader.classList.add('max-w-6xl'); } - const wrapper = document.createElement('div') - wrapper.className = 'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors' + const wrapper = document.createElement('div'); + wrapper.className = + 'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors'; - const innerDiv = document.createElement('div') - innerDiv.className = 'flex items-center justify-between' + const innerDiv = document.createElement('div'); + innerDiv.className = 'flex items-center justify-between'; - const infoDiv = document.createElement('div') - infoDiv.className = 'flex-1 min-w-0' + const infoDiv = document.createElement('div'); + infoDiv.className = 'flex-1 min-w-0'; - const nameSpan = document.createElement('p') - nameSpan.className = 'truncate font-medium text-white' - nameSpan.textContent = selectedFile.name + const nameSpan = document.createElement('p'); + nameSpan.className = 'truncate font-medium text-white'; + nameSpan.textContent = selectedFile.name; - const sizeSpan = document.createElement('p') - sizeSpan.className = 'text-gray-400 text-sm' - sizeSpan.textContent = formatBytes(selectedFile.size) + const sizeSpan = document.createElement('p'); + sizeSpan.className = 'text-gray-400 text-sm'; + sizeSpan.textContent = formatBytes(selectedFile.size); - infoDiv.append(nameSpan, sizeSpan) + infoDiv.append(nameSpan, sizeSpan); - const deleteBtn = document.createElement('button') - deleteBtn.className = 'text-red-400 hover:text-red-300 p-2 flex-shrink-0 ml-2' - deleteBtn.title = 'Remove file' - deleteBtn.innerHTML = '' + const deleteBtn = document.createElement('button'); + deleteBtn.className = + 'text-red-400 hover:text-red-300 p-2 flex-shrink-0 ml-2'; + deleteBtn.title = 'Remove file'; + deleteBtn.innerHTML = ''; deleteBtn.onclick = (e) => { - e.stopPropagation() - resetState() - } + e.stopPropagation(); + resetState(); + }; - innerDiv.append(infoDiv, deleteBtn) - wrapper.appendChild(innerDiv) - fileListDiv.appendChild(wrapper) + innerDiv.append(infoDiv, deleteBtn); + wrapper.appendChild(innerDiv); + fileListDiv.appendChild(wrapper); - createIcons({ icons }) + createIcons({ icons }); } async function adjustViewerHeight(file: File) { - if (!viewerContainer) return + if (!viewerContainer) return; try { - const arrayBuffer = await file.arrayBuffer() - const loadingTask = getPDFDocument({ data: arrayBuffer }) - const pdf = await loadingTask.promise - const page = await pdf.getPage(1) - const viewport = page.getViewport({ scale: 1 }) + const arrayBuffer = await file.arrayBuffer(); + const loadingTask = getPDFDocument({ data: arrayBuffer }); + const pdf = await loadingTask.promise; + const page = await pdf.getPage(1); + const viewport = page.getViewport({ scale: 1 }); // Add ~50px for toolbar height relative to page height - const aspectRatio = viewport.width / (viewport.height + 50) + const aspectRatio = viewport.width / (viewport.height + 50); - viewerContainer.style.height = 'auto' - viewerContainer.style.aspectRatio = `${aspectRatio}` + viewerContainer.style.height = 'auto'; + viewerContainer.style.aspectRatio = `${aspectRatio}`; } catch (e) { - console.error('Error adjusting viewer height:', e) + console.error('Error adjusting viewer height:', e); // Fallback if calculation fails - viewerContainer.style.height = '70vh' + viewerContainer.style.height = '70vh'; } } async function loadPdfInViewer(file: File) { - if (!viewerContainer) return + if (!viewerContainer) return; if (viewerCard) { - viewerCard.classList.remove('hidden') + viewerCard.classList.remove('hidden'); } // Clear existing iframe and blob URL if (viewerIframe && viewerIframe.parentElement === viewerContainer) { - viewerContainer.removeChild(viewerIframe) + viewerContainer.removeChild(viewerIframe); } if (currentBlobUrl) { - URL.revokeObjectURL(currentBlobUrl) - currentBlobUrl = null + URL.revokeObjectURL(currentBlobUrl); + currentBlobUrl = null; } - viewerIframe = null - viewerReady = false + viewerIframe = null; + viewerReady = false; // Calculate and apply dynamic height - await adjustViewerHeight(file) + await adjustViewerHeight(file); - const arrayBuffer = await readFileAsArrayBuffer(file) - const blob = new Blob([arrayBuffer as BlobPart], { type: 'application/pdf' }) - currentBlobUrl = URL.createObjectURL(blob) + const arrayBuffer = await readFileAsArrayBuffer(file); + const blob = new Blob([arrayBuffer as BlobPart], { type: 'application/pdf' }); + currentBlobUrl = URL.createObjectURL(blob); try { - const existingPrefsRaw = localStorage.getItem('pdfjs.preferences') - const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {} - delete (existingPrefs as any).annotationEditorMode + const existingPrefsRaw = localStorage.getItem('pdfjs.preferences'); + const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {}; + delete (existingPrefs as any).annotationEditorMode; const newPrefs = { ...existingPrefs, enablePermissions: false, - } - localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs)) - } catch { } + }; + localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs)); + } catch {} - const iframe = document.createElement('iframe') - iframe.className = 'w-full h-full border-0' - iframe.allowFullscreen = true + const iframe = document.createElement('iframe'); + iframe.className = 'w-full h-full border-0'; + iframe.allowFullscreen = true; - const viewerUrl = new URL(import.meta.env.BASE_URL + 'pdfjs-annotation-viewer/web/viewer.html', window.location.origin) - const stampUserName = usernameInput?.value?.trim() || '' + const viewerUrl = new URL( + import.meta.env.BASE_URL + 'pdfjs-annotation-viewer/web/viewer.html', + window.location.origin + ); + const stampUserName = usernameInput?.value?.trim() || ''; // ae_username is the hash parameter used by pdfjs-annotation-extension to set the username - const hashParams = stampUserName ? `#ae_username=${encodeURIComponent(stampUserName)}` : '' - iframe.src = `${viewerUrl.toString()}?file=${encodeURIComponent(currentBlobUrl)}${hashParams}` + const hashParams = stampUserName + ? `#ae_username=${encodeURIComponent(stampUserName)}` + : ''; + iframe.src = `${viewerUrl.toString()}?file=${encodeURIComponent(currentBlobUrl)}${hashParams}`; iframe.addEventListener('load', () => { - setupAnnotationViewer(iframe) - }) + setupAnnotationViewer(iframe); + }); - viewerContainer.appendChild(iframe) - viewerIframe = iframe + viewerContainer.appendChild(iframe); + viewerIframe = iframe; } function setupAnnotationViewer(iframe: HTMLIFrameElement) { try { - const win = iframe.contentWindow as any - const doc = win?.document as Document | null - if (!win || !doc) return + const win = iframe.contentWindow as any; + const doc = win?.document as Document | null; + if (!win || !doc) return; const initialize = async () => { try { - const app = win.PDFViewerApplication + const app = win.PDFViewerApplication; if (app?.initializedPromise) { - await app.initializedPromise + await app.initializedPromise; } - const eventBus = app?.eventBus + const eventBus = app?.eventBus; if (eventBus && typeof eventBus._on === 'function') { eventBus._on('annotationeditoruimanager', () => { try { - const stampBtn = doc.getElementById('editorStampButton') as HTMLButtonElement | null - stampBtn?.click() - } catch { } - }) + const stampBtn = doc.getElementById( + 'editorStampButton' + ) as HTMLButtonElement | null; + stampBtn?.click(); + } catch {} + }); } - const root = doc.querySelector('.PdfjsAnnotationExtension') as HTMLElement | null + const root = doc.querySelector( + '.PdfjsAnnotationExtension' + ) as HTMLElement | null; if (root) { - root.classList.add('PdfjsAnnotationExtension_Comment_hidden') + root.classList.add('PdfjsAnnotationExtension_Comment_hidden'); } - viewerReady = true + viewerReady = true; } catch (e) { - console.error('Failed to initialize annotation viewer for Add Stamps:', e) + console.error( + 'Failed to initialize annotation viewer for Add Stamps:', + e + ); } - } + }; - void initialize() + void initialize(); } catch (e) { - console.error('Error wiring Add Stamps viewer:', e) + console.error('Error wiring Add Stamps viewer:', e); } } async function onPdfSelected(file: File) { - selectedFile = file - updateFileList() - if (saveStampedBtn) saveStampedBtn.classList.remove('hidden') - await loadPdfInViewer(file) + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; + result.pdf.destroy(); + selectedFile = result.file; + updateFileList(); + if (saveStampedBtn) saveStampedBtn.classList.remove('hidden'); + await loadPdfInViewer(result.file); } if (pdfInput) { pdfInput.addEventListener('change', async (e) => { - const target = e.target as HTMLInputElement + const target = e.target as HTMLInputElement; if (target.files && target.files.length > 0) { - const file = target.files[0] - await onPdfSelected(file) + const file = target.files[0]; + await onPdfSelected(file); } - }) + }); } // Add drag/drop support -const dropZone = document.getElementById('drop-zone') +const dropZone = document.getElementById('drop-zone'); if (dropZone) { dropZone.addEventListener('dragover', (e) => { - e.preventDefault() - dropZone.classList.add('border-indigo-500') - }) + e.preventDefault(); + dropZone.classList.add('border-indigo-500'); + }); dropZone.addEventListener('dragleave', () => { - dropZone.classList.remove('border-indigo-500') - }) + dropZone.classList.remove('border-indigo-500'); + }); dropZone.addEventListener('drop', async (e) => { - e.preventDefault() - dropZone.classList.remove('border-indigo-500') - const file = e.dataTransfer?.files[0] + e.preventDefault(); + dropZone.classList.remove('border-indigo-500'); + const file = e.dataTransfer?.files[0]; if (file && file.type === 'application/pdf') { - await onPdfSelected(file) + await onPdfSelected(file); } - }) + }); } if (saveStampedBtn) { saveStampedBtn.addEventListener('click', () => { if (!viewerIframe) { - alert('Viewer not ready. Please upload a PDF and wait for it to finish loading.') - return + alert( + 'Viewer not ready. Please upload a PDF and wait for it to finish loading.' + ); + return; } try { - const win = viewerIframe.contentWindow as any - const extensionInstance = win?.pdfjsAnnotationExtensionInstance as any + const win = viewerIframe.contentWindow as any; + const extensionInstance = win?.pdfjsAnnotationExtensionInstance as any; - if (extensionInstance && typeof extensionInstance.exportPdf === 'function') { - const result = extensionInstance.exportPdf() + if ( + extensionInstance && + typeof extensionInstance.exportPdf === 'function' + ) { + const result = extensionInstance.exportPdf(); if (result && typeof result.then === 'function') { - result.then(() => { - // Reset state after successful export - setTimeout(() => resetState(), 500) - }).catch((err: unknown) => { - console.error('Error while exporting stamped PDF via annotation extension:', err) - }) + result + .then(() => { + // Reset state after successful export + setTimeout(() => resetState(), 500); + }) + .catch((err: unknown) => { + console.error( + 'Error while exporting stamped PDF via annotation extension:', + err + ); + }); } - return + return; } - alert('Could not access the stamped-PDF exporter. Please use the Export → PDF button in the viewer toolbar as a fallback.') + alert( + 'Could not access the stamped-PDF exporter. Please use the Export → PDF button in the viewer toolbar as a fallback.' + ); } catch (e) { - console.error('Failed to trigger stamped PDF export:', e) - alert('Could not export the stamped PDF. Please use the Export → PDF button in the viewer toolbar as a fallback.') + console.error('Failed to trigger stamped PDF export:', e); + alert( + 'Could not export the stamped PDF. Please use the Export → PDF button in the viewer toolbar as a fallback.' + ); } - }) + }); } if (backToToolsBtn) { backToToolsBtn.addEventListener('click', () => { - window.location.href = import.meta.env.BASE_URL - }) + window.location.href = import.meta.env.BASE_URL; + }); } -initializeGlobalShortcuts() \ No newline at end of file +initializeGlobalShortcuts(); diff --git a/src/js/logic/add-watermark-page.ts b/src/js/logic/add-watermark-page.ts index 38d0366..59714b0 100644 --- a/src/js/logic/add-watermark-page.ts +++ b/src/js/logic/add-watermark-page.ts @@ -14,6 +14,7 @@ import { } from '../utils/pdf-operations.js'; import { AddWatermarkState, PageWatermarkConfig } from '@/types'; import * as pdfjsLib from 'pdfjs-dist'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -115,16 +116,16 @@ async function handleFiles(files: FileList) { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; } - showLoader('Loading PDF...'); try { - const arrayBuffer = await file.arrayBuffer(); - const pdfBytes = new Uint8Array(arrayBuffer); + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; + showLoader('Loading PDF...'); + const pdfBytes = new Uint8Array(result.bytes); pageState.pdfDoc = await PDFLibDocument.load(pdfBytes); - pageState.file = file; + pageState.file = result.file; pageState.pdfBytes = pdfBytes; - cachedPdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes.slice() }) - .promise; + cachedPdfjsDoc = result.pdf; totalPageCount = cachedPdfjsDoc.numPages; currentPageNum = 1; pageWatermarks.clear(); diff --git a/src/js/logic/adjust-colors-page.ts b/src/js/logic/adjust-colors-page.ts index e46d4b1..5d36c49 100644 --- a/src/js/logic/adjust-colors-page.ts +++ b/src/js/logic/adjust-colors-page.ts @@ -11,6 +11,7 @@ import { applyColorAdjustments } from '../utils/image-effects.js'; import * as pdfjsLib from 'pdfjs-dist'; import type { AdjustColorsSettings } from '../types/adjust-colors-type.js'; import { t } from '../i18n/i18n'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -357,13 +358,13 @@ document.addEventListener('DOMContentLoaded', () => { return; } - files = [validFiles[0]]; - updateUI(); - - showLoader('Loading preview...'); try { - const buffer = await readFileAsArrayBuffer(validFiles[0]); - pdfjsDoc = await getPDFDocument({ data: buffer }).promise; + const result = await loadPdfWithPasswordPrompt(validFiles[0]); + if (!result) return; + showLoader('Loading preview...'); + files = [result.file]; + updateUI(); + pdfjsDoc = result.pdf; await renderPreview(); } catch (e) { console.error(e); diff --git a/src/js/logic/alternate-merge-page.ts b/src/js/logic/alternate-merge-page.ts index 9151789..bdc6b5b 100644 --- a/src/js/logic/alternate-merge-page.ts +++ b/src/js/logic/alternate-merge-page.ts @@ -9,6 +9,7 @@ import { showWasmRequiredDialog, WasmProvider, } from '../utils/wasm-provider.js'; +import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; const pageState: AlternateMergeState = { files: [], @@ -69,20 +70,21 @@ async function updateUI() { createIcons({ icons }); // Load PDFs and populate list + hideLoader(); + pageState.files = await batchDecryptIfNeeded(pageState.files); showLoader('Loading PDF files...'); fileList.innerHTML = ''; try { for (let i = 0; i < pageState.files.length; i++) { const file = pageState.files[i]; - const fileKey = makeUniqueFileKey(i, file.name); - const arrayBuffer = await file.arrayBuffer(); - pageState.pdfBytes.set(fileKey, arrayBuffer); - const bytesForPdfJs = arrayBuffer.slice(0); - const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise; - pageState.pdfDocs.set(fileKey, pdfjsDoc); - const pageCount = pdfjsDoc.numPages; + const fileKey = makeUniqueFileKey(i, file.name); + const bytes = await file.arrayBuffer(); + const pdf = await getPDFDocument({ data: bytes.slice(0) }).promise; + pageState.pdfBytes.set(fileKey, bytes); + pageState.pdfDocs.set(fileKey, pdf); + const pageCount = pdf.numPages; const li = document.createElement('li'); li.className = diff --git a/src/js/logic/background-color-page.ts b/src/js/logic/background-color-page.ts index da08bb9..1b9579b 100644 --- a/src/js/logic/background-color-page.ts +++ b/src/js/logic/background-color-page.ts @@ -3,102 +3,156 @@ import { showAlert, showLoader, hideLoader } from '../ui.js'; import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js'; import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib'; import { BackgroundColorState } from '@/types'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; const pageState: BackgroundColorState = { file: null, pdfDoc: null }; -if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializePage); } -else { initializePage(); } - -function initializePage() { - createIcons({ icons }); - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const backBtn = document.getElementById('back-to-tools'); - const processBtn = document.getElementById('process-btn'); - - if (fileInput) { - fileInput.addEventListener('change', handleFileUpload); - fileInput.addEventListener('click', () => { fileInput.value = ''; }); - } - if (dropZone) { - dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-indigo-500'); }); - dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('border-indigo-500'); }); - dropZone.addEventListener('drop', (e) => { - e.preventDefault(); dropZone.classList.remove('border-indigo-500'); - if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files); - }); - } - if (backBtn) backBtn.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; }); - if (processBtn) processBtn.addEventListener('click', changeBackgroundColor); +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializePage); +} else { + initializePage(); } -function handleFileUpload(e: Event) { const input = e.target as HTMLInputElement; if (input.files?.length) handleFiles(input.files); } +function initializePage() { + createIcons({ icons }); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const backBtn = document.getElementById('back-to-tools'); + const processBtn = document.getElementById('process-btn'); + + if (fileInput) { + fileInput.addEventListener('change', handleFileUpload); + fileInput.addEventListener('click', () => { + fileInput.value = ''; + }); + } + if (dropZone) { + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('border-indigo-500'); + }); + dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('border-indigo-500'); + }); + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('border-indigo-500'); + if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files); + }); + } + if (backBtn) + backBtn.addEventListener('click', () => { + window.location.href = import.meta.env.BASE_URL; + }); + if (processBtn) processBtn.addEventListener('click', changeBackgroundColor); +} + +function handleFileUpload(e: Event) { + const input = e.target as HTMLInputElement; + if (input.files?.length) handleFiles(input.files); +} async function handleFiles(files: FileList) { - const file = files[0]; - if (!file || file.type !== 'application/pdf') { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; } + const file = files[0]; + if (!file || file.type !== 'application/pdf') { + showAlert('Invalid File', 'Please upload a valid PDF file.'); + return; + } + try { + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; showLoader('Loading PDF...'); - try { - const arrayBuffer = await file.arrayBuffer(); - pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer); - pageState.file = file; - updateFileDisplay(); - document.getElementById('options-panel')?.classList.remove('hidden'); - } catch (error) { console.error(error); showAlert('Error', 'Failed to load PDF file.'); } - finally { hideLoader(); } + result.pdf.destroy(); + pageState.pdfDoc = await PDFLibDocument.load(result.bytes); + pageState.file = result.file; + updateFileDisplay(); + document.getElementById('options-panel')?.classList.remove('hidden'); + } catch (error) { + console.error(error); + showAlert('Error', 'Failed to load PDF file.'); + } finally { + hideLoader(); + } } function updateFileDisplay() { - const fileDisplayArea = document.getElementById('file-display-area'); - if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return; - fileDisplayArea.innerHTML = ''; - const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; - const infoContainer = document.createElement('div'); - infoContainer.className = 'flex flex-col flex-1 min-w-0'; - const nameSpan = document.createElement('div'); - nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; - nameSpan.textContent = pageState.file.name; - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`; - 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 = resetState; - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return; + fileDisplayArea.innerHTML = ''; + const fileDiv = document.createElement('div'); + fileDiv.className = + 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col flex-1 min-w-0'; + const nameSpan = document.createElement('div'); + nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; + nameSpan.textContent = pageState.file.name; + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`; + 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 = resetState; + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); } function resetState() { - pageState.file = null; pageState.pdfDoc = null; - const fileDisplayArea = document.getElementById('file-display-area'); - if (fileDisplayArea) fileDisplayArea.innerHTML = ''; - document.getElementById('options-panel')?.classList.add('hidden'); - const fileInput = document.getElementById('file-input') as HTMLInputElement; - if (fileInput) fileInput.value = ''; + pageState.file = null; + pageState.pdfDoc = null; + const fileDisplayArea = document.getElementById('file-display-area'); + if (fileDisplayArea) fileDisplayArea.innerHTML = ''; + document.getElementById('options-panel')?.classList.add('hidden'); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; } async function changeBackgroundColor() { - if (!pageState.pdfDoc) { showAlert('Error', 'Please upload a PDF file first.'); return; } - const colorHex = (document.getElementById('background-color') as HTMLInputElement).value; - const color = hexToRgb(colorHex); - showLoader('Changing background color...'); - try { - const newPdfDoc = await PDFLibDocument.create(); - for (let i = 0; i < pageState.pdfDoc.getPageCount(); i++) { - const [originalPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]); - const { width, height } = originalPage.getSize(); - const newPage = newPdfDoc.addPage([width, height]); - newPage.drawRectangle({ x: 0, y: 0, width, height, color: rgb(color.r, color.g, color.b) }); - const embeddedPage = await newPdfDoc.embedPage(originalPage); - newPage.drawPage(embeddedPage, { x: 0, y: 0, width, height }); - } - const newPdfBytes = await newPdfDoc.save(); - downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'background-changed.pdf'); - showAlert('Success', 'Background color changed successfully!', 'success', () => { resetState(); }); - } catch (e) { console.error(e); showAlert('Error', 'Could not change the background color.'); } - finally { hideLoader(); } + if (!pageState.pdfDoc) { + showAlert('Error', 'Please upload a PDF file first.'); + return; + } + const colorHex = ( + document.getElementById('background-color') as HTMLInputElement + ).value; + const color = hexToRgb(colorHex); + showLoader('Changing background color...'); + try { + const newPdfDoc = await PDFLibDocument.create(); + for (let i = 0; i < pageState.pdfDoc.getPageCount(); i++) { + const [originalPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]); + const { width, height } = originalPage.getSize(); + const newPage = newPdfDoc.addPage([width, height]); + newPage.drawRectangle({ + x: 0, + y: 0, + width, + height, + color: rgb(color.r, color.g, color.b), + }); + const embeddedPage = await newPdfDoc.embedPage(originalPage); + newPage.drawPage(embeddedPage, { x: 0, y: 0, width, height }); + } + const newPdfBytes = await newPdfDoc.save(); + downloadFile( + new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), + 'background-changed.pdf' + ); + showAlert( + 'Success', + 'Background color changed successfully!', + 'success', + () => { + resetState(); + } + ); + } catch (e) { + console.error(e); + showAlert('Error', 'Could not change the background color.'); + } finally { + hideLoader(); + } } diff --git a/src/js/logic/bates-numbering-page.ts b/src/js/logic/bates-numbering-page.ts index fe093ac..df6ae5d 100644 --- a/src/js/logic/bates-numbering-page.ts +++ b/src/js/logic/bates-numbering-page.ts @@ -2,6 +2,7 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js'; import { PDFDocument, StandardFonts, rgb } from 'pdf-lib'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import JSZip from 'jszip'; import Sortable from 'sortablejs'; import { FileEntry, Position, StylePreset } from '@/types'; @@ -178,9 +179,13 @@ async function handleFiles(fileList: FileList) { try { for (const file of Array.from(fileList)) { if (file.type !== 'application/pdf') continue; - const arrayBuffer = await file.arrayBuffer(); - const pdfDoc = await PDFDocument.load(arrayBuffer); - files.push({ file, pageCount: pdfDoc.getPageCount() }); + hideLoader(); + const result = await loadPdfWithPasswordPrompt(file); + if (!result) continue; + showLoader('Loading PDFs...'); + result.pdf.destroy(); + const pdfDoc = await PDFDocument.load(result.bytes); + files.push({ file: result.file, pageCount: pdfDoc.getPageCount() }); } if (files.length === 0) { diff --git a/src/js/logic/bookmark-pdf.ts b/src/js/logic/bookmark-pdf.ts index c8c3e94..5d230aa 100644 --- a/src/js/logic/bookmark-pdf.ts +++ b/src/js/logic/bookmark-pdf.ts @@ -13,6 +13,7 @@ import { escapeHtml, hexToRgb, } from '../utils/helpers.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { BookmarkNode, BookmarkTree, @@ -1223,7 +1224,14 @@ async function loadPDF(e?: Event): Promise { if (filenameDisplay) filenameDisplay.textContent = truncateFilename(file.name); renderFileDisplay(file); - const arrayBuffer = await file.arrayBuffer(); + + loaderModal?.classList.add('hidden'); + const result = await loadPdfWithPasswordPrompt(file); + if (!result) { + loaderModal?.classList.add('hidden'); + return; + } + loaderModal?.classList.remove('hidden'); currentPage = 1; bookmarkTree = []; @@ -1232,12 +1240,8 @@ async function loadPDF(e?: Event): Promise { selectedBookmarks.clear(); collapsedNodes.clear(); - pdfLibDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryption: true }); - - const loadingTask = getPDFDocument({ - data: new Uint8Array(arrayBuffer), - }); - pdfJsDoc = await loadingTask.promise; + pdfLibDoc = await PDFDocument.load(result.bytes, { ignoreEncryption: true }); + pdfJsDoc = result.pdf; if (gotoPageInput) gotoPageInput.max = String(pdfJsDoc.numPages); diff --git a/src/js/logic/combine-single-page-page.ts b/src/js/logic/combine-single-page-page.ts index e6a89df..a9fa74c 100644 --- a/src/js/logic/combine-single-page-page.ts +++ b/src/js/logic/combine-single-page-page.ts @@ -1,309 +1,349 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { downloadFile, formatBytes, hexToRgb, getPDFDocument } from '../utils/helpers.js'; +import { + downloadFile, + formatBytes, + hexToRgb, + getPDFDocument, +} from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib'; import * as pdfjsLib from 'pdfjs-dist'; import { CombineSinglePageState } from '@/types'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; -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(); const pageState: CombineSinglePageState = { - file: null, - pdfDoc: null, + file: null, + pdfDoc: null, }; function resetState() { - pageState.file = null; - pageState.pdfDoc = null; + pageState.file = null; + pageState.pdfDoc = null; - 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 fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.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'); - 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.file) { + 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 = pageState.file.name; - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`; + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`; - 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 () { + resetState(); + }; - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); - try { - showLoader('Loading PDF...'); - const arrayBuffer = await pageState.file.arrayBuffer(); - pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, { - ignoreEncryption: true, - throwOnInvalidObject: false - }); - hideLoader(); + try { + const result = await loadPdfWithPasswordPrompt(pageState.file); + if (!result) { + resetState(); + return; + } + showLoader('Loading PDF...'); + result.pdf.destroy(); + pageState.file = result.file; + pageState.pdfDoc = await PDFLibDocument.load(result.bytes, { + throwOnInvalidObject: false, + }); + hideLoader(); - const pageCount = pageState.pdfDoc.getPageCount(); - metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`; + const pageCount = pageState.pdfDoc.getPageCount(); + metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`; - if (toolOptions) toolOptions.classList.remove('hidden'); - } catch (error) { - console.error('Error loading PDF:', error); - hideLoader(); - showAlert('Error', 'Failed to load PDF file.'); - resetState(); - } - } else { - if (toolOptions) toolOptions.classList.add('hidden'); + if (toolOptions) toolOptions.classList.remove('hidden'); + } catch (error) { + console.error('Error loading PDF:', error); + hideLoader(); + showAlert('Error', 'Failed to load PDF file.'); + resetState(); } + } else { + if (toolOptions) toolOptions.classList.add('hidden'); + } } async function combineToSinglePage() { - if (!pageState.pdfDoc || !pageState.file) { - showAlert('Error', 'Please upload a PDF first.'); - return; + if (!pageState.pdfDoc || !pageState.file) { + showAlert('Error', 'Please upload a PDF first.'); + return; + } + + const orientation = ( + document.getElementById('combine-orientation') as HTMLSelectElement + ).value; + const spacing = + parseInt( + (document.getElementById('page-spacing') as HTMLInputElement).value + ) || 0; + const backgroundColorHex = ( + document.getElementById('background-color') as HTMLInputElement + ).value; + const addSeparator = ( + document.getElementById('add-separator') as HTMLInputElement + ).checked; + const separatorThickness = + parseFloat( + (document.getElementById('separator-thickness') as HTMLInputElement).value + ) || 0.5; + const separatorColorHex = ( + document.getElementById('separator-color') as HTMLInputElement + ).value; + + const backgroundColor = hexToRgb(backgroundColorHex); + const separatorColor = hexToRgb(separatorColorHex); + + showLoader('Combining pages...'); + + try { + const sourceDoc = pageState.pdfDoc; + const newDoc = await PDFLibDocument.create(); + + const pdfBytes = await sourceDoc.save(); + const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise; + + const sourcePages = sourceDoc.getPages(); + let maxWidth = 0; + let maxHeight = 0; + let totalWidth = 0; + let totalHeight = 0; + + sourcePages.forEach(function (page) { + const { width, height } = page.getSize(); + if (width > maxWidth) maxWidth = width; + if (height > maxHeight) maxHeight = height; + totalWidth += width; + totalHeight += height; + }); + + let finalWidth: number, finalHeight: number; + if (orientation === 'horizontal') { + finalWidth = totalWidth + Math.max(0, sourcePages.length - 1) * spacing; + finalHeight = maxHeight; + } else { + finalWidth = maxWidth; + finalHeight = totalHeight + Math.max(0, sourcePages.length - 1) * spacing; } - const orientation = (document.getElementById('combine-orientation') as HTMLSelectElement).value; - const spacing = parseInt((document.getElementById('page-spacing') as HTMLInputElement).value) || 0; - const backgroundColorHex = (document.getElementById('background-color') as HTMLInputElement).value; - const addSeparator = (document.getElementById('add-separator') as HTMLInputElement).checked; - const separatorThickness = parseFloat((document.getElementById('separator-thickness') as HTMLInputElement).value) || 0.5; - const separatorColorHex = (document.getElementById('separator-color') as HTMLInputElement).value; + const newPage = newDoc.addPage([finalWidth, finalHeight]); - const backgroundColor = hexToRgb(backgroundColorHex); - const separatorColor = hexToRgb(separatorColorHex); + if (backgroundColorHex.toUpperCase() !== '#FFFFFF') { + newPage.drawRectangle({ + x: 0, + y: 0, + width: finalWidth, + height: finalHeight, + color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b), + }); + } - showLoader('Combining pages...'); + let currentX = 0; + let currentY = finalHeight; - try { - const sourceDoc = pageState.pdfDoc; - const newDoc = await PDFLibDocument.create(); + for (let i = 0; i < sourcePages.length; i++) { + showLoader(`Processing page ${i + 1} of ${sourcePages.length}...`); + const sourcePage = sourcePages[i]; + const { width, height } = sourcePage.getSize(); - const pdfBytes = await sourceDoc.save(); - const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise; + try { + const page = await pdfjsDoc.getPage(i + 1); + const scale = 2.0; + const viewport = page.getViewport({ scale }); - const sourcePages = sourceDoc.getPages(); - let maxWidth = 0; - let maxHeight = 0; - let totalWidth = 0; - let totalHeight = 0; + const canvas = document.createElement('canvas'); + canvas.width = viewport.width; + canvas.height = viewport.height; + const context = canvas.getContext('2d')!; - sourcePages.forEach(function (page) { - const { width, height } = page.getSize(); - if (width > maxWidth) maxWidth = width; - if (height > maxHeight) maxHeight = height; - totalWidth += width; - totalHeight += height; - }); + await page.render({ + canvasContext: context, + viewport, + canvas, + }).promise; + + const pngDataUrl = canvas.toDataURL('image/png'); + const pngImage = await newDoc.embedPng(pngDataUrl); - let finalWidth: number, finalHeight: number; if (orientation === 'horizontal') { - finalWidth = totalWidth + Math.max(0, sourcePages.length - 1) * spacing; - finalHeight = maxHeight; + const y = (finalHeight - height) / 2; + newPage.drawImage(pngImage, { x: currentX, y, width, height }); } else { - finalWidth = maxWidth; - finalHeight = totalHeight + Math.max(0, sourcePages.length - 1) * spacing; + currentY -= height; + const x = (finalWidth - width) / 2; + newPage.drawImage(pngImage, { x, y: currentY, width, height }); } + } catch (renderError) { + console.warn(`Failed to render page ${i + 1}:`, renderError); + } - const newPage = newDoc.addPage([finalWidth, finalHeight]); - - if (backgroundColorHex.toUpperCase() !== '#FFFFFF') { - newPage.drawRectangle({ - x: 0, - y: 0, - width: finalWidth, - height: finalHeight, - color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b), - }); + if (addSeparator && i < sourcePages.length - 1) { + if (orientation === 'horizontal') { + const lineX = currentX + width + spacing / 2; + newPage.drawLine({ + start: { x: lineX, y: 0 }, + end: { x: lineX, y: finalHeight }, + thickness: separatorThickness, + color: rgb(separatorColor.r, separatorColor.g, separatorColor.b), + }); + currentX += width + spacing; + } else { + const lineY = currentY - spacing / 2; + newPage.drawLine({ + start: { x: 0, y: lineY }, + end: { x: finalWidth, y: lineY }, + thickness: separatorThickness, + color: rgb(separatorColor.r, separatorColor.g, separatorColor.b), + }); + currentY -= spacing; } - - let currentX = 0; - let currentY = finalHeight; - - for (let i = 0; i < sourcePages.length; i++) { - showLoader(`Processing page ${i + 1} of ${sourcePages.length}...`); - const sourcePage = sourcePages[i]; - const { width, height } = sourcePage.getSize(); - - try { - const page = await pdfjsDoc.getPage(i + 1); - const scale = 2.0; - const viewport = page.getViewport({ scale }); - - const canvas = document.createElement('canvas'); - canvas.width = viewport.width; - canvas.height = viewport.height; - const context = canvas.getContext('2d')!; - - await page.render({ - canvasContext: context, - viewport, - canvas - }).promise; - - const pngDataUrl = canvas.toDataURL('image/png'); - const pngImage = await newDoc.embedPng(pngDataUrl); - - if (orientation === 'horizontal') { - const y = (finalHeight - height) / 2; - newPage.drawImage(pngImage, { x: currentX, y, width, height }); - } else { - currentY -= height; - const x = (finalWidth - width) / 2; - newPage.drawImage(pngImage, { x, y: currentY, width, height }); - } - } catch (renderError) { - console.warn(`Failed to render page ${i + 1}:`, renderError); - } - - if (addSeparator && i < sourcePages.length - 1) { - if (orientation === 'horizontal') { - const lineX = currentX + width + spacing / 2; - newPage.drawLine({ - start: { x: lineX, y: 0 }, - end: { x: lineX, y: finalHeight }, - thickness: separatorThickness, - color: rgb(separatorColor.r, separatorColor.g, separatorColor.b), - }); - currentX += width + spacing; - } else { - const lineY = currentY - spacing / 2; - newPage.drawLine({ - start: { x: 0, y: lineY }, - end: { x: finalWidth, y: lineY }, - thickness: separatorThickness, - color: rgb(separatorColor.r, separatorColor.g, separatorColor.b), - }); - currentY -= spacing; - } - } else { - if (orientation === 'horizontal') { - currentX += width + spacing; - } else { - currentY -= spacing; - } - } + } else { + if (orientation === 'horizontal') { + currentX += width + spacing; + } else { + currentY -= spacing; } - - const newPdfBytes = await newDoc.save(); - const originalName = pageState.file.name.replace(/\.pdf$/i, ''); - - downloadFile( - new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), - `${originalName}_combined.pdf` - ); - - showAlert('Success', 'Pages combined successfully!', 'success', function () { - resetState(); - }); - } catch (e) { - console.error(e); - showAlert('Error', 'An error occurred while combining pages.'); - } finally { - hideLoader(); + } } + + const newPdfBytes = await newDoc.save(); + const originalName = pageState.file.name.replace(/\.pdf$/i, ''); + + downloadFile( + new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), + `${originalName}_combined.pdf` + ); + + showAlert( + 'Success', + 'Pages combined successfully!', + 'success', + function () { + resetState(); + } + ); + } catch (e) { + console.error(e); + showAlert('Error', 'An error occurred while combining pages.'); + } finally { + hideLoader(); + } } 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 file = files[0]; + if ( + file.type === 'application/pdf' || + file.name.toLowerCase().endsWith('.pdf') + ) { + pageState.file = file; + updateUI(); } + } } 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 addSeparatorCheckbox = document.getElementById('add-separator'); - const separatorOptions = document.getElementById('separator-options'); + 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 addSeparatorCheckbox = document.getElementById('add-separator'); + const separatorOptions = document.getElementById('separator-options'); - 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 (addSeparatorCheckbox && separatorOptions) { + addSeparatorCheckbox.addEventListener('change', function () { + if ((addSeparatorCheckbox as HTMLInputElement).checked) { + separatorOptions.classList.remove('hidden'); + } else { + separatorOptions.classList.add('hidden'); + } + }); + } + + 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('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); + } + } + }); - if (addSeparatorCheckbox && separatorOptions) { - addSeparatorCheckbox.addEventListener('change', function () { - if ((addSeparatorCheckbox as HTMLInputElement).checked) { - separatorOptions.classList.remove('hidden'); - } else { - separatorOptions.classList.add('hidden'); - } - }); - } + fileInput.addEventListener('click', function () { + fileInput.value = ''; + }); + } - 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('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); - } - } - }); - - fileInput.addEventListener('click', function () { - fileInput.value = ''; - }); - } - - if (processBtn) { - processBtn.addEventListener('click', combineToSinglePage); - } + if (processBtn) { + processBtn.addEventListener('click', combineToSinglePage); + } }); diff --git a/src/js/logic/compare-pdfs-page.ts b/src/js/logic/compare-pdfs-page.ts index 80c1ad3..d06d92d 100644 --- a/src/js/logic/compare-pdfs-page.ts +++ b/src/js/logic/compare-pdfs-page.ts @@ -1,5 +1,5 @@ import { showLoader, hideLoader, showAlert } from '../ui.ts'; -import { getPDFDocument } from '../utils/helpers.ts'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { icons, createIcons } from 'lucide'; import * as pdfjsLib from 'pdfjs-dist'; import { CompareState } from '@/types'; @@ -745,9 +745,11 @@ async function handleFileInput( } try { - showLoader(`Loading ${file.name}...`); - const arrayBuffer = await file.arrayBuffer(); - pageState[docKey] = await getPDFDocument({ data: arrayBuffer }).promise; + hideLoader(); + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; + showLoader(`Loading ${result.file.name}...`); + pageState[docKey] = result.pdf; caches.pageModelCache.clear(); caches.comparisonCache.clear(); caches.comparisonResultsCache.clear(); diff --git a/src/js/logic/compress-pdf-page.ts b/src/js/logic/compress-pdf-page.ts index f4d8cd2..1fabe77 100644 --- a/src/js/logic/compress-pdf-page.ts +++ b/src/js/logic/compress-pdf-page.ts @@ -5,12 +5,14 @@ import { formatBytes, getPDFDocument, } from '../utils/helpers.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { state } from '../state.js'; import { PDFDocument } from 'pdf-lib'; import { createIcons, icons } from 'lucide'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; import * as pdfjsLib from 'pdfjs-dist'; +import type { PDFDocumentProxy } from 'pdfjs-dist'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -120,15 +122,27 @@ async function performCondenseCompression( return { ...result, usedFallback: true }; } - throw new Error(`PDF compression failed: ${errorMessage}`); + throw new Error(`PDF compression failed: ${errorMessage}`, { + cause: error, + }); } } async function performPhotonCompression( arrayBuffer: ArrayBuffer, - level: string + level: string, + file?: File ) { - const pdfJsDoc = await getPDFDocument({ data: arrayBuffer }).promise; + let pdfJsDoc: PDFDocumentProxy; + if (file) { + hideLoader(); + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return null; + showLoader('Running Photon compression...'); + pdfJsDoc = result.pdf; + } else { + pdfJsDoc = await getPDFDocument({ data: arrayBuffer }).promise; + } const newPdfDoc = await PDFDocument.create(); const settings = PHOTON_PRESETS[level as keyof typeof PHOTON_PRESETS] || @@ -429,8 +443,10 @@ document.addEventListener('DOMContentLoaded', () => { )) as ArrayBuffer; const resultBytes = await performPhotonCompression( arrayBuffer, - level + level, + originalFile ); + if (!resultBytes) return; const buffer = resultBytes.buffer.slice( resultBytes.byteOffset, resultBytes.byteOffset + resultBytes.byteLength @@ -494,7 +510,13 @@ document.addEventListener('DOMContentLoaded', () => { const arrayBuffer = (await readFileAsArrayBuffer( file )) as ArrayBuffer; - resultBytes = await performPhotonCompression(arrayBuffer, level); + const photonResult = await performPhotonCompression( + arrayBuffer, + level, + file + ); + if (!photonResult) return; + resultBytes = photonResult; } totalCompressedSize += resultBytes.length; diff --git a/src/js/logic/crop-pdf-page.ts b/src/js/logic/crop-pdf-page.ts index c509b24..582a373 100644 --- a/src/js/logic/crop-pdf-page.ts +++ b/src/js/logic/crop-pdf-page.ts @@ -1,11 +1,7 @@ import { createIcons, icons } from 'lucide'; import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { - downloadFile, - readFileAsArrayBuffer, - formatBytes, - getPDFDocument, -} from '../utils/helpers.js'; +import { downloadFile, formatBytes } from '../utils/helpers.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import Cropper from 'cropperjs'; import * as pdfjsLib from 'pdfjs-dist'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; @@ -88,16 +84,19 @@ async function handleFile(file: File) { return; } - showLoader('Loading PDF...'); cropperState.file = file; cropperState.pageCrops = {}; try { - const arrayBuffer = await readFileAsArrayBuffer(file); - cropperState.originalPdfBytes = arrayBuffer as ArrayBuffer; - cropperState.pdfDoc = await getPDFDocument({ - data: (arrayBuffer as ArrayBuffer).slice(0), - }).promise; + const result = await loadPdfWithPasswordPrompt(file); + if (!result) { + cropperState.file = null; + return; + } + showLoader('Loading PDF...'); + cropperState.file = result.file; + cropperState.originalPdfBytes = result.bytes; + cropperState.pdfDoc = result.pdf; cropperState.currentPageNum = 1; updateFileDisplay(); @@ -308,7 +307,7 @@ async function performMetadataCrop( ): Promise { const pdfToModify = await PDFLibDocument.load( cropperState.originalPdfBytes!, - { ignoreEncryption: true, throwOnInvalidObject: false } + { throwOnInvalidObject: false } ); for (const pageNum in cropData) { @@ -352,7 +351,7 @@ async function performFlatteningCrop( const newPdfDoc = await PDFLibDocument.create(); const sourcePdfDocForCopying = await PDFLibDocument.load( cropperState.originalPdfBytes!, - { ignoreEncryption: true, throwOnInvalidObject: false } + { throwOnInvalidObject: false } ); const totalPages = cropperState.pdfDoc.numPages; diff --git a/src/js/logic/delete-pages-page.ts b/src/js/logic/delete-pages-page.ts index 68c576b..59f831e 100644 --- a/src/js/logic/delete-pages-page.ts +++ b/src/js/logic/delete-pages-page.ts @@ -1,13 +1,12 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; import { - readFileAsArrayBuffer, formatBytes, downloadFile, - getPDFDocument, parsePageRanges, } from '../utils/helpers.js'; import { PDFDocument } from 'pdf-lib'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { deletePdfPages } from '../utils/pdf-operations.js'; import * as pdfjsLib from 'pdfjs-dist'; import { DeletePagesState } from '@/types'; @@ -85,18 +84,20 @@ async function handleFile(file: File) { return; } - showLoader('Loading PDF...'); deleteState.file = file; try { - const arrayBuffer = await readFileAsArrayBuffer(file); - deleteState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { - ignoreEncryption: true, + const result = await loadPdfWithPasswordPrompt(file); + if (!result) { + deleteState.file = null; + return; + } + showLoader('Loading PDF...'); + deleteState.file = result.file; + deleteState.pdfDoc = await PDFDocument.load(result.bytes, { throwOnInvalidObject: false, }); - deleteState.pdfJsDoc = await getPDFDocument({ - data: (arrayBuffer as ArrayBuffer).slice(0), - }).promise; + deleteState.pdfJsDoc = result.pdf; deleteState.totalPages = deleteState.pdfDoc.getPageCount(); deleteState.pagesToDelete = new Set(); diff --git a/src/js/logic/deskew-pdf-page.ts b/src/js/logic/deskew-pdf-page.ts index d609a25..e590d12 100644 --- a/src/js/logic/deskew-pdf-page.ts +++ b/src/js/logic/deskew-pdf-page.ts @@ -1,6 +1,7 @@ import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; import { createIcons, icons } from 'lucide'; import { downloadFile } from '../utils/helpers'; @@ -151,6 +152,8 @@ async function processDeskew(): Promise { const threshold = parseFloat(thresholdSelect?.value || '0.5'); const dpi = parseInt(dpiSelect?.value || '150', 10); + selectedFiles = await batchDecryptIfNeeded(selectedFiles); + showLoader('Initializing PyMuPDF...'); try { diff --git a/src/js/logic/digital-sign-pdf-page.ts b/src/js/logic/digital-sign-pdf-page.ts index 1b2d607..c597e9d 100644 --- a/src/js/logic/digital-sign-pdf-page.ts +++ b/src/js/logic/digital-sign-pdf-page.ts @@ -1,690 +1,782 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; -import { readFileAsArrayBuffer, formatBytes, downloadFile, getPDFDocument } from '../utils/helpers.js'; +import { + readFileAsArrayBuffer, + formatBytes, + downloadFile, +} from '../utils/helpers.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { t } from '../i18n/i18n'; import { - signPdf, - parsePfxFile, - parseCombinedPem, - getCertificateInfo, + signPdf, + parsePfxFile, + parseCombinedPem, + getCertificateInfo, } from './digital-sign-pdf.js'; -import { SignatureInfo, VisibleSignatureOptions, DigitalSignState } from '@/types'; +import { + SignatureInfo, + VisibleSignatureOptions, + DigitalSignState, +} from '@/types'; const state: DigitalSignState = { - pdfFile: null, - pdfBytes: null, - certFile: null, - certData: null, - sigImageData: null, - sigImageType: null, + pdfFile: null, + pdfBytes: null, + certFile: null, + certData: null, + sigImageData: null, + sigImageType: null, }; function resetState(): void { - state.pdfFile = null; - state.pdfBytes = null; - state.certFile = null; - state.certData = null; - state.sigImageData = null; - state.sigImageType = null; + state.pdfFile = null; + state.pdfBytes = null; + state.certFile = null; + state.certData = null; + state.sigImageData = null; + state.sigImageType = null; - const fileDisplayArea = getElement('file-display-area'); - if (fileDisplayArea) fileDisplayArea.innerHTML = ''; + const fileDisplayArea = getElement('file-display-area'); + if (fileDisplayArea) fileDisplayArea.innerHTML = ''; - const certDisplayArea = getElement('cert-display-area'); - if (certDisplayArea) certDisplayArea.innerHTML = ''; + const certDisplayArea = getElement('cert-display-area'); + if (certDisplayArea) certDisplayArea.innerHTML = ''; - const fileInput = getElement('file-input'); - if (fileInput) fileInput.value = ''; + const fileInput = getElement('file-input'); + if (fileInput) fileInput.value = ''; - const certInput = getElement('cert-input'); - if (certInput) certInput.value = ''; + const certInput = getElement('cert-input'); + if (certInput) certInput.value = ''; - const sigImageInput = getElement('sig-image-input'); - if (sigImageInput) sigImageInput.value = ''; + const sigImageInput = getElement('sig-image-input'); + if (sigImageInput) sigImageInput.value = ''; - const sigImagePreview = getElement('sig-image-preview'); - if (sigImagePreview) sigImagePreview.classList.add('hidden'); + const sigImagePreview = getElement('sig-image-preview'); + if (sigImagePreview) sigImagePreview.classList.add('hidden'); - const certSection = getElement('certificate-section'); - if (certSection) certSection.classList.add('hidden'); + const certSection = getElement('certificate-section'); + if (certSection) certSection.classList.add('hidden'); - hidePasswordSection(); - hideSignatureOptions(); - hideCertInfo(); - updateProcessButton(); + hidePasswordSection(); + hideSignatureOptions(); + hideCertInfo(); + updateProcessButton(); } function getElement(id: string): T | null { - return document.getElementById(id) as T | null; + return document.getElementById(id) as T | null; } function initializePage(): void { - createIcons({ icons }); + createIcons({ icons }); - const fileInput = getElement('file-input'); - const dropZone = getElement('drop-zone'); - const certInput = getElement('cert-input'); - const certDropZone = getElement('cert-drop-zone'); - const certPassword = getElement('cert-password'); - const processBtn = getElement('process-btn'); - const backBtn = getElement('back-to-tools'); + const fileInput = getElement('file-input'); + const dropZone = getElement('drop-zone'); + const certInput = getElement('cert-input'); + const certDropZone = getElement('cert-drop-zone'); + const certPassword = getElement('cert-password'); + const processBtn = getElement('process-btn'); + const backBtn = getElement('back-to-tools'); - if (fileInput) { - fileInput.addEventListener('change', handlePdfUpload); - fileInput.addEventListener('click', () => { - fileInput.value = ''; - }); - } + if (fileInput) { + fileInput.addEventListener('change', handlePdfUpload); + fileInput.addEventListener('click', () => { + fileInput.value = ''; + }); + } - if (dropZone) { - dropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - dropZone.classList.add('bg-gray-700'); - }); + if (dropZone) { + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); + }); - dropZone.addEventListener('dragleave', () => { - dropZone.classList.remove('bg-gray-700'); - }); + dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('bg-gray-700'); + }); - dropZone.addEventListener('drop', (e) => { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - const droppedFiles = e.dataTransfer?.files; - if (droppedFiles && droppedFiles.length > 0) { - handlePdfFile(droppedFiles[0]); - } - }); - } + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + const droppedFiles = e.dataTransfer?.files; + if (droppedFiles && droppedFiles.length > 0) { + handlePdfFile(droppedFiles[0]); + } + }); + } - if (certInput) { - certInput.addEventListener('change', handleCertUpload); - certInput.addEventListener('click', () => { - certInput.value = ''; - }); - } + if (certInput) { + certInput.addEventListener('change', handleCertUpload); + certInput.addEventListener('click', () => { + certInput.value = ''; + }); + } - if (certDropZone) { - certDropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - certDropZone.classList.add('bg-gray-700'); - }); + if (certDropZone) { + certDropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + certDropZone.classList.add('bg-gray-700'); + }); - certDropZone.addEventListener('dragleave', () => { - certDropZone.classList.remove('bg-gray-700'); - }); + certDropZone.addEventListener('dragleave', () => { + certDropZone.classList.remove('bg-gray-700'); + }); - certDropZone.addEventListener('drop', (e) => { - e.preventDefault(); - certDropZone.classList.remove('bg-gray-700'); - const droppedFiles = e.dataTransfer?.files; - if (droppedFiles && droppedFiles.length > 0) { - handleCertFile(droppedFiles[0]); - } - }); - } + certDropZone.addEventListener('drop', (e) => { + e.preventDefault(); + certDropZone.classList.remove('bg-gray-700'); + const droppedFiles = e.dataTransfer?.files; + if (droppedFiles && droppedFiles.length > 0) { + handleCertFile(droppedFiles[0]); + } + }); + } - if (certPassword) { - certPassword.addEventListener('input', handlePasswordInput); - certPassword.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - handlePasswordInput(); - } - }); - } + if (certPassword) { + certPassword.addEventListener('input', handlePasswordInput); + certPassword.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + handlePasswordInput(); + } + }); + } - if (processBtn) { - processBtn.addEventListener('click', processSignature); - } + if (processBtn) { + processBtn.addEventListener('click', processSignature); + } - if (backBtn) { - backBtn.addEventListener('click', () => { - window.location.href = import.meta.env.BASE_URL; - }); - } + if (backBtn) { + backBtn.addEventListener('click', () => { + window.location.href = import.meta.env.BASE_URL; + }); + } - const enableVisibleSig = getElement('enable-visible-sig'); - const visibleSigOptions = getElement('visible-sig-options'); - const sigPage = getElement('sig-page'); - const customPageWrapper = getElement('custom-page-wrapper'); - const sigImageInput = getElement('sig-image-input'); - const sigImagePreview = getElement('sig-image-preview'); - const sigImageThumb = getElement('sig-image-thumb'); - const removeSigImage = getElement('remove-sig-image'); - const enableSigText = getElement('enable-sig-text'); - const sigTextOptions = getElement('sig-text-options'); + const enableVisibleSig = getElement('enable-visible-sig'); + const visibleSigOptions = getElement('visible-sig-options'); + const sigPage = getElement('sig-page'); + const customPageWrapper = getElement('custom-page-wrapper'); + const sigImageInput = getElement('sig-image-input'); + const sigImagePreview = getElement('sig-image-preview'); + const sigImageThumb = getElement('sig-image-thumb'); + const removeSigImage = getElement('remove-sig-image'); + const enableSigText = getElement('enable-sig-text'); + const sigTextOptions = getElement('sig-text-options'); - if (enableVisibleSig && visibleSigOptions) { - enableVisibleSig.addEventListener('change', () => { - if (enableVisibleSig.checked) { - visibleSigOptions.classList.remove('hidden'); - } else { - visibleSigOptions.classList.add('hidden'); - } - }); - } + if (enableVisibleSig && visibleSigOptions) { + enableVisibleSig.addEventListener('change', () => { + if (enableVisibleSig.checked) { + visibleSigOptions.classList.remove('hidden'); + } else { + visibleSigOptions.classList.add('hidden'); + } + }); + } - if (sigPage && customPageWrapper) { - sigPage.addEventListener('change', () => { - if (sigPage.value === 'custom') { - customPageWrapper.classList.remove('hidden'); - } else { - customPageWrapper.classList.add('hidden'); - } - }); - } + if (sigPage && customPageWrapper) { + sigPage.addEventListener('change', () => { + if (sigPage.value === 'custom') { + customPageWrapper.classList.remove('hidden'); + } else { + customPageWrapper.classList.add('hidden'); + } + }); + } - if (sigImageInput) { - sigImageInput.addEventListener('change', async (e) => { - const input = e.target as HTMLInputElement; - if (input.files && input.files.length > 0) { - const file = input.files[0]; - const validTypes = ['image/png', 'image/jpeg', 'image/webp']; - if (!validTypes.includes(file.type)) { - showAlert('Invalid Image', 'Please select a PNG, JPG, or WebP image.'); - return; - } - state.sigImageData = await readFileAsArrayBuffer(file) as ArrayBuffer; - state.sigImageType = file.type.replace('image/', '') as 'png' | 'jpeg' | 'webp'; + if (sigImageInput) { + sigImageInput.addEventListener('change', async (e) => { + const input = e.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + const file = input.files[0]; + const validTypes = ['image/png', 'image/jpeg', 'image/webp']; + if (!validTypes.includes(file.type)) { + showAlert( + 'Invalid Image', + 'Please select a PNG, JPG, or WebP image.' + ); + return; + } + state.sigImageData = (await readFileAsArrayBuffer(file)) as ArrayBuffer; + state.sigImageType = file.type.replace('image/', '') as + | 'png' + | 'jpeg' + | 'webp'; - if (sigImageThumb && sigImagePreview) { - const url = URL.createObjectURL(file); - sigImageThumb.src = url; - sigImagePreview.classList.remove('hidden'); - } - } - }); - } + if (sigImageThumb && sigImagePreview) { + const url = URL.createObjectURL(file); + sigImageThumb.src = url; + sigImagePreview.classList.remove('hidden'); + } + } + }); + } - if (removeSigImage && sigImagePreview) { - removeSigImage.addEventListener('click', () => { - state.sigImageData = null; - state.sigImageType = null; - sigImagePreview.classList.add('hidden'); - if (sigImageInput) sigImageInput.value = ''; - }); - } + if (removeSigImage && sigImagePreview) { + removeSigImage.addEventListener('click', () => { + state.sigImageData = null; + state.sigImageType = null; + sigImagePreview.classList.add('hidden'); + if (sigImageInput) sigImageInput.value = ''; + }); + } - if (enableSigText && sigTextOptions) { - enableSigText.addEventListener('change', () => { - if (enableSigText.checked) { - sigTextOptions.classList.remove('hidden'); - } else { - sigTextOptions.classList.add('hidden'); - } - }); - } + if (enableSigText && sigTextOptions) { + enableSigText.addEventListener('change', () => { + if (enableSigText.checked) { + sigTextOptions.classList.remove('hidden'); + } else { + sigTextOptions.classList.add('hidden'); + } + }); + } } function handlePdfUpload(e: Event): void { - const input = e.target as HTMLInputElement; - if (input.files && input.files.length > 0) { - handlePdfFile(input.files[0]); - } + const input = e.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + handlePdfFile(input.files[0]); + } } async function handlePdfFile(file: File): Promise { - if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) { - showAlert('Invalid File', 'Please select a PDF file.'); - return; - } + if ( + file.type !== 'application/pdf' && + !file.name.toLowerCase().endsWith('.pdf') + ) { + showAlert('Invalid File', 'Please select a PDF file.'); + return; + } - state.pdfFile = file; - state.pdfBytes = new Uint8Array(await readFileAsArrayBuffer(file) as ArrayBuffer); + state.pdfFile = file; + state.pdfBytes = new Uint8Array( + (await readFileAsArrayBuffer(file)) as ArrayBuffer + ); - updatePdfDisplay(); - showCertificateSection(); + updatePdfDisplay(); + showCertificateSection(); } async function updatePdfDisplay(): Promise { - const fileDisplayArea = getElement('file-display-area'); + const fileDisplayArea = getElement('file-display-area'); - if (!fileDisplayArea || !state.pdfFile) return; + if (!fileDisplayArea || !state.pdfFile) return; + fileDisplayArea.innerHTML = ''; + + const fileDiv = document.createElement('div'); + fileDiv.className = + 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; + + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col flex-1 min-w-0'; + + const nameSpan = document.createElement('div'); + nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; + nameSpan.textContent = state.pdfFile.name; + + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = `${formatBytes(state.pdfFile.size)} • ${t('common.loadingPageCount')}`; + + 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 = () => { + state.pdfFile = null; + state.pdfBytes = null; fileDisplayArea.innerHTML = ''; + hideCertificateSection(); + updateProcessButton(); + }; - const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); - const infoContainer = document.createElement('div'); - infoContainer.className = 'flex flex-col flex-1 min-w-0'; - - const nameSpan = document.createElement('div'); - nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; - nameSpan.textContent = state.pdfFile.name; - - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(state.pdfFile.size)} • ${t('common.loadingPageCount')}`; - - 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 = () => { - state.pdfFile = null; - state.pdfBytes = null; - fileDisplayArea.innerHTML = ''; - hideCertificateSection(); - updateProcessButton(); - }; - - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); - - try { - if (state.pdfBytes) { - const pdfDoc = await getPDFDocument({ data: state.pdfBytes.slice() }).promise; - metaSpan.textContent = `${formatBytes(state.pdfFile.size)} • ${pdfDoc.numPages} pages`; - } - } catch (error) { - console.error('Error loading PDF:', error); - metaSpan.textContent = `${formatBytes(state.pdfFile.size)}`; + if (state.pdfFile) { + const result = await loadPdfWithPasswordPrompt(state.pdfFile); + if (!result) { + state.pdfFile = null; + state.pdfBytes = null; + fileDisplayArea.innerHTML = ''; + hideCertificateSection(); + updateProcessButton(); + return; } + state.pdfFile = result.file; + state.pdfBytes = new Uint8Array(result.bytes); + nameSpan.textContent = result.file.name; + metaSpan.textContent = `${formatBytes(result.file.size)} • ${result.pdf.numPages} pages`; + result.pdf.destroy(); + } } function showCertificateSection(): void { - const certSection = getElement('certificate-section'); - if (certSection) { - certSection.classList.remove('hidden'); - } + const certSection = getElement('certificate-section'); + if (certSection) { + certSection.classList.remove('hidden'); + } } function hideCertificateSection(): void { - const certSection = getElement('certificate-section'); - const signatureOptions = getElement('signature-options'); + const certSection = getElement('certificate-section'); + const signatureOptions = getElement('signature-options'); - if (certSection) { - certSection.classList.add('hidden'); - } - if (signatureOptions) { - signatureOptions.classList.add('hidden'); - } + if (certSection) { + certSection.classList.add('hidden'); + } + if (signatureOptions) { + signatureOptions.classList.add('hidden'); + } - state.certFile = null; - state.certData = null; + state.certFile = null; + state.certData = null; - const certDisplayArea = getElement('cert-display-area'); - if (certDisplayArea) { - certDisplayArea.innerHTML = ''; - } + const certDisplayArea = getElement('cert-display-area'); + if (certDisplayArea) { + certDisplayArea.innerHTML = ''; + } - const certInfo = getElement('cert-info'); - if (certInfo) { - certInfo.classList.add('hidden'); - } + const certInfo = getElement('cert-info'); + if (certInfo) { + certInfo.classList.add('hidden'); + } - const certPasswordSection = getElement('cert-password-section'); - if (certPasswordSection) { - certPasswordSection.classList.add('hidden'); - } + const certPasswordSection = getElement( + 'cert-password-section' + ); + if (certPasswordSection) { + certPasswordSection.classList.add('hidden'); + } } function handleCertUpload(e: Event): void { - const input = e.target as HTMLInputElement; - if (input.files && input.files.length > 0) { - handleCertFile(input.files[0]); - } + const input = e.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + handleCertFile(input.files[0]); + } } async function handleCertFile(file: File): Promise { - const validExtensions = ['.pfx', '.p12', '.pem']; - const hasValidExtension = validExtensions.some(ext => - file.name.toLowerCase().endsWith(ext) + const validExtensions = ['.pfx', '.p12', '.pem']; + const hasValidExtension = validExtensions.some((ext) => + file.name.toLowerCase().endsWith(ext) + ); + + if (!hasValidExtension) { + showAlert( + 'Invalid Certificate', + 'Please select a .pfx, .p12, or .pem certificate file.' ); + return; + } - if (!hasValidExtension) { - showAlert('Invalid Certificate', 'Please select a .pfx, .p12, or .pem certificate file.'); - return; - } + state.certFile = file; + state.certData = null; - state.certFile = file; - state.certData = null; + updateCertDisplay(); - updateCertDisplay(); + const isPemFile = file.name.toLowerCase().endsWith('.pem'); - const isPemFile = file.name.toLowerCase().endsWith('.pem'); + if (isPemFile) { + try { + const pemContent = await file.text(); + const isEncrypted = pemContent.includes('ENCRYPTED'); - if (isPemFile) { - try { - const pemContent = await file.text(); - const isEncrypted = pemContent.includes('ENCRYPTED'); - - if (isEncrypted) { - showPasswordSection(); - updatePasswordLabel('Private Key Password'); - } else { - state.certData = parseCombinedPem(pemContent); - updateCertInfo(); - showSignatureOptions(); - - const certStatus = getElement('cert-status'); - if (certStatus) { - certStatus.innerHTML = 'Certificate loaded '; - createIcons({ icons }); - certStatus.className = 'text-xs text-green-400'; - } - } - } catch (error) { - const certStatus = getElement('cert-status'); - if (certStatus) { - certStatus.textContent = 'Failed to parse PEM file'; - certStatus.className = 'text-xs text-red-400'; - } - } - } else { + if (isEncrypted) { showPasswordSection(); - updatePasswordLabel('Certificate Password'); - } + updatePasswordLabel('Private Key Password'); + } else { + state.certData = parseCombinedPem(pemContent); + updateCertInfo(); + showSignatureOptions(); - hideSignatureOptions(); - updateProcessButton(); + const certStatus = getElement('cert-status'); + if (certStatus) { + certStatus.innerHTML = + 'Certificate loaded '; + createIcons({ icons }); + certStatus.className = 'text-xs text-green-400'; + } + } + } catch (error) { + const certStatus = getElement('cert-status'); + if (certStatus) { + certStatus.textContent = 'Failed to parse PEM file'; + certStatus.className = 'text-xs text-red-400'; + } + } + } else { + showPasswordSection(); + updatePasswordLabel('Certificate Password'); + } + + hideSignatureOptions(); + updateProcessButton(); } function updateCertDisplay(): void { - const certDisplayArea = getElement('cert-display-area'); + const certDisplayArea = getElement('cert-display-area'); - if (!certDisplayArea || !state.certFile) return; + if (!certDisplayArea || !state.certFile) return; + certDisplayArea.innerHTML = ''; + + const certDiv = document.createElement('div'); + certDiv.className = + 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; + + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col flex-1 min-w-0'; + + const nameSpan = document.createElement('div'); + nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; + nameSpan.textContent = state.certFile.name; + + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.id = 'cert-status'; + metaSpan.textContent = 'Enter password to unlock'; + + 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 = () => { + state.certFile = null; + state.certData = null; certDisplayArea.innerHTML = ''; + hidePasswordSection(); + hideCertInfo(); + hideSignatureOptions(); + updateProcessButton(); + }; - const certDiv = document.createElement('div'); - certDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; - - const infoContainer = document.createElement('div'); - infoContainer.className = 'flex flex-col flex-1 min-w-0'; - - const nameSpan = document.createElement('div'); - nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; - nameSpan.textContent = state.certFile.name; - - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.id = 'cert-status'; - metaSpan.textContent = 'Enter password to unlock'; - - 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 = () => { - state.certFile = null; - state.certData = null; - certDisplayArea.innerHTML = ''; - hidePasswordSection(); - hideCertInfo(); - hideSignatureOptions(); - updateProcessButton(); - }; - - certDiv.append(infoContainer, removeBtn); - certDisplayArea.appendChild(certDiv); - createIcons({ icons }); + certDiv.append(infoContainer, removeBtn); + certDisplayArea.appendChild(certDiv); + createIcons({ icons }); } function showPasswordSection(): void { - const certPasswordSection = getElement('cert-password-section'); - if (certPasswordSection) { - certPasswordSection.classList.remove('hidden'); - } + const certPasswordSection = getElement( + 'cert-password-section' + ); + if (certPasswordSection) { + certPasswordSection.classList.remove('hidden'); + } - const certPassword = getElement('cert-password'); - if (certPassword) { - certPassword.value = ''; - certPassword.focus(); - } + const certPassword = getElement('cert-password'); + if (certPassword) { + certPassword.value = ''; + certPassword.focus(); + } } function updatePasswordLabel(labelText: string): void { - const label = document.querySelector('label[for="cert-password"]'); - if (label) { - label.textContent = labelText; - } + const label = document.querySelector('label[for="cert-password"]'); + if (label) { + label.textContent = labelText; + } } function hidePasswordSection(): void { - const certPasswordSection = getElement('cert-password-section'); - if (certPasswordSection) { - certPasswordSection.classList.add('hidden'); - } + const certPasswordSection = getElement( + 'cert-password-section' + ); + if (certPasswordSection) { + certPasswordSection.classList.add('hidden'); + } } function showSignatureOptions(): void { - const signatureOptions = getElement('signature-options'); - if (signatureOptions) { - signatureOptions.classList.remove('hidden'); - } - const visibleSigSection = getElement('visible-signature-section'); - if (visibleSigSection) { - visibleSigSection.classList.remove('hidden'); - } + const signatureOptions = getElement('signature-options'); + if (signatureOptions) { + signatureOptions.classList.remove('hidden'); + } + const visibleSigSection = getElement( + 'visible-signature-section' + ); + if (visibleSigSection) { + visibleSigSection.classList.remove('hidden'); + } } function hideSignatureOptions(): void { - const signatureOptions = getElement('signature-options'); - if (signatureOptions) { - signatureOptions.classList.add('hidden'); - } - const visibleSigSection = getElement('visible-signature-section'); - if (visibleSigSection) { - visibleSigSection.classList.add('hidden'); - } + const signatureOptions = getElement('signature-options'); + if (signatureOptions) { + signatureOptions.classList.add('hidden'); + } + const visibleSigSection = getElement( + 'visible-signature-section' + ); + if (visibleSigSection) { + visibleSigSection.classList.add('hidden'); + } } function hideCertInfo(): void { - const certInfo = getElement('cert-info'); - if (certInfo) { - certInfo.classList.add('hidden'); - } + const certInfo = getElement('cert-info'); + if (certInfo) { + certInfo.classList.add('hidden'); + } } async function handlePasswordInput(): Promise { - const certPassword = getElement('cert-password'); - const password = certPassword?.value ?? ''; + const certPassword = getElement('cert-password'); + const password = certPassword?.value ?? ''; - if (!state.certFile || !password) { - return; + if (!state.certFile || !password) { + return; + } + + try { + const isPemFile = state.certFile.name.toLowerCase().endsWith('.pem'); + + if (isPemFile) { + const pemContent = await state.certFile.text(); + state.certData = parseCombinedPem(pemContent, password); + } else { + const certBytes = (await readFileAsArrayBuffer( + state.certFile + )) as ArrayBuffer; + state.certData = parsePfxFile(certBytes, password); } - try { - const isPemFile = state.certFile.name.toLowerCase().endsWith('.pem'); + updateCertInfo(); + showSignatureOptions(); + updateProcessButton(); - if (isPemFile) { - const pemContent = await state.certFile.text(); - state.certData = parseCombinedPem(pemContent, password); - } else { - const certBytes = await readFileAsArrayBuffer(state.certFile) as ArrayBuffer; - state.certData = parsePfxFile(certBytes, password); - } - - updateCertInfo(); - showSignatureOptions(); - updateProcessButton(); - - const certStatus = getElement('cert-status'); - if (certStatus) { - certStatus.innerHTML = 'Certificate unlocked '; - createIcons({ icons }); - certStatus.className = 'text-xs text-green-400'; - } - } catch (error) { - state.certData = null; - hideSignatureOptions(); - updateProcessButton(); - - const certStatus = getElement('cert-status'); - if (certStatus) { - const errorMessage = error instanceof Error ? error.message : 'Invalid password or certificate'; - certStatus.textContent = errorMessage.includes('password') - ? 'Incorrect password' - : 'Failed to parse certificate'; - certStatus.className = 'text-xs text-red-400'; - } + const certStatus = getElement('cert-status'); + if (certStatus) { + certStatus.innerHTML = + 'Certificate unlocked '; + createIcons({ icons }); + certStatus.className = 'text-xs text-green-400'; } + } catch (error) { + state.certData = null; + hideSignatureOptions(); + updateProcessButton(); + + const certStatus = getElement('cert-status'); + if (certStatus) { + const errorMessage = + error instanceof Error + ? error.message + : 'Invalid password or certificate'; + certStatus.textContent = errorMessage.includes('password') + ? 'Incorrect password' + : 'Failed to parse certificate'; + certStatus.className = 'text-xs text-red-400'; + } + } } function updateCertInfo(): void { - if (!state.certData) return; + if (!state.certData) return; - const certInfo = getElement('cert-info'); - const certSubject = getElement('cert-subject'); - const certIssuer = getElement('cert-issuer'); - const certValidity = getElement('cert-validity'); + const certInfo = getElement('cert-info'); + const certSubject = getElement('cert-subject'); + const certIssuer = getElement('cert-issuer'); + const certValidity = getElement('cert-validity'); - if (!certInfo) return; + if (!certInfo) return; - const info = getCertificateInfo(state.certData.certificate); + const info = getCertificateInfo(state.certData.certificate); - if (certSubject) { - certSubject.textContent = info.subject; - } - if (certIssuer) { - certIssuer.textContent = info.issuer; - } - if (certValidity) { - const formatDate = (date: Date) => date.toLocaleDateString(); - certValidity.textContent = `${formatDate(info.validFrom)} - ${formatDate(info.validTo)}`; - } + if (certSubject) { + certSubject.textContent = info.subject; + } + if (certIssuer) { + certIssuer.textContent = info.issuer; + } + if (certValidity) { + const formatDate = (date: Date) => date.toLocaleDateString(); + certValidity.textContent = `${formatDate(info.validFrom)} - ${formatDate(info.validTo)}`; + } - certInfo.classList.remove('hidden'); + certInfo.classList.remove('hidden'); } function updateProcessButton(): void { - const processBtn = getElement('process-btn'); - if (!processBtn) return; + const processBtn = getElement('process-btn'); + if (!processBtn) return; - const canProcess = state.pdfBytes !== null && state.certData !== null; + const canProcess = state.pdfBytes !== null && state.certData !== null; - if (canProcess) { - processBtn.style.display = ''; - } else { - processBtn.style.display = 'none'; - } + if (canProcess) { + processBtn.style.display = ''; + } else { + processBtn.style.display = 'none'; + } } async function processSignature(): Promise { - if (!state.pdfBytes || !state.certData) { - showAlert('Missing Data', 'Please upload both a PDF and a valid certificate.'); - return; + if (!state.pdfBytes || !state.certData) { + showAlert( + 'Missing Data', + 'Please upload both a PDF and a valid certificate.' + ); + return; + } + + const reason = getElement('sign-reason')?.value ?? ''; + const location = getElement('sign-location')?.value ?? ''; + const contactInfo = getElement('sign-contact')?.value ?? ''; + + const signatureInfo: SignatureInfo = {}; + if (reason) signatureInfo.reason = reason; + if (location) signatureInfo.location = location; + if (contactInfo) signatureInfo.contactInfo = contactInfo; + + let visibleSignature: VisibleSignatureOptions | undefined; + + const enableVisibleSig = getElement('enable-visible-sig'); + if (enableVisibleSig?.checked) { + const sigX = parseInt( + getElement('sig-x')?.value ?? '25', + 10 + ); + const sigY = parseInt( + getElement('sig-y')?.value ?? '700', + 10 + ); + const sigWidth = parseInt( + getElement('sig-width')?.value ?? '150', + 10 + ); + const sigHeight = parseInt( + getElement('sig-height')?.value ?? '70', + 10 + ); + + const sigPageSelect = getElement('sig-page'); + let sigPage: number | string = 0; + let numPages = 1; + + if (state.pdfFile) { + const pageCountResult = await loadPdfWithPasswordPrompt(state.pdfFile); + if (!pageCountResult) return; + state.pdfFile = pageCountResult.file; + state.pdfBytes = new Uint8Array(pageCountResult.bytes); + numPages = pageCountResult.pdf.numPages; + pageCountResult.pdf.destroy(); } - const reason = getElement('sign-reason')?.value ?? ''; - const location = getElement('sign-location')?.value ?? ''; - const contactInfo = getElement('sign-contact')?.value ?? ''; - - const signatureInfo: SignatureInfo = {}; - if (reason) signatureInfo.reason = reason; - if (location) signatureInfo.location = location; - if (contactInfo) signatureInfo.contactInfo = contactInfo; - - let visibleSignature: VisibleSignatureOptions | undefined; - - const enableVisibleSig = getElement('enable-visible-sig'); - if (enableVisibleSig?.checked) { - const sigX = parseInt(getElement('sig-x')?.value ?? '25', 10); - const sigY = parseInt(getElement('sig-y')?.value ?? '700', 10); - const sigWidth = parseInt(getElement('sig-width')?.value ?? '150', 10); - const sigHeight = parseInt(getElement('sig-height')?.value ?? '70', 10); - - const sigPageSelect = getElement('sig-page'); - let sigPage: number | string = 0; - let numPages = 1; - - try { - const pdfDoc = await getPDFDocument({ data: state.pdfBytes.slice() }).promise; - numPages = pdfDoc.numPages; - } catch (error) { - console.error('Error getting PDF page count:', error); - } - - if (sigPageSelect) { - if (sigPageSelect.value === 'last') { - sigPage = (numPages - 1).toString(); - } else if (sigPageSelect.value === 'all') { - if (numPages === 1) { - sigPage = '0'; - } else { - sigPage = `0-${numPages - 1}`; - } - } else if (sigPageSelect.value === 'custom') { - sigPage = parseInt(getElement('sig-custom-page')?.value ?? '1', 10) - 1; - } else { - sigPage = parseInt(sigPageSelect.value, 10); - } - } - - const enableSigText = getElement('enable-sig-text'); - let sigText = enableSigText?.checked ? getElement('sig-text')?.value : undefined; - const sigTextColor = getElement('sig-text-color')?.value ?? '#000000'; - const sigTextSize = parseInt(getElement('sig-text-size')?.value ?? '12', 10); - - if (!state.sigImageData && !sigText && state.certData) { - const certInfo = getCertificateInfo(state.certData.certificate); - const date = new Date().toLocaleDateString(); - sigText = `Digitally signed by ${certInfo.subject}\n${date}`; - } - - let finalHeight = sigHeight; - if (sigText && !state.sigImageData) { - const lineCount = (sigText.match(/\n/g) || []).length + 1; - const lineHeightFactor = 1.4; - const padding = 16; - const calculatedHeight = Math.ceil(lineCount * sigTextSize * lineHeightFactor + padding); - finalHeight = Math.max(calculatedHeight, sigHeight); - } - - visibleSignature = { - enabled: true, - x: sigX, - y: sigY, - width: sigWidth, - height: finalHeight, - page: sigPage, - imageData: state.sigImageData ?? undefined, - imageType: state.sigImageType ?? undefined, - text: sigText, - textColor: sigTextColor, - textSize: sigTextSize, - }; - } - - showLoader('Applying digital signature...'); - - try { - const signedPdfBytes = await signPdf(state.pdfBytes, state.certData, { - signatureInfo, - visibleSignature, - }); - - const blob = new Blob([signedPdfBytes.slice().buffer], { type: 'application/pdf' }); - const originalName = state.pdfFile?.name ?? 'document.pdf'; - const signedName = originalName.replace(/\.pdf$/i, '_signed.pdf'); - - downloadFile(blob, signedName); - - hideLoader(); - showAlert('Success', 'PDF signed successfully! The signature can be verified in any PDF reader.', 'success', () => { resetState(); }); - } catch (error) { - hideLoader(); - console.error('Signing error:', error); - const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; - - // Check if this is a CORS/network error from certificate chain fetching - if (errorMessage.includes('Failed to fetch') || errorMessage.includes('CORS') || errorMessage.includes('NetworkError')) { - showAlert( - 'Signing Failed', - 'Failed to fetch certificate chain. This may be due to network issues or the certificate proxy being unavailable. Please check your internet connection and try again. If the issue persists, contact support.' - ); + if (sigPageSelect) { + if (sigPageSelect.value === 'last') { + sigPage = (numPages - 1).toString(); + } else if (sigPageSelect.value === 'all') { + if (numPages === 1) { + sigPage = '0'; } else { - showAlert('Signing Failed', `Failed to sign PDF: ${errorMessage}`); + sigPage = `0-${numPages - 1}`; } + } else if (sigPageSelect.value === 'custom') { + sigPage = + parseInt( + getElement('sig-custom-page')?.value ?? '1', + 10 + ) - 1; + } else { + sigPage = parseInt(sigPageSelect.value, 10); + } } + + const enableSigText = getElement('enable-sig-text'); + let sigText = enableSigText?.checked + ? getElement('sig-text')?.value + : undefined; + const sigTextColor = + getElement('sig-text-color')?.value ?? '#000000'; + const sigTextSize = parseInt( + getElement('sig-text-size')?.value ?? '12', + 10 + ); + + if (!state.sigImageData && !sigText && state.certData) { + const certInfo = getCertificateInfo(state.certData.certificate); + const date = new Date().toLocaleDateString(); + sigText = `Digitally signed by ${certInfo.subject}\n${date}`; + } + + let finalHeight = sigHeight; + if (sigText && !state.sigImageData) { + const lineCount = (sigText.match(/\n/g) || []).length + 1; + const lineHeightFactor = 1.4; + const padding = 16; + const calculatedHeight = Math.ceil( + lineCount * sigTextSize * lineHeightFactor + padding + ); + finalHeight = Math.max(calculatedHeight, sigHeight); + } + + visibleSignature = { + enabled: true, + x: sigX, + y: sigY, + width: sigWidth, + height: finalHeight, + page: sigPage, + imageData: state.sigImageData ?? undefined, + imageType: state.sigImageType ?? undefined, + text: sigText, + textColor: sigTextColor, + textSize: sigTextSize, + }; + } + + showLoader('Applying digital signature...'); + + try { + const signedPdfBytes = await signPdf(state.pdfBytes, state.certData, { + signatureInfo, + visibleSignature, + }); + + const blob = new Blob([signedPdfBytes.slice().buffer], { + type: 'application/pdf', + }); + const originalName = state.pdfFile?.name ?? 'document.pdf'; + const signedName = originalName.replace(/\.pdf$/i, '_signed.pdf'); + + downloadFile(blob, signedName); + + hideLoader(); + showAlert( + 'Success', + 'PDF signed successfully! The signature can be verified in any PDF reader.', + 'success', + () => { + resetState(); + } + ); + } catch (error) { + hideLoader(); + console.error('Signing error:', error); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error occurred'; + + // Check if this is a CORS/network error from certificate chain fetching + if ( + errorMessage.includes('Failed to fetch') || + errorMessage.includes('CORS') || + errorMessage.includes('NetworkError') + ) { + showAlert( + 'Signing Failed', + 'Failed to fetch certificate chain. This may be due to network issues or the certificate proxy being unavailable. Please check your internet connection and try again. If the issue persists, contact support.' + ); + } else { + showAlert('Signing Failed', `Failed to sign PDF: ${errorMessage}`); + } + } } if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializePage); + document.addEventListener('DOMContentLoaded', initializePage); } else { - initializePage(); + initializePage(); } diff --git a/src/js/logic/divide-pages-page.ts b/src/js/logic/divide-pages-page.ts index 11133cc..e48d207 100644 --- a/src/js/logic/divide-pages-page.ts +++ b/src/js/logic/divide-pages-page.ts @@ -1,235 +1,269 @@ import { DividePagesState } from '@/types'; import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { downloadFile, formatBytes, parsePageRanges } from '../utils/helpers.js'; +import { + downloadFile, + formatBytes, + parsePageRanges, +} from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; const pageState: DividePagesState = { - file: null, - pdfDoc: null, - totalPages: 0, + file: null, + pdfDoc: null, + totalPages: 0, }; function resetState() { - pageState.file = null; - pageState.pdfDoc = null; - pageState.totalPages = 0; + pageState.file = null; + pageState.pdfDoc = null; + pageState.totalPages = 0; - 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 fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; - const splitTypeSelect = document.getElementById('split-type') as HTMLSelectElement; - if (splitTypeSelect) splitTypeSelect.value = 'vertical'; + const splitTypeSelect = document.getElementById( + 'split-type' + ) as HTMLSelectElement; + if (splitTypeSelect) splitTypeSelect.value = 'vertical'; - const pageRangeInput = document.getElementById('page-range') as HTMLInputElement; - if (pageRangeInput) pageRangeInput.value = ''; + const pageRangeInput = document.getElementById( + 'page-range' + ) as HTMLInputElement; + if (pageRangeInput) pageRangeInput.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'); - 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.file) { + 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 = pageState.file.name; - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`; + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`; - 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 () { + resetState(); + }; - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); - try { - showLoader('Loading PDF...'); - const arrayBuffer = await pageState.file.arrayBuffer(); - pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, { - ignoreEncryption: true, - throwOnInvalidObject: false - }); - pageState.totalPages = pageState.pdfDoc.getPageCount(); - hideLoader(); + try { + const result = await loadPdfWithPasswordPrompt(pageState.file); + if (!result) { + resetState(); + return; + } + result.pdf.destroy(); + pageState.file = result.file; + showLoader('Loading PDF...'); - metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.totalPages} pages`; + pageState.pdfDoc = await PDFLibDocument.load(result.bytes); + pageState.totalPages = pageState.pdfDoc.getPageCount(); + hideLoader(); - if (toolOptions) toolOptions.classList.remove('hidden'); - } catch (error) { - console.error('Error loading PDF:', error); - hideLoader(); - showAlert('Error', 'Failed to load PDF file.'); - resetState(); - } - } else { - if (toolOptions) toolOptions.classList.add('hidden'); + metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.totalPages} pages`; + + if (toolOptions) toolOptions.classList.remove('hidden'); + } catch (error) { + console.error('Error loading PDF:', error); + hideLoader(); + showAlert('Error', 'Failed to load PDF file.'); + resetState(); } + } else { + if (toolOptions) toolOptions.classList.add('hidden'); + } } async function dividePages() { - if (!pageState.pdfDoc || !pageState.file) { - showAlert('Error', 'Please upload a PDF first.'); - return; + if (!pageState.pdfDoc || !pageState.file) { + showAlert('Error', 'Please upload a PDF first.'); + return; + } + + const pageRangeInput = document.getElementById( + 'page-range' + ) as HTMLInputElement; + const pageRangeValue = pageRangeInput?.value.trim().toLowerCase() || ''; + const splitTypeSelect = document.getElementById( + 'split-type' + ) as HTMLSelectElement; + const splitType = splitTypeSelect.value; + + let pagesToDivide: Set; + + if (pageRangeValue === '' || pageRangeValue === 'all') { + pagesToDivide = new Set( + Array.from({ length: pageState.totalPages }, (_, i) => i + 1) + ); + } else { + const parsedIndices = parsePageRanges(pageRangeValue, pageState.totalPages); + pagesToDivide = new Set(parsedIndices.map((i) => i + 1)); + + if (pagesToDivide.size === 0) { + showAlert( + 'Invalid Range', + 'Please enter a valid page range (e.g., 1-5, 8, 11-13).' + ); + return; } + } - const pageRangeInput = document.getElementById('page-range') as HTMLInputElement; - const pageRangeValue = pageRangeInput?.value.trim().toLowerCase() || ''; - const splitTypeSelect = document.getElementById('split-type') as HTMLSelectElement; - const splitType = splitTypeSelect.value; + showLoader('Splitting PDF pages...'); - let pagesToDivide: Set; + try { + const newPdfDoc = await PDFLibDocument.create(); + const pages = pageState.pdfDoc.getPages(); - if (pageRangeValue === '' || pageRangeValue === 'all') { - pagesToDivide = new Set(Array.from({ length: pageState.totalPages }, (_, i) => i + 1)); - } else { - const parsedIndices = parsePageRanges(pageRangeValue, pageState.totalPages); - pagesToDivide = new Set(parsedIndices.map(i => i + 1)); + for (let i = 0; i < pages.length; i++) { + const pageNum = i + 1; + const originalPage = pages[i]; + const { width, height } = originalPage.getSize(); - if (pagesToDivide.size === 0) { - showAlert('Invalid Range', 'Please enter a valid page range (e.g., 1-5, 8, 11-13).'); - return; - } - } + showLoader(`Processing page ${pageNum} of ${pages.length}...`); - showLoader('Splitting PDF pages...'); + if (pagesToDivide.has(pageNum)) { + const [page1] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]); + const [page2] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]); - try { - const newPdfDoc = await PDFLibDocument.create(); - const pages = pageState.pdfDoc.getPages(); - - for (let i = 0; i < pages.length; i++) { - const pageNum = i + 1; - const originalPage = pages[i]; - const { width, height } = originalPage.getSize(); - - showLoader(`Processing page ${pageNum} of ${pages.length}...`); - - if (pagesToDivide.has(pageNum)) { - const [page1] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]); - const [page2] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]); - - switch (splitType) { - case 'vertical': - page1.setCropBox(0, 0, width / 2, height); - page2.setCropBox(width / 2, 0, width / 2, height); - break; - case 'horizontal': - page1.setCropBox(0, height / 2, width, height / 2); - page2.setCropBox(0, 0, width, height / 2); - break; - } - - newPdfDoc.addPage(page1); - newPdfDoc.addPage(page2); - } else { - const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]); - newPdfDoc.addPage(copiedPage); - } + switch (splitType) { + case 'vertical': + page1.setCropBox(0, 0, width / 2, height); + page2.setCropBox(width / 2, 0, width / 2, height); + break; + case 'horizontal': + page1.setCropBox(0, height / 2, width, height / 2); + page2.setCropBox(0, 0, width, height / 2); + break; } - const newPdfBytes = await newPdfDoc.save(); - const originalName = pageState.file.name.replace(/\.pdf$/i, ''); - - downloadFile( - new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), - `${originalName}_divided.pdf` - ); - - showAlert('Success', 'Pages have been divided successfully!', 'success', function () { - resetState(); - }); - } catch (e) { - console.error(e); - showAlert('Error', 'An error occurred while dividing the PDF.'); - } finally { - hideLoader(); + newPdfDoc.addPage(page1); + newPdfDoc.addPage(page2); + } else { + const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]); + newPdfDoc.addPage(copiedPage); + } } + + const newPdfBytes = await newPdfDoc.save(); + const originalName = pageState.file.name.replace(/\.pdf$/i, ''); + + downloadFile( + new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), + `${originalName}_divided.pdf` + ); + + showAlert( + 'Success', + 'Pages have been divided successfully!', + 'success', + function () { + resetState(); + } + ); + } catch (e) { + console.error(e); + showAlert('Error', 'An error occurred while dividing the PDF.'); + } finally { + hideLoader(); + } } 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 file = files[0]; + if ( + file.type === 'application/pdf' || + file.name.toLowerCase().endsWith('.pdf') + ) { + pageState.file = file; + updateUI(); } + } } 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 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); + }); + + 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('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); + } + } + }); - if (fileInput && dropZone) { - fileInput.addEventListener('change', function (e) { - handleFileSelect((e.target as HTMLInputElement).files); - }); + fileInput.addEventListener('click', function () { + fileInput.value = ''; + }); + } - 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('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); - } - } - }); - - fileInput.addEventListener('click', function () { - fileInput.value = ''; - }); - } - - if (processBtn) { - processBtn.addEventListener('click', dividePages); - } + if (processBtn) { + processBtn.addEventListener('click', dividePages); + } }); diff --git a/src/js/logic/duplicate-organize.ts b/src/js/logic/duplicate-organize.ts index fe516e8..533a820 100644 --- a/src/js/logic/duplicate-organize.ts +++ b/src/js/logic/duplicate-organize.ts @@ -1,13 +1,20 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { downloadFile, getPDFDocument } from '../utils/helpers.js'; +import { downloadFile } from '../utils/helpers.js'; import { state } from '../state.js'; -import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; +import { + renderPagesProgressively, + cleanupLazyRendering, +} from '../utils/render-utils.js'; import Sortable from 'sortablejs'; import { icons, createIcons } from 'lucide'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import * as pdfjsLib from 'pdfjs-dist'; -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(); const duplicateOrganizeState = { sortableInstances: {}, @@ -91,7 +98,15 @@ export async function renderDuplicateOrganizeThumbnails() { showLoader('Rendering page previews...'); const pdfData = await state.pdfDoc.save(); - const pdfjsDoc = await getPDFDocument({ data: pdfData }).promise; + hideLoader(); + const loadResult = await loadPdfWithPasswordPrompt( + state.files[0], + state.files, + 0 + ); + if (!loadResult) return; + showLoader('Rendering page previews...'); + const pdfjsDoc = loadResult.pdf; grid.textContent = ''; @@ -148,22 +163,17 @@ export async function renderDuplicateOrganizeThumbnails() { try { // Render pages progressively with lazy loading - await renderPagesProgressively( - pdfjsDoc, - grid, - createWrapper, - { - batchSize: 8, - useLazyLoading: true, - lazyLoadMargin: '400px', - onProgress: (current, total) => { - showLoader(`Rendering page previews: ${current}/${total}`); - }, - onBatchComplete: () => { - createIcons({ icons }); - } - } - ); + await renderPagesProgressively(pdfjsDoc, grid, createWrapper, { + batchSize: 8, + useLazyLoading: true, + lazyLoadMargin: '400px', + onProgress: (current, total) => { + showLoader(`Rendering page previews: ${current}/${total}`); + }, + onBatchComplete: () => { + createIcons({ icons }); + }, + }); initializePageGridSortable(); } catch (error) { @@ -181,8 +191,10 @@ export async function processAndSave() { const finalPageElements = grid.querySelectorAll('.page-thumbnail'); const finalIndices = Array.from(finalPageElements) - .map((el) => parseInt((el as HTMLElement).dataset.originalPageIndex || '', 10)) - .filter(index => !isNaN(index) && index >= 0); + .map((el) => + parseInt((el as HTMLElement).dataset.originalPageIndex || '', 10) + ) + .filter((index) => !isNaN(index) && index >= 0); console.log('Saving PDF with indices:', finalIndices); console.log('Original PDF Page Count:', state.pdfDoc?.getPageCount()); @@ -195,10 +207,13 @@ export async function processAndSave() { const newPdfDoc = await PDFLibDocument.create(); const totalPages = state.pdfDoc.getPageCount(); - const invalidIndices = finalIndices.filter(i => i >= totalPages); + const invalidIndices = finalIndices.filter((i) => i >= totalPages); if (invalidIndices.length > 0) { console.error('Found invalid indices:', invalidIndices); - showAlert('Error', 'Some pages could not be processed. Please try again.'); + showAlert( + 'Error', + 'Some pages could not be processed. Please try again.' + ); return; } @@ -212,7 +227,10 @@ export async function processAndSave() { ); } catch (e) { console.error('Save error:', e); - showAlert('Error', 'Failed to save the new PDF. Check console for details.'); + showAlert( + 'Error', + 'Failed to save the new PDF. Check console for details.' + ); } finally { hideLoader(); } diff --git a/src/js/logic/edit-attachments-page.ts b/src/js/logic/edit-attachments-page.ts index d7fe32a..a19f125 100644 --- a/src/js/logic/edit-attachments-page.ts +++ b/src/js/logic/edit-attachments-page.ts @@ -7,6 +7,7 @@ import { showWasmRequiredDialog, WasmProvider, } from '../utils/wasm-provider.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; const worker = new Worker( import.meta.env.BASE_URL + 'workers/edit-attachments.worker.js' @@ -327,14 +328,17 @@ async function updateUI() { } } -function handleFileSelect(files: FileList | null) { +async 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; + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; + result.pdf.destroy(); + pageState.file = result.file; updateUI(); } } diff --git a/src/js/logic/edit-metadata-page.ts b/src/js/logic/edit-metadata-page.ts index 5036c54..3c9cb6e 100644 --- a/src/js/logic/edit-metadata-page.ts +++ b/src/js/logic/edit-metadata-page.ts @@ -3,367 +3,453 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; import { PDFDocument as PDFLibDocument, PDFName, PDFString } from 'pdf-lib'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; const pageState: EditMetadataState = { - file: null, - pdfDoc: null, + file: null, + pdfDoc: null, }; function resetState() { - pageState.file = null; - pageState.pdfDoc = null; + pageState.file = null; + pageState.pdfDoc = null; - 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 fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; - // Clear form fields - const fields = ['meta-title', 'meta-author', 'meta-subject', 'meta-keywords', 'meta-creator', 'meta-producer', 'meta-creation-date', 'meta-mod-date']; - fields.forEach(function (fieldId) { - const field = document.getElementById(fieldId) as HTMLInputElement; - if (field) field.value = ''; - }); + // Clear form fields + const fields = [ + 'meta-title', + 'meta-author', + 'meta-subject', + 'meta-keywords', + 'meta-creator', + 'meta-producer', + 'meta-creation-date', + 'meta-mod-date', + ]; + fields.forEach(function (fieldId) { + const field = document.getElementById(fieldId) as HTMLInputElement; + if (field) field.value = ''; + }); - // Clear custom fields - const customFieldsContainer = document.getElementById('custom-fields-container'); - if (customFieldsContainer) customFieldsContainer.innerHTML = ''; + // Clear custom fields + const customFieldsContainer = document.getElementById( + 'custom-fields-container' + ); + if (customFieldsContainer) customFieldsContainer.innerHTML = ''; } function formatDateForInput(date: Date | undefined): string { - if (!date) return ''; - try { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - return `${year}-${month}-${day}T${hours}:${minutes}`; - } catch { - return ''; - } + if (!date) return ''; + try { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}`; + } catch { + return ''; + } } function addCustomFieldRow(key: string = '', value: string = '') { - const container = document.getElementById('custom-fields-container'); - if (!container) return; + const container = document.getElementById('custom-fields-container'); + if (!container) return; - const row = document.createElement('div'); - row.className = 'flex flex-col gap-2'; + const row = document.createElement('div'); + row.className = 'flex flex-col gap-2'; - const keyInput = document.createElement('input'); - keyInput.type = 'text'; - keyInput.placeholder = 'Key (e.g., Department)'; - keyInput.value = key; - keyInput.className = 'custom-meta-key w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5'; + const keyInput = document.createElement('input'); + keyInput.type = 'text'; + keyInput.placeholder = 'Key (e.g., Department)'; + keyInput.value = key; + keyInput.className = + 'custom-meta-key w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5'; - const valueInput = document.createElement('input'); - valueInput.type = 'text'; - valueInput.placeholder = 'Value (e.g., Marketing)'; - valueInput.value = value; - valueInput.className = 'custom-meta-value w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5'; + const valueInput = document.createElement('input'); + valueInput.type = 'text'; + valueInput.placeholder = 'Value (e.g., Marketing)'; + valueInput.value = value; + valueInput.className = + 'custom-meta-value w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5'; - const removeBtn = document.createElement('button'); - removeBtn.type = 'button'; - removeBtn.className = 'text-red-400 hover:text-red-300 p-2 self-center'; - removeBtn.innerHTML = ''; - removeBtn.onclick = function () { - row.remove(); - }; + const removeBtn = document.createElement('button'); + removeBtn.type = 'button'; + removeBtn.className = 'text-red-400 hover:text-red-300 p-2 self-center'; + removeBtn.innerHTML = ''; + removeBtn.onclick = function () { + row.remove(); + }; - row.append(keyInput, valueInput, removeBtn); - container.appendChild(row); - createIcons({ icons }); + row.append(keyInput, valueInput, removeBtn); + container.appendChild(row); + createIcons({ icons }); } function populateMetadataFields() { - if (!pageState.pdfDoc) return; + if (!pageState.pdfDoc) return; - const titleInput = document.getElementById('meta-title') as HTMLInputElement; - const authorInput = document.getElementById('meta-author') as HTMLInputElement; - const subjectInput = document.getElementById('meta-subject') as HTMLInputElement; - const keywordsInput = document.getElementById('meta-keywords') as HTMLInputElement; - const creatorInput = document.getElementById('meta-creator') as HTMLInputElement; - const producerInput = document.getElementById('meta-producer') as HTMLInputElement; - const creationDateInput = document.getElementById('meta-creation-date') as HTMLInputElement; - const modDateInput = document.getElementById('meta-mod-date') as HTMLInputElement; + const titleInput = document.getElementById('meta-title') as HTMLInputElement; + const authorInput = document.getElementById( + 'meta-author' + ) as HTMLInputElement; + const subjectInput = document.getElementById( + 'meta-subject' + ) as HTMLInputElement; + const keywordsInput = document.getElementById( + 'meta-keywords' + ) as HTMLInputElement; + const creatorInput = document.getElementById( + 'meta-creator' + ) as HTMLInputElement; + const producerInput = document.getElementById( + 'meta-producer' + ) as HTMLInputElement; + const creationDateInput = document.getElementById( + 'meta-creation-date' + ) as HTMLInputElement; + const modDateInput = document.getElementById( + 'meta-mod-date' + ) as HTMLInputElement; - if (titleInput) titleInput.value = pageState.pdfDoc.getTitle() || ''; - if (authorInput) authorInput.value = pageState.pdfDoc.getAuthor() || ''; - if (subjectInput) subjectInput.value = pageState.pdfDoc.getSubject() || ''; - if (keywordsInput) keywordsInput.value = pageState.pdfDoc.getKeywords() || ''; - if (creatorInput) creatorInput.value = pageState.pdfDoc.getCreator() || ''; - if (producerInput) producerInput.value = pageState.pdfDoc.getProducer() || ''; - if (creationDateInput) creationDateInput.value = formatDateForInput(pageState.pdfDoc.getCreationDate()); - if (modDateInput) modDateInput.value = formatDateForInput(pageState.pdfDoc.getModificationDate()); + if (titleInput) titleInput.value = pageState.pdfDoc.getTitle() || ''; + if (authorInput) authorInput.value = pageState.pdfDoc.getAuthor() || ''; + if (subjectInput) subjectInput.value = pageState.pdfDoc.getSubject() || ''; + if (keywordsInput) keywordsInput.value = pageState.pdfDoc.getKeywords() || ''; + if (creatorInput) creatorInput.value = pageState.pdfDoc.getCreator() || ''; + if (producerInput) producerInput.value = pageState.pdfDoc.getProducer() || ''; + if (creationDateInput) + creationDateInput.value = formatDateForInput( + pageState.pdfDoc.getCreationDate() + ); + if (modDateInput) + modDateInput.value = formatDateForInput( + pageState.pdfDoc.getModificationDate() + ); - // Load custom fields - const customFieldsContainer = document.getElementById('custom-fields-container'); - if (customFieldsContainer) customFieldsContainer.innerHTML = ''; + // Load custom fields + const customFieldsContainer = document.getElementById( + 'custom-fields-container' + ); + if (customFieldsContainer) customFieldsContainer.innerHTML = ''; - try { - // @ts-expect-error getInfoDict is private but accessible at runtime - const infoDict = pageState.pdfDoc.getInfoDict(); - const standardKeys = new Set([ - 'Title', 'Author', 'Subject', 'Keywords', 'Creator', - 'Producer', 'CreationDate', 'ModDate' - ]); + try { + // @ts-expect-error getInfoDict is private but accessible at runtime + const infoDict = pageState.pdfDoc.getInfoDict(); + const standardKeys = new Set([ + 'Title', + 'Author', + 'Subject', + 'Keywords', + 'Creator', + 'Producer', + 'CreationDate', + 'ModDate', + ]); - const allKeys = infoDict - .keys() - .map(function (key: { asString: () => string }) { - return key.asString().substring(1); - }); + const allKeys = infoDict.keys().map(function (key: { + asString: () => string; + }) { + return key.asString().substring(1); + }); - allKeys.forEach(function (key: string) { - if (!standardKeys.has(key)) { - const rawValue = infoDict.lookup(key); - let displayValue = ''; + allKeys.forEach(function (key: string) { + if (!standardKeys.has(key)) { + const rawValue = infoDict.lookup(key); + let displayValue = ''; - if (rawValue && typeof rawValue.decodeText === 'function') { - displayValue = rawValue.decodeText(); - } else if (rawValue && typeof rawValue.asString === 'function') { - displayValue = rawValue.asString(); - } else if (rawValue) { - displayValue = String(rawValue); - } + if (rawValue && typeof rawValue.decodeText === 'function') { + displayValue = rawValue.decodeText(); + } else if (rawValue && typeof rawValue.asString === 'function') { + displayValue = rawValue.asString(); + } else if (rawValue) { + displayValue = String(rawValue); + } - addCustomFieldRow(key, displayValue); - } - }); - } catch (e) { - console.warn('Could not read custom metadata fields:', e); - } + addCustomFieldRow(key, displayValue); + } + }); + } catch (e) { + console.warn('Could not read custom metadata fields:', e); + } } 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'); - 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.file) { + 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 = pageState.file.name; - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`; + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`; - 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 () { + resetState(); + }; - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); - try { - showLoader('Loading PDF...'); - const arrayBuffer = await pageState.file.arrayBuffer(); - pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, { - ignoreEncryption: true, - throwOnInvalidObject: false - }); - hideLoader(); + try { + const result = await loadPdfWithPasswordPrompt(pageState.file); + if (!result) { + resetState(); + return; + } + showLoader('Loading PDF...'); + result.pdf.destroy(); + pageState.file = result.file; + pageState.pdfDoc = await PDFLibDocument.load(result.bytes, { + throwOnInvalidObject: false, + }); + hideLoader(); - const pageCount = pageState.pdfDoc.getPageCount(); - metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`; + const pageCount = pageState.pdfDoc.getPageCount(); + metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`; - populateMetadataFields(); + populateMetadataFields(); - if (toolOptions) toolOptions.classList.remove('hidden'); - } catch (error) { - console.error('Error loading PDF:', error); - hideLoader(); - showAlert('Error', 'Failed to load PDF file.'); - resetState(); - } - } else { - if (toolOptions) toolOptions.classList.add('hidden'); + if (toolOptions) toolOptions.classList.remove('hidden'); + } catch (error) { + console.error('Error loading PDF:', error); + hideLoader(); + showAlert('Error', 'Failed to load PDF file.'); + resetState(); } + } else { + if (toolOptions) toolOptions.classList.add('hidden'); + } } async function saveMetadata() { - if (!pageState.pdfDoc || !pageState.file) { - showAlert('Error', 'Please upload a PDF first.'); - return; + if (!pageState.pdfDoc || !pageState.file) { + showAlert('Error', 'Please upload a PDF first.'); + return; + } + + showLoader('Updating metadata...'); + + try { + const titleInput = document.getElementById( + 'meta-title' + ) as HTMLInputElement; + const authorInput = document.getElementById( + 'meta-author' + ) as HTMLInputElement; + const subjectInput = document.getElementById( + 'meta-subject' + ) as HTMLInputElement; + const keywordsInput = document.getElementById( + 'meta-keywords' + ) as HTMLInputElement; + const creatorInput = document.getElementById( + 'meta-creator' + ) as HTMLInputElement; + const producerInput = document.getElementById( + 'meta-producer' + ) as HTMLInputElement; + const creationDateInput = document.getElementById( + 'meta-creation-date' + ) as HTMLInputElement; + const modDateInput = document.getElementById( + 'meta-mod-date' + ) as HTMLInputElement; + + pageState.pdfDoc.setTitle(titleInput.value); + pageState.pdfDoc.setAuthor(authorInput.value); + pageState.pdfDoc.setSubject(subjectInput.value); + pageState.pdfDoc.setCreator(creatorInput.value); + pageState.pdfDoc.setProducer(producerInput.value); + + const keywords = keywordsInput.value; + pageState.pdfDoc.setKeywords( + keywords + .split(',') + .map(function (k) { + return k.trim(); + }) + .filter(Boolean) + ); + + // Handle creation date + if (creationDateInput.value) { + pageState.pdfDoc.setCreationDate(new Date(creationDateInput.value)); } - showLoader('Updating metadata...'); - - try { - const titleInput = document.getElementById('meta-title') as HTMLInputElement; - const authorInput = document.getElementById('meta-author') as HTMLInputElement; - const subjectInput = document.getElementById('meta-subject') as HTMLInputElement; - const keywordsInput = document.getElementById('meta-keywords') as HTMLInputElement; - const creatorInput = document.getElementById('meta-creator') as HTMLInputElement; - const producerInput = document.getElementById('meta-producer') as HTMLInputElement; - const creationDateInput = document.getElementById('meta-creation-date') as HTMLInputElement; - const modDateInput = document.getElementById('meta-mod-date') as HTMLInputElement; - - pageState.pdfDoc.setTitle(titleInput.value); - pageState.pdfDoc.setAuthor(authorInput.value); - pageState.pdfDoc.setSubject(subjectInput.value); - pageState.pdfDoc.setCreator(creatorInput.value); - pageState.pdfDoc.setProducer(producerInput.value); - - const keywords = keywordsInput.value; - pageState.pdfDoc.setKeywords( - keywords - .split(',') - .map(function (k) { return k.trim(); }) - .filter(Boolean) - ); - - // Handle creation date - if (creationDateInput.value) { - pageState.pdfDoc.setCreationDate(new Date(creationDateInput.value)); - } - - // Handle modification date - if (modDateInput.value) { - pageState.pdfDoc.setModificationDate(new Date(modDateInput.value)); - } else { - pageState.pdfDoc.setModificationDate(new Date()); - } - - // Handle custom fields - // @ts-expect-error getInfoDict is private but accessible at runtime - const infoDict = pageState.pdfDoc.getInfoDict(); - const standardKeys = new Set([ - 'Title', 'Author', 'Subject', 'Keywords', 'Creator', - 'Producer', 'CreationDate', 'ModDate' - ]); - - // Remove existing custom keys - const allKeys = infoDict - .keys() - .map(function (key: { asString: () => string }) { - return key.asString().substring(1); - }); - - allKeys.forEach(function (key: string) { - if (!standardKeys.has(key)) { - infoDict.delete(PDFName.of(key)); - } - }); - - // Add new custom fields - const customKeys = document.querySelectorAll('.custom-meta-key'); - const customValues = document.querySelectorAll('.custom-meta-value'); - - customKeys.forEach(function (keyInput, index) { - const key = (keyInput as HTMLInputElement).value.trim(); - const value = (customValues[index] as HTMLInputElement).value.trim(); - if (key && value) { - infoDict.set(PDFName.of(key), PDFString.of(value)); - } - }); - - const newPdfBytes = await pageState.pdfDoc.save(); - const originalName = pageState.file.name.replace(/\.pdf$/i, ''); - - downloadFile( - new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), - `${originalName}_metadata-edited.pdf` - ); - - showAlert('Success', 'Metadata updated successfully!', 'success', function () { - resetState(); - }); - } catch (e) { - console.error(e); - showAlert('Error', 'Could not update metadata. Please check that date formats are correct.'); - } finally { - hideLoader(); + // Handle modification date + if (modDateInput.value) { + pageState.pdfDoc.setModificationDate(new Date(modDateInput.value)); + } else { + pageState.pdfDoc.setModificationDate(new Date()); } + + // Handle custom fields + // @ts-expect-error getInfoDict is private but accessible at runtime + const infoDict = pageState.pdfDoc.getInfoDict(); + const standardKeys = new Set([ + 'Title', + 'Author', + 'Subject', + 'Keywords', + 'Creator', + 'Producer', + 'CreationDate', + 'ModDate', + ]); + + // Remove existing custom keys + const allKeys = infoDict.keys().map(function (key: { + asString: () => string; + }) { + return key.asString().substring(1); + }); + + allKeys.forEach(function (key: string) { + if (!standardKeys.has(key)) { + infoDict.delete(PDFName.of(key)); + } + }); + + // Add new custom fields + const customKeys = document.querySelectorAll('.custom-meta-key'); + const customValues = document.querySelectorAll('.custom-meta-value'); + + customKeys.forEach(function (keyInput, index) { + const key = (keyInput as HTMLInputElement).value.trim(); + const value = (customValues[index] as HTMLInputElement).value.trim(); + if (key && value) { + infoDict.set(PDFName.of(key), PDFString.of(value)); + } + }); + + const newPdfBytes = await pageState.pdfDoc.save(); + const originalName = pageState.file.name.replace(/\.pdf$/i, ''); + + downloadFile( + new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), + `${originalName}_metadata-edited.pdf` + ); + + showAlert( + 'Success', + 'Metadata updated successfully!', + 'success', + function () { + resetState(); + } + ); + } catch (e) { + console.error(e); + showAlert( + 'Error', + 'Could not update metadata. Please check that date formats are correct.' + ); + } finally { + hideLoader(); + } } 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 file = files[0]; + if ( + file.type === 'application/pdf' || + file.name.toLowerCase().endsWith('.pdf') + ) { + pageState.file = file; + updateUI(); } + } } 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 addCustomFieldBtn = document.getElementById('add-custom-field'); + 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 addCustomFieldBtn = document.getElementById('add-custom-field'); - 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 (addCustomFieldBtn) { + addCustomFieldBtn.addEventListener('click', function () { + addCustomFieldRow(); + }); + } + + 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('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); + } + } + }); - if (addCustomFieldBtn) { - addCustomFieldBtn.addEventListener('click', function () { - addCustomFieldRow(); - }); - } + fileInput.addEventListener('click', function () { + fileInput.value = ''; + }); + } - 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('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); - } - } - }); - - fileInput.addEventListener('click', function () { - fileInput.value = ''; - }); - } - - if (processBtn) { - processBtn.addEventListener('click', saveMetadata); - } + if (processBtn) { + processBtn.addEventListener('click', saveMetadata); + } }); diff --git a/src/js/logic/edit-pdf-page.ts b/src/js/logic/edit-pdf-page.ts index 25041eb..1a0c986 100644 --- a/src/js/logic/edit-pdf-page.ts +++ b/src/js/logic/edit-pdf-page.ts @@ -3,6 +3,7 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; import { formatBytes, downloadFile } from '../utils/helpers.js'; import { makeUniqueFileKey } from '../utils/deduplicate-filename.js'; +import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; const embedPdfWasmUrl = new URL( 'embedpdf-snippet/dist/pdfium.wasm', @@ -112,8 +113,17 @@ async function handleFiles(files: FileList) { if (!pdfWrapper || !pdfContainer || !fileDisplayArea) return; + hideLoader(); + const decryptedFiles = await batchDecryptIfNeeded(pdfFiles); + showLoader('Loading PDF Editor...'); + + if (decryptedFiles.length === 0) { + hideLoader(); + return; + } + if (!isViewerInitialized) { - const firstFile = pdfFiles[0]; + const firstFile = decryptedFiles[0]; const firstBuffer = await firstFile.arrayBuffer(); pdfContainer.textContent = ''; @@ -163,7 +173,7 @@ async function handleFiles(files: FileList) { } }); - addFileEntries(fileDisplayArea, pdfFiles); + addFileEntries(fileDisplayArea, decryptedFiles); docManagerPlugin.openDocumentBuffer({ buffer: firstBuffer, @@ -171,11 +181,11 @@ async function handleFiles(files: FileList) { autoActivate: true, }); - for (let i = 1; i < pdfFiles.length; i++) { - const buffer = await pdfFiles[i].arrayBuffer(); + for (let i = 1; i < decryptedFiles.length; i++) { + const buffer = await decryptedFiles[i].arrayBuffer(); docManagerPlugin.openDocumentBuffer({ buffer, - name: makeUniqueFileKey(i, pdfFiles[i].name), + name: makeUniqueFileKey(i, decryptedFiles[i].name), autoActivate: false, }); } @@ -214,13 +224,13 @@ async function handleFiles(files: FileList) { }); } } else { - addFileEntries(fileDisplayArea, pdfFiles); + addFileEntries(fileDisplayArea, decryptedFiles); - for (let i = 0; i < pdfFiles.length; i++) { - const buffer = await pdfFiles[i].arrayBuffer(); + for (let i = 0; i < decryptedFiles.length; i++) { + const buffer = await decryptedFiles[i].arrayBuffer(); docManagerPlugin.openDocumentBuffer({ buffer, - name: makeUniqueFileKey(i, pdfFiles[i].name), + name: makeUniqueFileKey(i, decryptedFiles[i].name), autoActivate: true, }); } diff --git a/src/js/logic/extract-attachments-page.ts b/src/js/logic/extract-attachments-page.ts index 3707117..9ff70a1 100644 --- a/src/js/logic/extract-attachments-page.ts +++ b/src/js/logic/extract-attachments-page.ts @@ -1,4 +1,4 @@ -import { showAlert } from '../ui.js'; +import { showAlert, showLoader } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; import JSZip from 'jszip'; @@ -7,6 +7,7 @@ import { showWasmRequiredDialog, WasmProvider, } from '../utils/wasm-provider.js'; +import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; const worker = new Worker( import.meta.env.BASE_URL + 'workers/extract-attachments.worker.js' @@ -198,6 +199,9 @@ async function extractAttachments() { showStatus('Reading files...', 'info'); try { + pageState.files = await batchDecryptIfNeeded(pageState.files); + showLoader('Reading files...'); + const fileBuffers: ArrayBuffer[] = []; const fileNames: string[] = []; @@ -207,6 +211,14 @@ async function extractAttachments() { fileNames.push(file.name); } + if (fileBuffers.length === 0) { + if (processBtn) { + processBtn.classList.remove('opacity-50', 'cursor-not-allowed'); + processBtn.removeAttribute('disabled'); + } + return; + } + showStatus( `Extracting attachments from ${pageState.files.length} file(s)...`, 'info' diff --git a/src/js/logic/extract-images-page.ts b/src/js/logic/extract-images-page.ts index aaab5d1..f9dd28c 100644 --- a/src/js/logic/extract-images-page.ts +++ b/src/js/logic/extract-images-page.ts @@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; interface ExtractedImage { data: Uint8Array; @@ -158,7 +159,9 @@ document.addEventListener('DOMContentLoaded', () => { return; } + const decryptedFiles = await batchDecryptIfNeeded(state.files); showLoader('Loading PDF processor...'); + state.files = decryptedFiles; const pymupdf = await loadPyMuPDF(); extractedImages = []; diff --git a/src/js/logic/extract-pages-page.ts b/src/js/logic/extract-pages-page.ts index 5d7c02b..9a3a6fc 100644 --- a/src/js/logic/extract-pages-page.ts +++ b/src/js/logic/extract-pages-page.ts @@ -1,209 +1,235 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; -import { readFileAsArrayBuffer, formatBytes, downloadFile, parsePageRanges } from '../utils/helpers.js'; +import { + formatBytes, + downloadFile, + parsePageRanges, +} from '../utils/helpers.js'; import { PDFDocument } from 'pdf-lib'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import JSZip from 'jszip'; interface ExtractState { - file: File | null; - pdfDoc: any; - totalPages: number; + file: File | null; + pdfDoc: any; + totalPages: number; } const extractState: ExtractState = { - file: null, - pdfDoc: null, - totalPages: 0, + file: null, + pdfDoc: null, + totalPages: 0, }; if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializePage); + document.addEventListener('DOMContentLoaded', initializePage); } else { - initializePage(); + initializePage(); } function initializePage() { - createIcons({ icons }); + createIcons({ icons }); - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const processBtn = document.getElementById('process-btn'); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const processBtn = document.getElementById('process-btn'); - if (fileInput) { - fileInput.addEventListener('change', handleFileUpload); - } + if (fileInput) { + fileInput.addEventListener('change', handleFileUpload); + } - if (dropZone) { - dropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - dropZone.classList.add('bg-gray-700'); - }); - - dropZone.addEventListener('dragleave', () => { - dropZone.classList.remove('bg-gray-700'); - }); - - dropZone.addEventListener('drop', (e) => { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - const droppedFiles = e.dataTransfer?.files; - if (droppedFiles && droppedFiles.length > 0) { - handleFile(droppedFiles[0]); - } - }); - - // Clear value on click to allow re-selecting the same file - fileInput?.addEventListener('click', () => { - if (fileInput) fileInput.value = ''; - }); - } - - if (processBtn) { - processBtn.addEventListener('click', extractPages); - } - - document.getElementById('back-to-tools')?.addEventListener('click', () => { - window.location.href = import.meta.env.BASE_URL; + if (dropZone) { + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); }); + + dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('bg-gray-700'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + const droppedFiles = e.dataTransfer?.files; + if (droppedFiles && droppedFiles.length > 0) { + handleFile(droppedFiles[0]); + } + }); + + // Clear value on click to allow re-selecting the same file + fileInput?.addEventListener('click', () => { + if (fileInput) fileInput.value = ''; + }); + } + + if (processBtn) { + processBtn.addEventListener('click', extractPages); + } + + document.getElementById('back-to-tools')?.addEventListener('click', () => { + window.location.href = import.meta.env.BASE_URL; + }); } function handleFileUpload(e: Event) { - const input = e.target as HTMLInputElement; - if (input.files && input.files.length > 0) { - handleFile(input.files[0]); - } + const input = e.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + handleFile(input.files[0]); + } } async function handleFile(file: File) { - if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) { - showAlert('Invalid File', 'Please select a PDF file.'); - return; - } + if ( + file.type !== 'application/pdf' && + !file.name.toLowerCase().endsWith('.pdf') + ) { + showAlert('Invalid File', 'Please select a PDF file.'); + return; + } + extractState.file = file; + + try { + const result = await loadPdfWithPasswordPrompt(file); + if (!result) { + extractState.file = null; + return; + } showLoader('Loading PDF...'); - extractState.file = file; + extractState.file = result.file; + result.pdf.destroy(); + extractState.pdfDoc = await PDFDocument.load(result.bytes, { + throwOnInvalidObject: false, + }); + extractState.totalPages = extractState.pdfDoc.getPageCount(); - try { - const arrayBuffer = await readFileAsArrayBuffer(file); - extractState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { - ignoreEncryption: true, - throwOnInvalidObject: false, - }); - extractState.totalPages = extractState.pdfDoc.getPageCount(); - - updateFileDisplay(); - showOptions(); - hideLoader(); - } catch (error) { - console.error('Error loading PDF:', error); - hideLoader(); - showAlert('Error', 'Failed to load PDF file.'); - } + updateFileDisplay(); + showOptions(); + hideLoader(); + } catch (error) { + console.error('Error loading PDF:', error); + hideLoader(); + showAlert('Error', 'Failed to load PDF file.'); + } } function updateFileDisplay() { - const fileDisplayArea = document.getElementById('file-display-area'); - if (!fileDisplayArea || !extractState.file) return; + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea || !extractState.file) return; - fileDisplayArea.innerHTML = ''; - const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; + fileDisplayArea.innerHTML = ''; + const fileDiv = document.createElement('div'); + fileDiv.className = + 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; - const infoContainer = document.createElement('div'); - infoContainer.className = 'flex flex-col flex-1 min-w-0'; + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col flex-1 min-w-0'; - const nameSpan = document.createElement('div'); - nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; - nameSpan.textContent = extractState.file.name; + const nameSpan = document.createElement('div'); + nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; + nameSpan.textContent = extractState.file.name; - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(extractState.file.size)} • ${extractState.totalPages} pages`; + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = `${formatBytes(extractState.file.size)} • ${extractState.totalPages} pages`; - 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 = () => resetState(); + const removeBtn = document.createElement('button'); + removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.innerHTML = ''; + removeBtn.onclick = () => resetState(); - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); } function showOptions() { - const extractOptions = document.getElementById('extract-options'); - const totalPagesSpan = document.getElementById('total-pages'); + const extractOptions = document.getElementById('extract-options'); + const totalPagesSpan = document.getElementById('total-pages'); - if (extractOptions) { - extractOptions.classList.remove('hidden'); - } - if (totalPagesSpan) { - totalPagesSpan.textContent = extractState.totalPages.toString(); - } + if (extractOptions) { + extractOptions.classList.remove('hidden'); + } + if (totalPagesSpan) { + totalPagesSpan.textContent = extractState.totalPages.toString(); + } } - - async function extractPages() { - const pagesInput = document.getElementById('pages-to-extract') as HTMLInputElement; - if (!pagesInput || !pagesInput.value.trim()) { - showAlert('No Pages', 'Please enter page numbers to extract.'); - return; + const pagesInput = document.getElementById( + 'pages-to-extract' + ) as HTMLInputElement; + if (!pagesInput || !pagesInput.value.trim()) { + showAlert('No Pages', 'Please enter page numbers to extract.'); + return; + } + + const pagesToExtract = parsePageRanges( + pagesInput.value, + extractState.totalPages + ).map((i) => i + 1); + if (pagesToExtract.length === 0) { + showAlert('Invalid Pages', 'No valid page numbers found.'); + return; + } + + showLoader('Extracting pages...'); + + try { + const zip = new JSZip(); + const baseName = extractState.file?.name.replace('.pdf', '') || 'document'; + + for (const pageNum of pagesToExtract) { + const newPdf = await PDFDocument.create(); + const [copiedPage] = await newPdf.copyPages(extractState.pdfDoc, [ + pageNum - 1, + ]); + newPdf.addPage(copiedPage); + const pdfBytes = await newPdf.save(); + zip.file(`${baseName}_page_${pageNum}.pdf`, pdfBytes); } - const pagesToExtract = parsePageRanges(pagesInput.value, extractState.totalPages).map(i => i + 1); - if (pagesToExtract.length === 0) { - showAlert('Invalid Pages', 'No valid page numbers found.'); - return; - } + const zipBlob = await zip.generateAsync({ type: 'blob' }); + downloadFile(zipBlob, `${baseName}_extracted_pages.zip`); - showLoader('Extracting pages...'); - - try { - const zip = new JSZip(); - const baseName = extractState.file?.name.replace('.pdf', '') || 'document'; - - for (const pageNum of pagesToExtract) { - const newPdf = await PDFDocument.create(); - const [copiedPage] = await newPdf.copyPages(extractState.pdfDoc, [pageNum - 1]); - newPdf.addPage(copiedPage); - const pdfBytes = await newPdf.save(); - zip.file(`${baseName}_page_${pageNum}.pdf`, pdfBytes); - } - - const zipBlob = await zip.generateAsync({ type: 'blob' }); - downloadFile(zipBlob, `${baseName}_extracted_pages.zip`); - - hideLoader(); - showAlert('Success', `Extracted ${pagesToExtract.length} page(s) successfully!`, 'success', () => { - resetState(); - }); - } catch (error) { - console.error('Error extracting pages:', error); - hideLoader(); - showAlert('Error', 'Failed to extract pages.'); - } + hideLoader(); + showAlert( + 'Success', + `Extracted ${pagesToExtract.length} page(s) successfully!`, + 'success', + () => { + resetState(); + } + ); + } catch (error) { + console.error('Error extracting pages:', error); + hideLoader(); + showAlert('Error', 'Failed to extract pages.'); + } } function resetState() { - extractState.file = null; - extractState.pdfDoc = null; - extractState.totalPages = 0; + extractState.file = null; + extractState.pdfDoc = null; + extractState.totalPages = 0; - const extractOptions = document.getElementById('extract-options'); - if (extractOptions) { - extractOptions.classList.add('hidden'); - } + const extractOptions = document.getElementById('extract-options'); + if (extractOptions) { + extractOptions.classList.add('hidden'); + } - const fileDisplayArea = document.getElementById('file-display-area'); - if (fileDisplayArea) { - fileDisplayArea.innerHTML = ''; - } + const fileDisplayArea = document.getElementById('file-display-area'); + if (fileDisplayArea) { + fileDisplayArea.innerHTML = ''; + } - const pagesInput = document.getElementById('pages-to-extract') as HTMLInputElement; - if (pagesInput) { - pagesInput.value = ''; - } + const pagesInput = document.getElementById( + 'pages-to-extract' + ) as HTMLInputElement; + if (pagesInput) { + pagesInput.value = ''; + } } diff --git a/src/js/logic/extract-tables-page.ts b/src/js/logic/extract-tables-page.ts index d49d359..c7bbb8b 100644 --- a/src/js/logic/extract-tables-page.ts +++ b/src/js/logic/extract-tables-page.ts @@ -5,6 +5,7 @@ import JSZip from 'jszip'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; let file: File | null = null; const updateUI = () => { @@ -93,6 +94,13 @@ async function extract() { try { showLoader('Loading Engine...'); const pymupdf = await loadPyMuPDF(); + + hideLoader(); + const pwResult = await loadPdfWithPasswordPrompt(file); + if (!pwResult) return; + pwResult.pdf.destroy(); + file = pwResult.file; + showLoader('Extracting tables...'); const doc = await pymupdf.open(file); diff --git a/src/js/logic/fix-page-size-page.ts b/src/js/logic/fix-page-size-page.ts index 976c771..0f77401 100644 --- a/src/js/logic/fix-page-size-page.ts +++ b/src/js/logic/fix-page-size-page.ts @@ -1,6 +1,7 @@ import { showAlert } from '../ui.js'; import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js'; import { fixPageSize as fixPageSizeCore } from '../utils/pdf-operations'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { icons, createIcons } from 'lucide'; import { FixPageSizeState } from '@/types'; @@ -64,14 +65,17 @@ async function updateUI() { } } -function handleFileSelect(files: FileList | null) { +async 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; + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; + result.pdf.destroy(); + pageState.file = result.file; updateUI(); } } diff --git a/src/js/logic/flatten-pdf-page.ts b/src/js/logic/flatten-pdf-page.ts index 635402f..cb2ed65 100644 --- a/src/js/logic/flatten-pdf-page.ts +++ b/src/js/logic/flatten-pdf-page.ts @@ -1,9 +1,6 @@ import { showAlert } from '../ui.js'; -import { - downloadFile, - formatBytes, - readFileAsArrayBuffer, -} from '../utils/helpers.js'; +import { downloadFile, formatBytes } from '../utils/helpers.js'; +import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; import { PDFDocument } from 'pdf-lib'; import { flattenAnnotations } from '../utils/flatten-annotations.js'; import { icons, createIcons } from 'lucide'; @@ -109,23 +106,22 @@ async function flattenPdf() { const loaderModal = document.getElementById('loader-modal'); const loaderText = document.getElementById('loader-text'); + pageState.files = await batchDecryptIfNeeded(pageState.files); + try { if (pageState.files.length === 1) { if (loaderModal) loaderModal.classList.remove('hidden'); if (loaderText) loaderText.textContent = 'Flattening PDF...'; const file = pageState.files[0]; - const arrayBuffer = await readFileAsArrayBuffer(file); - const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { - ignoreEncryption: true, - }); + const arrayBuffer = await file.arrayBuffer(); + const pdfDoc = await PDFDocument.load(arrayBuffer); try { flattenFormsInDoc(pdfDoc); - } catch (e: any) { - if (e.message.includes('getForm')) { - // Ignore if no form found - } else { + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + if (!msg.includes('getForm')) { throw e; } } @@ -157,17 +153,14 @@ async function flattenPdf() { loaderText.textContent = `Flattening ${i + 1}/${pageState.files.length}: ${file.name}...`; try { - const arrayBuffer = await readFileAsArrayBuffer(file); - const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { - ignoreEncryption: true, - }); + const arrayBuffer = await file.arrayBuffer(); + const pdfDoc = await PDFDocument.load(arrayBuffer); try { flattenFormsInDoc(pdfDoc); - } catch (e: any) { - if (e.message.includes('getForm')) { - // Ignore if no form found - } else { + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + if (!msg.includes('getForm')) { throw e; } } @@ -207,10 +200,12 @@ async function flattenPdf() { } if (loaderModal) loaderModal.classList.add('hidden'); } - } catch (e: any) { + } catch (e: unknown) { console.error(e); if (loaderModal) loaderModal.classList.add('hidden'); - showAlert('Error', e.message || 'An unexpected error occurred.'); + const errorMessage = + e instanceof Error ? e.message : 'An unexpected error occurred.'; + showAlert('Error', errorMessage); } } diff --git a/src/js/logic/font-to-outline-page.ts b/src/js/logic/font-to-outline-page.ts index d5df991..884188f 100644 --- a/src/js/logic/font-to-outline-page.ts +++ b/src/js/logic/font-to-outline-page.ts @@ -3,6 +3,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js'; import { convertFileToOutlines } from '../utils/ghostscript-loader.js'; import { isGhostscriptAvailable } from '../utils/ghostscript-dynamic-loader.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; +import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; import { icons, createIcons } from 'lucide'; import JSZip from 'jszip'; import { deduplicateFileName } from '../utils/deduplicate-filename.js'; @@ -107,6 +108,8 @@ async function processFiles() { return; } + pageState.files = await batchDecryptIfNeeded(pageState.files); + const loaderModal = document.getElementById('loader-modal'); const loaderText = document.getElementById('loader-text'); diff --git a/src/js/logic/form-creator.ts b/src/js/logic/form-creator.ts index 9e24907..994708e 100644 --- a/src/js/logic/form-creator.ts +++ b/src/js/logic/form-creator.ts @@ -29,6 +29,7 @@ type PdfViewerWindow = Window & { import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'; import { downloadFile, hexToRgb, getPDFDocument } from '../utils/helpers.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { createIcons, icons } from 'lucide'; import * as pdfjsLib from 'pdfjs-dist'; import type { PDFDocumentProxy } from 'pdfjs-dist'; @@ -3135,7 +3136,10 @@ function extractExistingFields(pdfDoc: PDFDocument): void { async function handlePdfUpload(file: File) { try { - const arrayBuffer = await file.arrayBuffer(); + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; + const arrayBuffer = result.bytes; + uploadedPdfjsDoc = result.pdf; uploadedPdfDoc = await PDFDocument.load(arrayBuffer); // Check for existing fields and update counter @@ -3173,8 +3177,6 @@ async function handlePdfUpload(file: File) { console.log('No form fields found or error reading fields:', e); } - uploadedPdfjsDoc = await getPDFDocument({ data: arrayBuffer }).promise; - const pageCount = uploadedPdfDoc.getPageCount(); pages = []; diff --git a/src/js/logic/form-filler-page.ts b/src/js/logic/form-filler-page.ts index e7c58af..3ad9e5f 100644 --- a/src/js/logic/form-filler-page.ts +++ b/src/js/logic/form-filler-page.ts @@ -1,6 +1,7 @@ // Self-contained Form Filler logic for standalone page import { createIcons, icons } from 'lucide'; import { getPDFDocument } from '../utils/helpers.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; let viewerIframe: HTMLIFrameElement | null = null; let viewerReady = false; @@ -8,46 +9,52 @@ let currentFile: File | null = null; // UI helpers function showLoader(message: string = 'Processing...') { - const loader = document.getElementById('loader-modal'); - const loaderText = document.getElementById('loader-text'); - if (loader) loader.classList.remove('hidden'); - if (loaderText) loaderText.textContent = message; + const loader = document.getElementById('loader-modal'); + const loaderText = document.getElementById('loader-text'); + if (loader) loader.classList.remove('hidden'); + if (loaderText) loaderText.textContent = message; } function hideLoader() { - const loader = document.getElementById('loader-modal'); - if (loader) loader.classList.add('hidden'); + const loader = document.getElementById('loader-modal'); + if (loader) loader.classList.add('hidden'); } -function showAlert(title: string, message: string, type: string = 'error', callback?: () => void) { - const modal = document.getElementById('alert-modal'); - const alertTitle = document.getElementById('alert-title'); - const alertMessage = document.getElementById('alert-message'); - const okBtn = document.getElementById('alert-ok'); +function showAlert( + title: string, + message: string, + type: string = 'error', + callback?: () => void +) { + const modal = document.getElementById('alert-modal'); + const alertTitle = document.getElementById('alert-title'); + const alertMessage = document.getElementById('alert-message'); + const okBtn = document.getElementById('alert-ok'); - if (alertTitle) alertTitle.textContent = title; - if (alertMessage) alertMessage.textContent = message; - if (modal) modal.classList.remove('hidden'); + if (alertTitle) alertTitle.textContent = title; + if (alertMessage) alertMessage.textContent = message; + if (modal) modal.classList.remove('hidden'); - if (okBtn) { - const newOkBtn = okBtn.cloneNode(true) as HTMLElement; - okBtn.replaceWith(newOkBtn); - newOkBtn.addEventListener('click', () => { - modal?.classList.add('hidden'); - if (callback) callback(); - }); - } + if (okBtn) { + const newOkBtn = okBtn.cloneNode(true) as HTMLElement; + okBtn.replaceWith(newOkBtn); + newOkBtn.addEventListener('click', () => { + modal?.classList.add('hidden'); + if (callback) callback(); + }); + } } function updateFileDisplay() { - const displayArea = document.getElementById('file-display-area'); - if (!displayArea || !currentFile) return; + const displayArea = document.getElementById('file-display-area'); + if (!displayArea || !currentFile) return; - const fileSize = currentFile.size < 1024 * 1024 - ? `${(currentFile.size / 1024).toFixed(1)} KB` - : `${(currentFile.size / 1024 / 1024).toFixed(2)} MB`; + const fileSize = + currentFile.size < 1024 * 1024 + ? `${(currentFile.size / 1024).toFixed(1)} KB` + : `${(currentFile.size / 1024 / 1024).toFixed(2)} MB`; - displayArea.innerHTML = ` + displayArea.innerHTML = `
@@ -61,196 +68,226 @@ function updateFileDisplay() {
`; - createIcons({ icons }); + createIcons({ icons }); - document.getElementById('remove-file')?.addEventListener('click', () => resetState()); + document + .getElementById('remove-file') + ?.addEventListener('click', () => resetState()); } function resetState() { - viewerIframe = null; - viewerReady = false; - currentFile = null; - const displayArea = document.getElementById('file-display-area'); - if (displayArea) displayArea.innerHTML = ''; - document.getElementById('form-filler-options')?.classList.add('hidden'); - const fileInput = document.getElementById('file-input') as HTMLInputElement; - if (fileInput) fileInput.value = ''; + viewerIframe = null; + viewerReady = false; + currentFile = null; + const displayArea = document.getElementById('file-display-area'); + if (displayArea) displayArea.innerHTML = ''; + document.getElementById('form-filler-options')?.classList.add('hidden'); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; - // Clear viewer - const viewerContainer = document.getElementById('pdf-viewer-container'); - if (viewerContainer) { - viewerContainer.innerHTML = ''; - viewerContainer.style.height = ''; - viewerContainer.style.aspectRatio = ''; - } + // Clear viewer + const viewerContainer = document.getElementById('pdf-viewer-container'); + if (viewerContainer) { + viewerContainer.innerHTML = ''; + viewerContainer.style.height = ''; + viewerContainer.style.aspectRatio = ''; + } - const toolUploader = document.getElementById('tool-uploader'); - const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false'; - if (toolUploader && !isFullWidth) { - toolUploader.classList.remove('max-w-6xl'); - toolUploader.classList.add('max-w-2xl'); - } + const toolUploader = document.getElementById('tool-uploader'); + const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false'; + if (toolUploader && !isFullWidth) { + toolUploader.classList.remove('max-w-6xl'); + toolUploader.classList.add('max-w-2xl'); + } } // File handling async function handleFileUpload(file: File) { - if (!file || file.type !== 'application/pdf') { - showAlert('Error', 'Please upload a valid PDF file.'); - return; - } + if (!file || file.type !== 'application/pdf') { + showAlert('Error', 'Please upload a valid PDF file.'); + return; + } - currentFile = file; + try { + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; + result.pdf.destroy(); + currentFile = result.file; updateFileDisplay(); await setupFormViewer(); + } catch (error) { + console.error(error); + showAlert('Error', 'Failed to load PDF file.'); + hideLoader(); + } } async function adjustViewerHeight(file: File) { - const viewerContainer = document.getElementById('pdf-viewer-container'); - if (!viewerContainer) return; + const viewerContainer = document.getElementById('pdf-viewer-container'); + if (!viewerContainer) return; - try { - const arrayBuffer = await file.arrayBuffer(); - const loadingTask = getPDFDocument({ data: arrayBuffer }); - const pdf = await loadingTask.promise; - const page = await pdf.getPage(1); - const viewport = page.getViewport({ scale: 1 }); + try { + const arrayBuffer = await file.arrayBuffer(); + const loadingTask = getPDFDocument({ data: arrayBuffer }); + const pdf = await loadingTask.promise; + const page = await pdf.getPage(1); + const viewport = page.getViewport({ scale: 1 }); - // Add ~50px for toolbar height - const aspectRatio = viewport.width / (viewport.height + 50); + // Add ~50px for toolbar height + const aspectRatio = viewport.width / (viewport.height + 50); - viewerContainer.style.height = 'auto'; - viewerContainer.style.aspectRatio = `${aspectRatio}`; - } catch (e) { - console.error('Error adjusting viewer height:', e); - viewerContainer.style.height = '80vh'; - } + viewerContainer.style.height = 'auto'; + viewerContainer.style.aspectRatio = `${aspectRatio}`; + } catch (e) { + console.error('Error adjusting viewer height:', e); + viewerContainer.style.height = '80vh'; + } } async function setupFormViewer() { - if (!currentFile) return; + if (!currentFile) return; - showLoader('Loading PDF form...'); - const pdfViewerContainer = document.getElementById('pdf-viewer-container'); + showLoader('Loading PDF form...'); + const pdfViewerContainer = document.getElementById('pdf-viewer-container'); - if (!pdfViewerContainer) { - console.error('PDF viewer container not found'); - hideLoader(); - return; - } + if (!pdfViewerContainer) { + console.error('PDF viewer container not found'); + hideLoader(); + return; + } - const toolUploader = document.getElementById('tool-uploader'); - // Default to true if not set - const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false'; - if (toolUploader && !isFullWidth) { - toolUploader.classList.remove('max-w-2xl'); - toolUploader.classList.add('max-w-6xl'); - } + const toolUploader = document.getElementById('tool-uploader'); + // Default to true if not set + const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false'; + if (toolUploader && !isFullWidth) { + toolUploader.classList.remove('max-w-2xl'); + toolUploader.classList.add('max-w-6xl'); + } - try { - // Apply dynamic height - await adjustViewerHeight(currentFile); + try { + // Apply dynamic height + await adjustViewerHeight(currentFile); - pdfViewerContainer.innerHTML = ''; + pdfViewerContainer.innerHTML = ''; - const arrayBuffer = await currentFile.arrayBuffer(); - const blob = new Blob([arrayBuffer], { type: 'application/pdf' }); - const blobUrl = URL.createObjectURL(blob); + const arrayBuffer = await currentFile.arrayBuffer(); + const blob = new Blob([arrayBuffer], { type: 'application/pdf' }); + const blobUrl = URL.createObjectURL(blob); - viewerIframe = document.createElement('iframe'); - viewerIframe.src = `${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html?file=${encodeURIComponent(blobUrl)}`; - viewerIframe.style.width = '100%'; - viewerIframe.style.height = '100%'; - viewerIframe.style.border = 'none'; + viewerIframe = document.createElement('iframe'); + viewerIframe.src = `${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html?file=${encodeURIComponent(blobUrl)}`; + viewerIframe.style.width = '100%'; + viewerIframe.style.height = '100%'; + viewerIframe.style.border = 'none'; - viewerIframe.onload = () => { - viewerReady = true; - hideLoader(); - }; + viewerIframe.onload = () => { + viewerReady = true; + hideLoader(); + }; - pdfViewerContainer.appendChild(viewerIframe); + pdfViewerContainer.appendChild(viewerIframe); - const formFillerOptions = document.getElementById('form-filler-options'); - if (formFillerOptions) formFillerOptions.classList.remove('hidden'); - } catch (e) { - console.error('Critical error setting up form filler:', e); - showAlert('Error', 'Failed to load PDF form viewer.'); - hideLoader(); - } + const formFillerOptions = document.getElementById('form-filler-options'); + if (formFillerOptions) formFillerOptions.classList.remove('hidden'); + } catch (e) { + console.error('Critical error setting up form filler:', e); + showAlert('Error', 'Failed to load PDF form viewer.'); + hideLoader(); + } } async function processAndDownloadForm() { - if (!viewerIframe || !viewerReady) { - showAlert('Viewer not ready', 'Please wait for the form to finish loading.'); - return; + if (!viewerIframe || !viewerReady) { + showAlert( + 'Viewer not ready', + 'Please wait for the form to finish loading.' + ); + return; + } + + try { + const viewerWindow = viewerIframe.contentWindow; + if (!viewerWindow) { + console.error('Cannot access iframe window'); + showAlert( + 'Download', + 'Please use the Download button in the PDF viewer toolbar above.' + ); + return; } - try { - const viewerWindow = viewerIframe.contentWindow; - if (!viewerWindow) { - console.error('Cannot access iframe window'); - showAlert('Download', 'Please use the Download button in the PDF viewer toolbar above.'); - return; - } - - const viewerDoc = viewerWindow.document; - if (!viewerDoc) { - console.error('Cannot access iframe document'); - showAlert('Download', 'Please use the Download button in the PDF viewer toolbar above.'); - return; - } - - const downloadBtn = viewerDoc.getElementById('downloadButton') as HTMLButtonElement | null; - - if (downloadBtn) { - console.log('Clicking download button...'); - downloadBtn.click(); - } else { - console.error('Download button not found in viewer'); - const secondaryDownload = viewerDoc.getElementById('secondaryDownload') as HTMLButtonElement | null; - if (secondaryDownload) { - console.log('Clicking secondary download button...'); - secondaryDownload.click(); - } else { - showAlert('Download', 'Please use the Download button in the PDF viewer toolbar above.'); - } - } - } catch (e) { - console.error('Failed to trigger download:', e); - showAlert('Download', 'Cannot access viewer controls. Please use the Download button in the PDF viewer toolbar above.'); + const viewerDoc = viewerWindow.document; + if (!viewerDoc) { + console.error('Cannot access iframe document'); + showAlert( + 'Download', + 'Please use the Download button in the PDF viewer toolbar above.' + ); + return; } + + const downloadBtn = viewerDoc.getElementById( + 'downloadButton' + ) as HTMLButtonElement | null; + + if (downloadBtn) { + console.log('Clicking download button...'); + downloadBtn.click(); + } else { + console.error('Download button not found in viewer'); + const secondaryDownload = viewerDoc.getElementById( + 'secondaryDownload' + ) as HTMLButtonElement | null; + if (secondaryDownload) { + console.log('Clicking secondary download button...'); + secondaryDownload.click(); + } else { + showAlert( + 'Download', + 'Please use the Download button in the PDF viewer toolbar above.' + ); + } + } + } catch (e) { + console.error('Failed to trigger download:', e); + showAlert( + 'Download', + 'Cannot access viewer controls. Please use the Download button in the PDF viewer toolbar above.' + ); + } } // Initialize document.addEventListener('DOMContentLoaded', () => { - 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 backBtn = document.getElementById('back-to-tools'); - fileInput?.addEventListener('change', (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; - if (file) handleFileUpload(file); - }); + fileInput?.addEventListener('change', (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (file) handleFileUpload(file); + }); - 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 file = e.dataTransfer?.files[0]; - if (file) handleFileUpload(file); - }); + dropZone?.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('border-indigo-500'); + const file = e.dataTransfer?.files[0]; + if (file) handleFileUpload(file); + }); - processBtn?.addEventListener('click', processAndDownloadForm); + processBtn?.addEventListener('click', processAndDownloadForm); - backBtn?.addEventListener('click', () => { - window.location.href = '../../index.html'; - }); + backBtn?.addEventListener('click', () => { + window.location.href = '../../index.html'; + }); }); diff --git a/src/js/logic/header-footer-page.ts b/src/js/logic/header-footer-page.ts index c2d0dc3..d342603 100644 --- a/src/js/logic/header-footer-page.ts +++ b/src/js/logic/header-footer-page.ts @@ -1,132 +1,260 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; -import { downloadFile, hexToRgb, formatBytes, parsePageRanges } from '../utils/helpers.js'; +import { + downloadFile, + hexToRgb, + formatBytes, + parsePageRanges, +} from '../utils/helpers.js'; import { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { HeaderFooterState } from '@/types'; const pageState: HeaderFooterState = { file: null, pdfDoc: null }; if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializePage); -} else { initializePage(); } - -function initializePage() { - createIcons({ icons }); - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const backBtn = document.getElementById('back-to-tools'); - const processBtn = document.getElementById('process-btn'); - - if (fileInput) { - fileInput.addEventListener('change', handleFileUpload); - fileInput.addEventListener('click', () => { fileInput.value = ''; }); - } - if (dropZone) { - dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-indigo-500'); }); - dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('border-indigo-500'); }); - dropZone.addEventListener('drop', (e) => { - e.preventDefault(); dropZone.classList.remove('border-indigo-500'); - if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files); - }); - } - if (backBtn) backBtn.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; }); - if (processBtn) processBtn.addEventListener('click', addHeaderFooter); + document.addEventListener('DOMContentLoaded', initializePage); +} else { + initializePage(); } -function handleFileUpload(e: Event) { const input = e.target as HTMLInputElement; if (input.files?.length) handleFiles(input.files); } +function initializePage() { + createIcons({ icons }); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const backBtn = document.getElementById('back-to-tools'); + const processBtn = document.getElementById('process-btn'); + + if (fileInput) { + fileInput.addEventListener('change', handleFileUpload); + fileInput.addEventListener('click', () => { + fileInput.value = ''; + }); + } + if (dropZone) { + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('border-indigo-500'); + }); + dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('border-indigo-500'); + }); + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('border-indigo-500'); + if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files); + }); + } + if (backBtn) + backBtn.addEventListener('click', () => { + window.location.href = import.meta.env.BASE_URL; + }); + if (processBtn) processBtn.addEventListener('click', addHeaderFooter); +} + +function handleFileUpload(e: Event) { + const input = e.target as HTMLInputElement; + if (input.files?.length) handleFiles(input.files); +} async function handleFiles(files: FileList) { - const file = files[0]; - if (!file || file.type !== 'application/pdf') { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; } + const file = files[0]; + if (!file || file.type !== 'application/pdf') { + showAlert('Invalid File', 'Please upload a valid PDF file.'); + return; + } + try { + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; showLoader('Loading PDF...'); - try { - const arrayBuffer = await file.arrayBuffer(); - pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer); - pageState.file = file; - updateFileDisplay(); - document.getElementById('options-panel')?.classList.remove('hidden'); - const totalPagesSpan = document.getElementById('total-pages'); - if (totalPagesSpan) totalPagesSpan.textContent = String(pageState.pdfDoc.getPageCount()); - } catch (error) { console.error(error); showAlert('Error', 'Failed to load PDF file.'); } - finally { hideLoader(); } + + pageState.pdfDoc = await PDFLibDocument.load(result.bytes); + pageState.file = result.file; + result.pdf.destroy(); + + updateFileDisplay(); + document.getElementById('options-panel')?.classList.remove('hidden'); + const totalPagesSpan = document.getElementById('total-pages'); + if (totalPagesSpan) + totalPagesSpan.textContent = String(pageState.pdfDoc.getPageCount()); + } catch (error) { + console.error(error); + showAlert('Error', 'Failed to load PDF file.'); + } finally { + hideLoader(); + } } function updateFileDisplay() { - const fileDisplayArea = document.getElementById('file-display-area'); - if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return; - fileDisplayArea.innerHTML = ''; - const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; - const infoContainer = document.createElement('div'); - infoContainer.className = 'flex flex-col flex-1 min-w-0'; - const nameSpan = document.createElement('div'); - nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; - nameSpan.textContent = pageState.file.name; - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`; - 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 = resetState; - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return; + fileDisplayArea.innerHTML = ''; + const fileDiv = document.createElement('div'); + fileDiv.className = + 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col flex-1 min-w-0'; + const nameSpan = document.createElement('div'); + nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; + nameSpan.textContent = pageState.file.name; + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`; + 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 = resetState; + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); } function resetState() { - pageState.file = null; pageState.pdfDoc = null; - const fileDisplayArea = document.getElementById('file-display-area'); - if (fileDisplayArea) fileDisplayArea.innerHTML = ''; - document.getElementById('options-panel')?.classList.add('hidden'); - const fileInput = document.getElementById('file-input') as HTMLInputElement; - if (fileInput) fileInput.value = ''; + pageState.file = null; + pageState.pdfDoc = null; + const fileDisplayArea = document.getElementById('file-display-area'); + if (fileDisplayArea) fileDisplayArea.innerHTML = ''; + document.getElementById('options-panel')?.classList.add('hidden'); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; } async function addHeaderFooter() { - if (!pageState.pdfDoc) { showAlert('Error', 'Please upload a PDF file first.'); return; } - showLoader('Adding header & footer...'); - try { - const helveticaFont = await pageState.pdfDoc.embedFont(StandardFonts.Helvetica); - const allPages = pageState.pdfDoc.getPages(); - const totalPages = allPages.length; - const margin = 40; - const fontSize = parseInt((document.getElementById('font-size') as HTMLInputElement)?.value || '10') || 10; - const colorHex = (document.getElementById('font-color') as HTMLInputElement)?.value || '#000000'; - const fontColor = hexToRgb(colorHex); - const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement)?.value || ''; - const texts = { - headerLeft: (document.getElementById('header-left') as HTMLInputElement)?.value || '', - headerCenter: (document.getElementById('header-center') as HTMLInputElement)?.value || '', - headerRight: (document.getElementById('header-right') as HTMLInputElement)?.value || '', - footerLeft: (document.getElementById('footer-left') as HTMLInputElement)?.value || '', - footerCenter: (document.getElementById('footer-center') as HTMLInputElement)?.value || '', - footerRight: (document.getElementById('footer-right') as HTMLInputElement)?.value || '', - }; - const indicesToProcess = parsePageRanges(pageRangeInput, totalPages); - if (indicesToProcess.length === 0) throw new Error("Invalid page range specified."); - const drawOptions = { font: helveticaFont, size: fontSize, color: rgb(fontColor.r, fontColor.g, fontColor.b) }; + if (!pageState.pdfDoc) { + showAlert('Error', 'Please upload a PDF file first.'); + return; + } + showLoader('Adding header & footer...'); + try { + const helveticaFont = await pageState.pdfDoc.embedFont( + StandardFonts.Helvetica + ); + const allPages = pageState.pdfDoc.getPages(); + const totalPages = allPages.length; + const margin = 40; + const fontSize = + parseInt( + (document.getElementById('font-size') as HTMLInputElement)?.value || + '10' + ) || 10; + const colorHex = + (document.getElementById('font-color') as HTMLInputElement)?.value || + '#000000'; + const fontColor = hexToRgb(colorHex); + const pageRangeInput = + (document.getElementById('page-range') as HTMLInputElement)?.value || ''; + const texts = { + headerLeft: + (document.getElementById('header-left') as HTMLInputElement)?.value || + '', + headerCenter: + (document.getElementById('header-center') as HTMLInputElement)?.value || + '', + headerRight: + (document.getElementById('header-right') as HTMLInputElement)?.value || + '', + footerLeft: + (document.getElementById('footer-left') as HTMLInputElement)?.value || + '', + footerCenter: + (document.getElementById('footer-center') as HTMLInputElement)?.value || + '', + footerRight: + (document.getElementById('footer-right') as HTMLInputElement)?.value || + '', + }; + const indicesToProcess = parsePageRanges(pageRangeInput, totalPages); + if (indicesToProcess.length === 0) + throw new Error('Invalid page range specified.'); + const drawOptions = { + font: helveticaFont, + size: fontSize, + color: rgb(fontColor.r, fontColor.g, fontColor.b), + }; - for (const pageIndex of indicesToProcess) { - const page = allPages[pageIndex]; - const { width, height } = page.getSize(); - const pageNumber = pageIndex + 1; - const processText = (text: string) => text.replace(/{page}/g, String(pageNumber)).replace(/{total}/g, String(totalPages)); - const processed = { - headerLeft: processText(texts.headerLeft), headerCenter: processText(texts.headerCenter), headerRight: processText(texts.headerRight), - footerLeft: processText(texts.footerLeft), footerCenter: processText(texts.footerCenter), footerRight: processText(texts.footerRight), - }; - if (processed.headerLeft) page.drawText(processed.headerLeft, { ...drawOptions, x: margin, y: height - margin }); - if (processed.headerCenter) page.drawText(processed.headerCenter, { ...drawOptions, x: width / 2 - helveticaFont.widthOfTextAtSize(processed.headerCenter, fontSize) / 2, y: height - margin }); - if (processed.headerRight) page.drawText(processed.headerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processed.headerRight, fontSize), y: height - margin }); - if (processed.footerLeft) page.drawText(processed.footerLeft, { ...drawOptions, x: margin, y: margin }); - if (processed.footerCenter) page.drawText(processed.footerCenter, { ...drawOptions, x: width / 2 - helveticaFont.widthOfTextAtSize(processed.footerCenter, fontSize) / 2, y: margin }); - if (processed.footerRight) page.drawText(processed.footerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processed.footerRight, fontSize), y: margin }); - } - const newPdfBytes = await pageState.pdfDoc.save(); - downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'header-footer-added.pdf'); - showAlert('Success', 'Header & Footer added successfully!', 'success', () => { resetState(); }); - } catch (e: any) { console.error(e); showAlert('Error', e.message || 'Could not add header or footer.'); } - finally { hideLoader(); } + for (const pageIndex of indicesToProcess) { + const page = allPages[pageIndex]; + const { width, height } = page.getSize(); + const pageNumber = pageIndex + 1; + const processText = (text: string) => + text + .replace(/{page}/g, String(pageNumber)) + .replace(/{total}/g, String(totalPages)); + const processed = { + headerLeft: processText(texts.headerLeft), + headerCenter: processText(texts.headerCenter), + headerRight: processText(texts.headerRight), + footerLeft: processText(texts.footerLeft), + footerCenter: processText(texts.footerCenter), + footerRight: processText(texts.footerRight), + }; + if (processed.headerLeft) + page.drawText(processed.headerLeft, { + ...drawOptions, + x: margin, + y: height - margin, + }); + if (processed.headerCenter) + page.drawText(processed.headerCenter, { + ...drawOptions, + x: + width / 2 - + helveticaFont.widthOfTextAtSize(processed.headerCenter, fontSize) / + 2, + y: height - margin, + }); + if (processed.headerRight) + page.drawText(processed.headerRight, { + ...drawOptions, + x: + width - + margin - + helveticaFont.widthOfTextAtSize(processed.headerRight, fontSize), + y: height - margin, + }); + if (processed.footerLeft) + page.drawText(processed.footerLeft, { + ...drawOptions, + x: margin, + y: margin, + }); + if (processed.footerCenter) + page.drawText(processed.footerCenter, { + ...drawOptions, + x: + width / 2 - + helveticaFont.widthOfTextAtSize(processed.footerCenter, fontSize) / + 2, + y: margin, + }); + if (processed.footerRight) + page.drawText(processed.footerRight, { + ...drawOptions, + x: + width - + margin - + helveticaFont.widthOfTextAtSize(processed.footerRight, fontSize), + y: margin, + }); + } + const newPdfBytes = await pageState.pdfDoc.save(); + downloadFile( + new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), + 'header-footer-added.pdf' + ); + showAlert( + 'Success', + 'Header & Footer added successfully!', + 'success', + () => { + resetState(); + } + ); + } catch (e: any) { + console.error(e); + showAlert('Error', e.message || 'Could not add header or footer.'); + } finally { + hideLoader(); + } } diff --git a/src/js/logic/invert-colors-page.ts b/src/js/logic/invert-colors-page.ts index cea6048..c9e2591 100644 --- a/src/js/logic/invert-colors-page.ts +++ b/src/js/logic/invert-colors-page.ts @@ -5,6 +5,7 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { applyInvertColors } from '../utils/image-effects.js'; import * as pdfjsLib from 'pdfjs-dist'; import { InvertColorsState } from '@/types'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -64,11 +65,13 @@ async function handleFiles(files: FileList) { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; } - showLoader('Loading PDF...'); try { - const arrayBuffer = await file.arrayBuffer(); - pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer); - pageState.file = file; + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; + showLoader('Loading PDF...'); + result.pdf.destroy(); + pageState.pdfDoc = await PDFLibDocument.load(result.bytes); + pageState.file = result.file; updateFileDisplay(); document.getElementById('options-panel')?.classList.remove('hidden'); } catch (error) { diff --git a/src/js/logic/merge-pdf-page.ts b/src/js/logic/merge-pdf-page.ts index 195a8ac..8d70c6b 100644 --- a/src/js/logic/merge-pdf-page.ts +++ b/src/js/logic/merge-pdf-page.ts @@ -1,10 +1,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { - downloadFile, - readFileAsArrayBuffer, - getPDFDocument, -} from '../utils/helpers.js'; +import { downloadFile } from '../utils/helpers.js'; import { state } from '../state.js'; +import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; import { renderPagesProgressively, cleanupLazyRendering, @@ -453,15 +450,23 @@ export async function refreshMergeUI() { mergeState.pdfDocs = {}; mergeState.pdfBytes = {}; + hideLoader(); + state.files = await batchDecryptIfNeeded(state.files); + showLoader('Loading PDF documents...'); + for (let i = 0; i < state.files.length; i++) { const file = state.files[i]; const fileKey = `${i}_${file.name}`; - const pdfBytes = await readFileAsArrayBuffer(file); - mergeState.pdfBytes[fileKey] = pdfBytes as ArrayBuffer; - const bytesForPdfJs = (pdfBytes as ArrayBuffer).slice(0); - const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise; - mergeState.pdfDocs[fileKey] = pdfjsDoc; + const bytes = await file.arrayBuffer(); + const pdf = await pdfjsLib.getDocument({ data: bytes.slice(0) }).promise; + mergeState.pdfBytes[fileKey] = bytes; + mergeState.pdfDocs[fileKey] = pdf; + } + + if (state.files.length === 0) { + hideLoader(); + return; } } catch (error) { console.error('Error loading PDFs:', error); diff --git a/src/js/logic/n-up-pdf-page.ts b/src/js/logic/n-up-pdf-page.ts index 3fed4b5..64a6d47 100644 --- a/src/js/logic/n-up-pdf-page.ts +++ b/src/js/logic/n-up-pdf-page.ts @@ -2,265 +2,304 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; interface NUpState { - file: File | null; - pdfDoc: PDFLibDocument | null; + file: File | null; + pdfDoc: PDFLibDocument | null; } const pageState: NUpState = { - file: null, - pdfDoc: null, + file: null, + pdfDoc: null, }; function resetState() { - pageState.file = null; - pageState.pdfDoc = null; + pageState.file = null; + pageState.pdfDoc = null; - 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 fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.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'); - 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.file) { + 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 = pageState.file.name; - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`; + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`; - 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 () { + resetState(); + }; - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); - try { - showLoader('Loading PDF...'); - const arrayBuffer = await pageState.file.arrayBuffer(); - pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, { - ignoreEncryption: true, - throwOnInvalidObject: false - }); - hideLoader(); + try { + const result = await loadPdfWithPasswordPrompt(pageState.file); + if (!result) { + resetState(); + return; + } + showLoader('Loading PDF...'); + result.pdf.destroy(); + pageState.file = result.file; + pageState.pdfDoc = await PDFLibDocument.load(result.bytes, { + throwOnInvalidObject: false, + }); + hideLoader(); - const pageCount = pageState.pdfDoc.getPageCount(); - metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`; + const pageCount = pageState.pdfDoc.getPageCount(); + metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`; - if (toolOptions) toolOptions.classList.remove('hidden'); - } catch (error) { - console.error('Error loading PDF:', error); - hideLoader(); - showAlert('Error', 'Failed to load PDF file.'); - resetState(); - } - } else { - if (toolOptions) toolOptions.classList.add('hidden'); + if (toolOptions) toolOptions.classList.remove('hidden'); + } catch (error) { + console.error('Error loading PDF:', error); + hideLoader(); + showAlert('Error', 'Failed to load PDF file.'); + resetState(); } + } else { + if (toolOptions) toolOptions.classList.add('hidden'); + } } async function nUpTool() { - if (!pageState.pdfDoc || !pageState.file) { - showAlert('Error', 'Please upload a PDF first.'); - return; + if (!pageState.pdfDoc || !pageState.file) { + showAlert('Error', 'Please upload a PDF first.'); + return; + } + + const n = parseInt( + (document.getElementById('pages-per-sheet') as HTMLSelectElement).value + ); + const pageSizeKey = ( + document.getElementById('output-page-size') as HTMLSelectElement + ).value as keyof typeof PageSizes; + let orientation = ( + document.getElementById('output-orientation') as HTMLSelectElement + ).value; + const useMargins = ( + document.getElementById('add-margins') as HTMLInputElement + ).checked; + const addBorder = (document.getElementById('add-border') as HTMLInputElement) + .checked; + const borderColor = hexToRgb( + (document.getElementById('border-color') as HTMLInputElement).value + ); + + showLoader('Creating N-Up PDF...'); + + try { + const sourceDoc = pageState.pdfDoc; + const newDoc = await PDFLibDocument.create(); + const sourcePages = sourceDoc.getPages(); + + const gridDims: Record = { + 2: [2, 1], + 4: [2, 2], + 9: [3, 3], + 16: [4, 4], + }; + const dims = gridDims[n]; + + let [pageWidth, pageHeight] = PageSizes[pageSizeKey]; + + if (orientation === 'auto') { + const firstPage = sourcePages[0]; + const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight(); + orientation = + isSourceLandscape && dims[0] > dims[1] ? 'landscape' : 'portrait'; } - const n = parseInt((document.getElementById('pages-per-sheet') as HTMLSelectElement).value); - const pageSizeKey = (document.getElementById('output-page-size') as HTMLSelectElement).value as keyof typeof PageSizes; - let orientation = (document.getElementById('output-orientation') as HTMLSelectElement).value; - const useMargins = (document.getElementById('add-margins') as HTMLInputElement).checked; - const addBorder = (document.getElementById('add-border') as HTMLInputElement).checked; - const borderColor = hexToRgb((document.getElementById('border-color') as HTMLInputElement).value); + if (orientation === 'landscape' && pageWidth < pageHeight) { + [pageWidth, pageHeight] = [pageHeight, pageWidth]; + } - showLoader('Creating N-Up PDF...'); + const margin = useMargins ? 36 : 0; + const gutter = useMargins ? 10 : 0; - try { - const sourceDoc = pageState.pdfDoc; - const newDoc = await PDFLibDocument.create(); - const sourcePages = sourceDoc.getPages(); + const usableWidth = pageWidth - margin * 2; + const usableHeight = pageHeight - margin * 2; - const gridDims: Record = { 2: [2, 1], 4: [2, 2], 9: [3, 3], 16: [4, 4] }; - const dims = gridDims[n]; + for (let i = 0; i < sourcePages.length; i += n) { + showLoader(`Processing sheet ${Math.floor(i / n) + 1}...`); + const chunk = sourcePages.slice(i, i + n); + const outputPage = newDoc.addPage([pageWidth, pageHeight]); - let [pageWidth, pageHeight] = PageSizes[pageSizeKey]; + const cellWidth = (usableWidth - gutter * (dims[0] - 1)) / dims[0]; + const cellHeight = (usableHeight - gutter * (dims[1] - 1)) / dims[1]; - if (orientation === 'auto') { - const firstPage = sourcePages[0]; - const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight(); - orientation = isSourceLandscape && dims[0] > dims[1] ? 'landscape' : 'portrait'; - } + for (let j = 0; j < chunk.length; j++) { + const sourcePage = chunk[j]; + const embeddedPage = await newDoc.embedPage(sourcePage); - if (orientation === 'landscape' && pageWidth < pageHeight) { - [pageWidth, pageHeight] = [pageHeight, pageWidth]; - } - - const margin = useMargins ? 36 : 0; - const gutter = useMargins ? 10 : 0; - - const usableWidth = pageWidth - margin * 2; - const usableHeight = pageHeight - margin * 2; - - for (let i = 0; i < sourcePages.length; i += n) { - showLoader(`Processing sheet ${Math.floor(i / n) + 1}...`); - const chunk = sourcePages.slice(i, i + n); - const outputPage = newDoc.addPage([pageWidth, pageHeight]); - - const cellWidth = (usableWidth - gutter * (dims[0] - 1)) / dims[0]; - const cellHeight = (usableHeight - gutter * (dims[1] - 1)) / dims[1]; - - for (let j = 0; j < chunk.length; j++) { - const sourcePage = chunk[j]; - const embeddedPage = await newDoc.embedPage(sourcePage); - - const scale = Math.min( - cellWidth / embeddedPage.width, - cellHeight / embeddedPage.height - ); - const scaledWidth = embeddedPage.width * scale; - const scaledHeight = embeddedPage.height * scale; - - const row = Math.floor(j / dims[0]); - const col = j % dims[0]; - const cellX = margin + col * (cellWidth + gutter); - const cellY = pageHeight - margin - (row + 1) * cellHeight - row * gutter; - - const x = cellX + (cellWidth - scaledWidth) / 2; - const y = cellY + (cellHeight - scaledHeight) / 2; - - outputPage.drawPage(embeddedPage, { - x, - y, - width: scaledWidth, - height: scaledHeight, - }); - - if (addBorder) { - outputPage.drawRectangle({ - x, - y, - width: scaledWidth, - height: scaledHeight, - borderColor: rgb(borderColor.r, borderColor.g, borderColor.b), - borderWidth: 1, - }); - } - } - } - - const newPdfBytes = await newDoc.save(); - const originalName = pageState.file.name.replace(/\.pdf$/i, ''); - - downloadFile( - new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), - `${originalName}_${n}-up.pdf` + const scale = Math.min( + cellWidth / embeddedPage.width, + cellHeight / embeddedPage.height ); + const scaledWidth = embeddedPage.width * scale; + const scaledHeight = embeddedPage.height * scale; - showAlert('Success', 'N-Up PDF created successfully!', 'success', function () { - resetState(); + const row = Math.floor(j / dims[0]); + const col = j % dims[0]; + const cellX = margin + col * (cellWidth + gutter); + const cellY = + pageHeight - margin - (row + 1) * cellHeight - row * gutter; + + const x = cellX + (cellWidth - scaledWidth) / 2; + const y = cellY + (cellHeight - scaledHeight) / 2; + + outputPage.drawPage(embeddedPage, { + x, + y, + width: scaledWidth, + height: scaledHeight, }); - } catch (e) { - console.error(e); - showAlert('Error', 'An error occurred while creating the N-Up PDF.'); - } finally { - hideLoader(); + + if (addBorder) { + outputPage.drawRectangle({ + x, + y, + width: scaledWidth, + height: scaledHeight, + borderColor: rgb(borderColor.r, borderColor.g, borderColor.b), + borderWidth: 1, + }); + } + } } + + const newPdfBytes = await newDoc.save(); + const originalName = pageState.file.name.replace(/\.pdf$/i, ''); + + downloadFile( + new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), + `${originalName}_${n}-up.pdf` + ); + + showAlert( + 'Success', + 'N-Up PDF created successfully!', + 'success', + function () { + resetState(); + } + ); + } catch (e) { + console.error(e); + showAlert('Error', 'An error occurred while creating the N-Up PDF.'); + } finally { + hideLoader(); + } } 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 file = files[0]; + if ( + file.type === 'application/pdf' || + file.name.toLowerCase().endsWith('.pdf') + ) { + pageState.file = file; + updateUI(); } + } } 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 addBorderCheckbox = document.getElementById('add-border'); - const borderColorWrapper = document.getElementById('border-color-wrapper'); + 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 addBorderCheckbox = document.getElementById('add-border'); + const borderColorWrapper = document.getElementById('border-color-wrapper'); - 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 (addBorderCheckbox && borderColorWrapper) { + addBorderCheckbox.addEventListener('change', function () { + borderColorWrapper.classList.toggle( + 'hidden', + !(addBorderCheckbox as HTMLInputElement).checked + ); + }); + } + + 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('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); + } + } + }); - if (addBorderCheckbox && borderColorWrapper) { - addBorderCheckbox.addEventListener('change', function () { - borderColorWrapper.classList.toggle('hidden', !(addBorderCheckbox as HTMLInputElement).checked); - }); - } + fileInput.addEventListener('click', function () { + fileInput.value = ''; + }); + } - 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('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); - } - } - }); - - fileInput.addEventListener('click', function () { - fileInput.value = ''; - }); - } - - if (processBtn) { - processBtn.addEventListener('click', nUpTool); - } + if (processBtn) { + processBtn.addEventListener('click', nUpTool); + } }); diff --git a/src/js/logic/ocr-pdf-page.ts b/src/js/logic/ocr-pdf-page.ts index 85af932..f498883 100644 --- a/src/js/logic/ocr-pdf-page.ts +++ b/src/js/logic/ocr-pdf-page.ts @@ -1,6 +1,7 @@ import { tesseractLanguages } from '../config/tesseract-languages.js'; import { showAlert } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { icons, createIcons } from 'lucide'; import { OcrState } from '@/types'; import { performOcr } from '../utils/ocr.js'; @@ -231,14 +232,17 @@ async function updateUI() { } } -function handleFileSelect(files: FileList | null) { +async 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; + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; + result.pdf.destroy(); + pageState.file = result.file; updateUI(); } } diff --git a/src/js/logic/organize-pdf-page.ts b/src/js/logic/organize-pdf-page.ts index 7ca97ff..6c08c76 100644 --- a/src/js/logic/organize-pdf-page.ts +++ b/src/js/logic/organize-pdf-page.ts @@ -1,13 +1,9 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; -import { - readFileAsArrayBuffer, - formatBytes, - downloadFile, - getPDFDocument, -} from '../utils/helpers.js'; +import { formatBytes, downloadFile } from '../utils/helpers.js'; import { initPagePreview } from '../utils/page-preview.js'; import { PDFDocument } from 'pdf-lib'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import * as pdfjsLib from 'pdfjs-dist'; import Sortable from 'sortablejs'; @@ -173,18 +169,18 @@ async function handleFile(file: File) { return; } - showLoader('Loading PDF...'); organizeState.file = file; try { - const arrayBuffer = await readFileAsArrayBuffer(file); - organizeState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { - ignoreEncryption: true, + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; + showLoader('Loading PDF...'); + + organizeState.pdfDoc = await PDFDocument.load(result.bytes, { throwOnInvalidObject: false, }); - organizeState.pdfJsDoc = await getPDFDocument({ - data: (arrayBuffer as ArrayBuffer).slice(0), - }).promise; + organizeState.pdfJsDoc = result.pdf; + organizeState.file = result.file; organizeState.totalPages = organizeState.pdfDoc.getPageCount(); updateFileDisplay(); diff --git a/src/js/logic/page-dimensions-page.ts b/src/js/logic/page-dimensions-page.ts index 71458f4..6144955 100644 --- a/src/js/logic/page-dimensions-page.ts +++ b/src/js/logic/page-dimensions-page.ts @@ -1,81 +1,86 @@ import { showAlert } from '../ui.js'; -import { formatBytes, getStandardPageName, convertPoints } from '../utils/helpers.js'; +import { + formatBytes, + getStandardPageName, + convertPoints, +} from '../utils/helpers.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { PDFDocument } from 'pdf-lib'; import { icons, createIcons } from 'lucide'; import { PageDimensionsState } from '@/types'; const pageState: PageDimensionsState = { - file: null, - pdfDoc: null, + file: null, + pdfDoc: null, }; let analyzedPagesData: any[] = []; function calculateAspectRatio(width: number, height: number): string { - const ratio = width / height; - return ratio.toFixed(3); + const ratio = width / height; + return ratio.toFixed(3); } function calculateArea(width: number, height: number, unit: string): string { - const areaInPoints = width * height; - let convertedArea = 0; - let unitSuffix = ''; + const areaInPoints = width * height; + let convertedArea: number; + let unitSuffix: string; - switch (unit) { - case 'in': - convertedArea = areaInPoints / (72 * 72); - unitSuffix = 'in²'; - break; - case 'mm': - convertedArea = areaInPoints / (72 * 72) * (25.4 * 25.4); - unitSuffix = 'mm²'; - break; - case 'px': - const pxPerPoint = 96 / 72; - convertedArea = areaInPoints * (pxPerPoint * pxPerPoint); - unitSuffix = 'px²'; - break; - default: - convertedArea = areaInPoints; - unitSuffix = 'pt²'; - break; - } + switch (unit) { + case 'in': + convertedArea = areaInPoints / (72 * 72); + unitSuffix = 'in²'; + break; + case 'mm': + convertedArea = (areaInPoints / (72 * 72)) * (25.4 * 25.4); + unitSuffix = 'mm²'; + break; + case 'px': + const pxPerPoint = 96 / 72; + convertedArea = areaInPoints * (pxPerPoint * pxPerPoint); + unitSuffix = 'px²'; + break; + default: + convertedArea = areaInPoints; + unitSuffix = 'pt²'; + break; + } - return `${convertedArea.toFixed(2)} ${unitSuffix}`; + return `${convertedArea.toFixed(2)} ${unitSuffix}`; } function getSummaryStats() { - const totalPages = analyzedPagesData.length; + const totalPages = analyzedPagesData.length; - const uniqueSizes = new Map(); - analyzedPagesData.forEach((pageData: any) => { - const key = `${pageData.width.toFixed(2)}x${pageData.height.toFixed(2)}`; - const label = `${pageData.standardSize} (${pageData.orientation})`; - uniqueSizes.set(key, { - count: (uniqueSizes.get(key)?.count || 0) + 1, - label: label, - width: pageData.width, - height: pageData.height - }); + const uniqueSizes = new Map(); + analyzedPagesData.forEach((pageData: any) => { + const key = `${pageData.width.toFixed(2)}x${pageData.height.toFixed(2)}`; + const label = `${pageData.standardSize} (${pageData.orientation})`; + uniqueSizes.set(key, { + count: (uniqueSizes.get(key)?.count || 0) + 1, + label: label, + width: pageData.width, + height: pageData.height, }); + }); - const hasMixedSizes = uniqueSizes.size > 1; + const hasMixedSizes = uniqueSizes.size > 1; - return { - totalPages, - uniqueSizesCount: uniqueSizes.size, - uniqueSizes: Array.from(uniqueSizes.values()), - hasMixedSizes - }; + return { + totalPages, + uniqueSizesCount: uniqueSizes.size, + uniqueSizes: Array.from(uniqueSizes.values()), + hasMixedSizes, + }; } function renderSummary() { - const summaryContainer = document.getElementById('dimensions-summary'); - if (!summaryContainer) return; + const summaryContainer = document.getElementById('dimensions-summary'); + if (!summaryContainer) return; - const stats = getSummaryStats(); + const stats = getSummaryStats(); - let summaryHTML = ` + let summaryHTML = `

Total Pages

@@ -94,8 +99,8 @@ function renderSummary() {
`; - if (stats.hasMixedSizes) { - summaryHTML += ` + if (stats.hasMixedSizes) { + summaryHTML += `
@@ -103,253 +108,284 @@ function renderSummary() {

Mixed Page Sizes Detected

This document contains pages with different dimensions:

    - ${stats.uniqueSizes.map((size: any) => ` + ${stats.uniqueSizes + .map( + (size: any) => `
  • • ${size.label}: ${size.count} page${size.count > 1 ? 's' : ''}
  • - `).join('')} + ` + ) + .join('')}
`; - } + } - summaryContainer.innerHTML = summaryHTML; + summaryContainer.innerHTML = summaryHTML; - if (stats.hasMixedSizes) { - createIcons({ icons }); - } + if (stats.hasMixedSizes) { + createIcons({ icons }); + } } function renderTable(unit: string) { - const tableBody = document.getElementById('dimensions-table-body'); - if (!tableBody) return; + const tableBody = document.getElementById('dimensions-table-body'); + if (!tableBody) return; - tableBody.textContent = ''; + tableBody.textContent = ''; - analyzedPagesData.forEach((pageData) => { - const width = convertPoints(pageData.width, unit); - const height = convertPoints(pageData.height, unit); - const aspectRatio = calculateAspectRatio(pageData.width, pageData.height); - const area = calculateArea(pageData.width, pageData.height, unit); + analyzedPagesData.forEach((pageData) => { + const width = convertPoints(pageData.width, unit); + const height = convertPoints(pageData.height, unit); + const aspectRatio = calculateAspectRatio(pageData.width, pageData.height); + const area = calculateArea(pageData.width, pageData.height, unit); - const row = document.createElement('tr'); + const row = document.createElement('tr'); - const pageNumCell = document.createElement('td'); - pageNumCell.className = 'px-4 py-3 text-white'; - pageNumCell.textContent = pageData.pageNum; + const pageNumCell = document.createElement('td'); + pageNumCell.className = 'px-4 py-3 text-white'; + pageNumCell.textContent = pageData.pageNum; - const dimensionsCell = document.createElement('td'); - dimensionsCell.className = 'px-4 py-3 text-gray-300'; - dimensionsCell.textContent = `${width} x ${height} ${unit}`; + const dimensionsCell = document.createElement('td'); + dimensionsCell.className = 'px-4 py-3 text-gray-300'; + dimensionsCell.textContent = `${width} x ${height} ${unit}`; - const sizeCell = document.createElement('td'); - sizeCell.className = 'px-4 py-3 text-gray-300'; - sizeCell.textContent = pageData.standardSize; + const sizeCell = document.createElement('td'); + sizeCell.className = 'px-4 py-3 text-gray-300'; + sizeCell.textContent = pageData.standardSize; - const orientationCell = document.createElement('td'); - orientationCell.className = 'px-4 py-3 text-gray-300'; - orientationCell.textContent = pageData.orientation; + const orientationCell = document.createElement('td'); + orientationCell.className = 'px-4 py-3 text-gray-300'; + orientationCell.textContent = pageData.orientation; - const aspectRatioCell = document.createElement('td'); - aspectRatioCell.className = 'px-4 py-3 text-gray-300'; - aspectRatioCell.textContent = aspectRatio; + const aspectRatioCell = document.createElement('td'); + aspectRatioCell.className = 'px-4 py-3 text-gray-300'; + aspectRatioCell.textContent = aspectRatio; - const areaCell = document.createElement('td'); - areaCell.className = 'px-4 py-3 text-gray-300'; - areaCell.textContent = area; + const areaCell = document.createElement('td'); + areaCell.className = 'px-4 py-3 text-gray-300'; + areaCell.textContent = area; - const rotationCell = document.createElement('td'); - rotationCell.className = 'px-4 py-3 text-gray-300'; - rotationCell.textContent = `${pageData.rotation}°`; + const rotationCell = document.createElement('td'); + rotationCell.className = 'px-4 py-3 text-gray-300'; + rotationCell.textContent = `${pageData.rotation}°`; - row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell, aspectRatioCell, areaCell, rotationCell); - tableBody.appendChild(row); - }); + row.append( + pageNumCell, + dimensionsCell, + sizeCell, + orientationCell, + aspectRatioCell, + areaCell, + rotationCell + ); + tableBody.appendChild(row); + }); } function exportToCSV() { - const unitsSelect = document.getElementById('units-select') as HTMLSelectElement; - const unit = unitsSelect?.value || 'pt'; + const unitsSelect = document.getElementById( + 'units-select' + ) as HTMLSelectElement; + const unit = unitsSelect?.value || 'pt'; - const headers = ['Page #', `Width (${unit})`, `Height (${unit})`, 'Standard Size', 'Orientation', 'Aspect Ratio', `Area (${unit}²)`, 'Rotation']; - const csvRows = [headers.join(',')]; + const headers = [ + 'Page #', + `Width (${unit})`, + `Height (${unit})`, + 'Standard Size', + 'Orientation', + 'Aspect Ratio', + `Area (${unit}²)`, + 'Rotation', + ]; + const csvRows = [headers.join(',')]; - analyzedPagesData.forEach((pageData: any) => { - const width = convertPoints(pageData.width, unit); - const height = convertPoints(pageData.height, unit); - const aspectRatio = calculateAspectRatio(pageData.width, pageData.height); - const area = calculateArea(pageData.width, pageData.height, unit); + analyzedPagesData.forEach((pageData: any) => { + const width = convertPoints(pageData.width, unit); + const height = convertPoints(pageData.height, unit); + const aspectRatio = calculateAspectRatio(pageData.width, pageData.height); + const area = calculateArea(pageData.width, pageData.height, unit); - const row = [ - pageData.pageNum, - width, - height, - pageData.standardSize, - pageData.orientation, - aspectRatio, - area, - `${pageData.rotation}°` - ]; - csvRows.push(row.join(',')); - }); + const row = [ + pageData.pageNum, + width, + height, + pageData.standardSize, + pageData.orientation, + aspectRatio, + area, + `${pageData.rotation}°`, + ]; + csvRows.push(row.join(',')); + }); - const csvContent = csvRows.join('\n'); - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = 'page-dimensions.csv'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); + const csvContent = csvRows.join('\n'); + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = 'page-dimensions.csv'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); } function analyzeAndDisplayDimensions() { - if (!pageState.pdfDoc) return; + if (!pageState.pdfDoc) return; - analyzedPagesData = []; - const pages = pageState.pdfDoc.getPages(); + analyzedPagesData = []; + const pages = pageState.pdfDoc.getPages(); - pages.forEach((page: any, index: number) => { - const { width, height } = page.getSize(); - const rotation = page.getRotation().angle || 0; + pages.forEach((page: any, index: number) => { + const { width, height } = page.getSize(); + const rotation = page.getRotation().angle || 0; - analyzedPagesData.push({ - pageNum: index + 1, - width, - height, - orientation: width > height ? 'Landscape' : 'Portrait', - standardSize: getStandardPageName(width, height), - rotation: rotation - }); + analyzedPagesData.push({ + pageNum: index + 1, + width, + height, + orientation: width > height ? 'Landscape' : 'Portrait', + standardSize: getStandardPageName(width, height), + rotation: rotation, }); + }); - const resultsContainer = document.getElementById('dimensions-results'); - const unitsSelect = document.getElementById('units-select') as HTMLSelectElement; + const resultsContainer = document.getElementById('dimensions-results'); + const unitsSelect = document.getElementById( + 'units-select' + ) as HTMLSelectElement; - renderSummary(); - renderTable(unitsSelect.value); + renderSummary(); + renderTable(unitsSelect.value); - if (resultsContainer) resultsContainer.classList.remove('hidden'); + if (resultsContainer) resultsContainer.classList.remove('hidden'); - unitsSelect.addEventListener('change', (e) => { - renderTable((e.target as HTMLSelectElement).value); - }); + unitsSelect.addEventListener('change', (e) => { + renderTable((e.target as HTMLSelectElement).value); + }); - const exportButton = document.getElementById('export-csv-btn'); - if (exportButton) { - exportButton.addEventListener('click', exportToCSV); - } + const exportButton = document.getElementById('export-csv-btn'); + if (exportButton) { + exportButton.addEventListener('click', exportToCSV); + } - createIcons({ icons }); + createIcons({ icons }); } function resetState() { - pageState.file = null; - pageState.pdfDoc = null; - analyzedPagesData = []; + pageState.file = null; + pageState.pdfDoc = null; + analyzedPagesData = []; - const fileDisplayArea = document.getElementById('file-display-area'); - if (fileDisplayArea) fileDisplayArea.innerHTML = ''; + const fileDisplayArea = document.getElementById('file-display-area'); + if (fileDisplayArea) fileDisplayArea.innerHTML = ''; - const resultsContainer = document.getElementById('dimensions-results'); - if (resultsContainer) resultsContainer.classList.add('hidden'); + const resultsContainer = document.getElementById('dimensions-results'); + if (resultsContainer) resultsContainer.classList.add('hidden'); - const fileInput = document.getElementById('file-input') as HTMLInputElement; - if (fileInput) fileInput.value = ''; + const fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; } async function updateUI() { - const fileDisplayArea = document.getElementById('file-display-area'); + const fileDisplayArea = document.getElementById('file-display-area'); - 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.file) { + 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 = pageState.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(pageState.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 () { + resetState(); + }; - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); - } + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); + } } async 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; + if (files && files.length > 0) { + const file = files[0]; + if ( + file.type === 'application/pdf' || + file.name.toLowerCase().endsWith('.pdf') + ) { + try { + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; + result.pdf.destroy(); - try { - const arrayBuffer = await file.arrayBuffer(); - pageState.pdfDoc = await PDFDocument.load(arrayBuffer); - updateUI(); - analyzeAndDisplayDimensions(); - } catch (e) { - console.error('Error loading PDF:', e); - showAlert('Error', 'Failed to load PDF file.'); - } - } + pageState.file = result.file; + pageState.pdfDoc = await PDFDocument.load(result.bytes); + updateUI(); + analyzeAndDisplayDimensions(); + } catch (e) { + console.error('Error loading PDF:', e); + showAlert('Error', 'Failed to load PDF file.'); + } } + } } document.addEventListener('DOMContentLoaded', function () { - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const backBtn = document.getElementById('back-to-tools'); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + 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'); - handleFileSelect(e.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 = ''; + }); + } }); diff --git a/src/js/logic/page-numbers-page.ts b/src/js/logic/page-numbers-page.ts index 3971814..92259ef 100644 --- a/src/js/logic/page-numbers-page.ts +++ b/src/js/logic/page-numbers-page.ts @@ -2,6 +2,7 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { addPageNumbers as addPageNumbersToPdf, type PageNumberPosition, @@ -83,11 +84,14 @@ async function handleFiles(files: FileList) { return; } - showLoader('Loading PDF...'); try { - const arrayBuffer = await file.arrayBuffer(); - pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer); - pageState.file = file; + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; + showLoader('Loading PDF...'); + + pageState.pdfDoc = await PDFLibDocument.load(result.bytes); + pageState.file = result.file; + result.pdf.destroy(); updateFileDisplay(); document.getElementById('options-panel')?.classList.remove('hidden'); diff --git a/src/js/logic/pdf-booklet-page.ts b/src/js/logic/pdf-booklet-page.ts index 0fbca6b..59641b9 100644 --- a/src/js/logic/pdf-booklet-page.ts +++ b/src/js/logic/pdf-booklet-page.ts @@ -3,6 +3,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; import { PDFDocument as PDFLibDocument, degrees, PageSizes } from 'pdf-lib'; import * as pdfjsLib from 'pdfjs-dist'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.mjs', @@ -87,19 +88,20 @@ async function updateUI() { createIcons({ icons }); try { + const result = await loadPdfWithPasswordPrompt(pageState.file); + if (!result) { + resetState(); + return; + } showLoader('Loading PDF...'); - const arrayBuffer = await pageState.file.arrayBuffer(); - pageState.pdfBytes = new Uint8Array(arrayBuffer); + pageState.file = result.file; + pageState.pdfBytes = new Uint8Array(result.bytes); + pageState.pdfjsDoc = result.pdf; pageState.pdfDoc = await PDFLibDocument.load(pageState.pdfBytes, { - ignoreEncryption: true, throwOnInvalidObject: false, }); - pageState.pdfjsDoc = await pdfjsLib.getDocument({ - data: pageState.pdfBytes.slice(), - }).promise; - hideLoader(); const pageCount = pageState.pdfDoc.getPageCount(); diff --git a/src/js/logic/pdf-layers-page.ts b/src/js/logic/pdf-layers-page.ts index e1a3836..cd01cc2 100644 --- a/src/js/logic/pdf-layers-page.ts +++ b/src/js/logic/pdf-layers-page.ts @@ -10,6 +10,7 @@ import { createIcons, icons } from 'lucide'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; interface LayerData { number: number; @@ -416,14 +417,17 @@ document.addEventListener('DOMContentLoaded', () => { } }; - const handleFileSelect = (files: FileList | null) => { + const handleFileSelect = async (files: FileList | null) => { if (files && files.length > 0) { const file = files[0]; if ( file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf') ) { - currentFile = file; + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; + result.pdf.destroy(); + currentFile = result.file; updateUI(); } else { showAlert('Invalid File', 'Please select a PDF file.'); diff --git a/src/js/logic/pdf-multi-tool.ts b/src/js/logic/pdf-multi-tool.ts index 4515f06..aa5388c 100644 --- a/src/js/logic/pdf-multi-tool.ts +++ b/src/js/logic/pdf-multi-tool.ts @@ -4,6 +4,7 @@ import * as pdfjsLib from 'pdfjs-dist'; import JSZip from 'jszip'; import Sortable from 'sortablejs'; import { downloadFile, getPDFDocument } from '../utils/helpers'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { renderPagesProgressively, cleanupLazyRendering, @@ -428,6 +429,12 @@ async function loadPdfs(files: File[]) { arrayBuffer = await file.arrayBuffer(); } + hideLoading(); + const pwResult = await loadPdfWithPasswordPrompt(file); + if (!pwResult) continue; + pwResult.pdf.destroy(); + arrayBuffer = pwResult.bytes as ArrayBuffer; + const pdfDoc = await PDFLibDocument.load(arrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false, @@ -848,15 +855,17 @@ async function handleInsertPdf(e: Event) { if (insertAfterIndex === undefined) return; try { - const arrayBuffer = await file.arrayBuffer(); - const pdfDoc = await PDFLibDocument.load(arrayBuffer, { + const pwResult = await loadPdfWithPasswordPrompt(file); + if (!pwResult) return; + pwResult.pdf.destroy(); + + const pdfDoc = await PDFLibDocument.load(pwResult.bytes, { ignoreEncryption: true, throwOnInvalidObject: false, }); currentPdfDocs.push(pdfDoc); const pdfIndex = currentPdfDocs.length - 1; - // Load PDF.js document for rendering const pdfBytes = await pdfDoc.save(); const pdfjsDoc = await getPDFDocument({ data: new Uint8Array(pdfBytes) }) .promise; diff --git a/src/js/logic/pdf-to-bmp-page.ts b/src/js/logic/pdf-to-bmp-page.ts index 046eea6..2e8356c 100644 --- a/src/js/logic/pdf-to-bmp-page.ts +++ b/src/js/logic/pdf-to-bmp-page.ts @@ -11,6 +11,7 @@ import JSZip from 'jszip'; import * as pdfjsLib from 'pdfjs-dist'; import { PDFPageProxy } from 'pdfjs-dist'; import { t } from '../i18n/i18n'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -98,10 +99,11 @@ async function convert() { ); return; } - showLoader(t('tools:pdfToBmp.loader.converting')); try { - const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) - .promise; + const result = await loadPdfWithPasswordPrompt(files[0], files, 0); + if (!result) return; + showLoader(t('tools:pdfToBmp.loader.converting')); + const { pdf } = result; if (pdf.numPages === 1) { const page = await pdf.getPage(1); diff --git a/src/js/logic/pdf-to-cbz-page.ts b/src/js/logic/pdf-to-cbz-page.ts index 0f4cd97..0e1fc88 100644 --- a/src/js/logic/pdf-to-cbz-page.ts +++ b/src/js/logic/pdf-to-cbz-page.ts @@ -17,6 +17,7 @@ import { generateComicBookInfoJson, } from '../utils/comic-info.js'; import type { CbzOptions, ComicMetadata } from '@/types'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -229,8 +230,11 @@ async function convert() { try { const options = getOptions(); - const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) - .promise; + hideLoader(); + const result = await loadPdfWithPasswordPrompt(files[0], files, 0); + if (!result) return; + showLoader(t('tools:pdfToCbz.converting')); + const { pdf } = result; if (pdf.numPages === 0) { throw new Error('PDF has no pages'); diff --git a/src/js/logic/pdf-to-csv-page.ts b/src/js/logic/pdf-to-csv-page.ts index ef94b8c..0c7450d 100644 --- a/src/js/logic/pdf-to-csv-page.ts +++ b/src/js/logic/pdf-to-csv-page.ts @@ -5,6 +5,7 @@ import JSZip from 'jszip'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; let file: File | null = null; const updateUI = () => { @@ -86,6 +87,13 @@ async function convert() { try { const pymupdf = await loadPyMuPDF(); + + hideLoader(); + const pwResult = await loadPdfWithPasswordPrompt(file); + if (!pwResult) return; + pwResult.pdf.destroy(); + file = pwResult.file; + showLoader('Extracting tables...'); const doc = await pymupdf.open(file); diff --git a/src/js/logic/pdf-to-docx-page.ts b/src/js/logic/pdf-to-docx-page.ts index 4f6d2f9..cba6a73 100644 --- a/src/js/logic/pdf-to-docx-page.ts +++ b/src/js/logic/pdf-to-docx-page.ts @@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js'; document.addEventListener('DOMContentLoaded', () => { @@ -105,6 +106,10 @@ document.addEventListener('DOMContentLoaded', () => { showLoader('Loading PDF converter...'); const pymupdf = await loadPyMuPDF(); + hideLoader(); + state.files = await batchDecryptIfNeeded(state.files); + showLoader('Converting...'); + if (state.files.length === 1) { const file = state.files[0]; showLoader(`Converting ${file.name}...`); @@ -122,7 +127,6 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } else { - showLoader('Converting multiple PDFs...'); const JSZip = (await import('jszip')).default; const zip = new JSZip(); const usedNames = new Set(); diff --git a/src/js/logic/pdf-to-excel-page.ts b/src/js/logic/pdf-to-excel-page.ts index 33fa38b..75e261c 100644 --- a/src/js/logic/pdf-to-excel-page.ts +++ b/src/js/logic/pdf-to-excel-page.ts @@ -4,6 +4,7 @@ import { createIcons, icons } from 'lucide'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import * as XLSX from 'xlsx'; let file: File | null = null; @@ -66,6 +67,13 @@ async function convert() { try { const pymupdf = await loadPyMuPDF(); + + hideLoader(); + const pwResult = await loadPdfWithPasswordPrompt(file); + if (!pwResult) return; + pwResult.pdf.destroy(); + file = pwResult.file; + showLoader('Extracting tables...'); const doc = await pymupdf.open(file); diff --git a/src/js/logic/pdf-to-greyscale-page.ts b/src/js/logic/pdf-to-greyscale-page.ts index c1e7546..4111050 100644 --- a/src/js/logic/pdf-to-greyscale-page.ts +++ b/src/js/logic/pdf-to-greyscale-page.ts @@ -10,6 +10,7 @@ import { PDFDocument } from 'pdf-lib'; import { applyGreyscale } from '../utils/image-effects.js'; import * as pdfjsLib from 'pdfjs-dist'; import { t } from '../i18n/i18n'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -94,13 +95,11 @@ async function convert() { showAlert('No File', 'Please upload a PDF file first.'); return; } - showLoader('Converting to greyscale...'); try { - const pdfBytes = (await readFileAsArrayBuffer(files[0])) as ArrayBuffer; - const pdfDoc = await PDFDocument.load(pdfBytes); - const pages = pdfDoc.getPages(); - - const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise; + const result = await loadPdfWithPasswordPrompt(files[0], files, 0); + if (!result) return; + showLoader('Converting to greyscale...'); + const { pdf: pdfjsDoc } = result; const newPdfDoc = await PDFDocument.create(); for (let i = 1; i <= pdfjsDoc.numPages; i++) { diff --git a/src/js/logic/pdf-to-jpg-page.ts b/src/js/logic/pdf-to-jpg-page.ts index 945d022..03a3cc6 100644 --- a/src/js/logic/pdf-to-jpg-page.ts +++ b/src/js/logic/pdf-to-jpg-page.ts @@ -11,6 +11,7 @@ import JSZip from 'jszip'; import * as pdfjsLib from 'pdfjs-dist'; import { PDFPageProxy } from 'pdfjs-dist'; import { t } from '../i18n/i18n'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -103,10 +104,11 @@ async function convert() { ); return; } - showLoader(t('tools:pdfToJpg.loader.converting')); try { - const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) - .promise; + const result = await loadPdfWithPasswordPrompt(files[0], files, 0); + if (!result) return; + showLoader(t('tools:pdfToJpg.loader.converting')); + const { pdf } = result; const qualityInput = document.getElementById( 'jpg-quality' diff --git a/src/js/logic/pdf-to-json.ts b/src/js/logic/pdf-to-json.ts index d353243..b2a3f52 100644 --- a/src/js/logic/pdf-to-json.ts +++ b/src/js/logic/pdf-to-json.ts @@ -11,6 +11,7 @@ import { showWasmRequiredDialog, WasmProvider, } from '../utils/wasm-provider.js'; +import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; const worker = new Worker( import.meta.env.BASE_URL + 'workers/pdf-to-json.worker.js' @@ -105,6 +106,10 @@ async function convertPDFsToJSON() { try { convertBtn.disabled = true; + showStatus('Checking for encrypted PDFs...', 'info'); + + selectedFiles = await batchDecryptIfNeeded(selectedFiles); + showStatus('Reading files (Main Thread)...', 'info'); const fileBuffers = await Promise.all( diff --git a/src/js/logic/pdf-to-markdown-page.ts b/src/js/logic/pdf-to-markdown-page.ts index 651cc64..7c7451d 100644 --- a/src/js/logic/pdf-to-markdown-page.ts +++ b/src/js/logic/pdf-to-markdown-page.ts @@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js'; document.addEventListener('DOMContentLoaded', () => { @@ -110,6 +111,10 @@ document.addEventListener('DOMContentLoaded', () => { const includeImages = includeImagesCheckbox?.checked ?? false; + hideLoader(); + state.files = await batchDecryptIfNeeded(state.files); + showLoader('Converting...'); + if (state.files.length === 1) { const file = state.files[0]; showLoader(`Converting ${file.name}...`); @@ -128,7 +133,6 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } else { - showLoader('Converting multiple PDFs...'); const JSZip = (await import('jszip')).default; const zip = new JSZip(); const usedNames = new Set(); diff --git a/src/js/logic/pdf-to-pdfa-page.ts b/src/js/logic/pdf-to-pdfa-page.ts index 6df051d..53e4169 100644 --- a/src/js/logic/pdf-to-pdfa-page.ts +++ b/src/js/logic/pdf-to-pdfa-page.ts @@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide'; import { convertFileToPdfA, type PdfALevel } from '../utils/ghostscript-loader'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; +import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js'; document.addEventListener('DOMContentLoaded', () => { @@ -108,10 +109,11 @@ document.addEventListener('DOMContentLoaded', () => { try { if (state.files.length === 0) { showAlert('No Files', 'Please select at least one PDF file.'); - hideLoader(); return; } + state.files = await batchDecryptIfNeeded(state.files); + if (state.files.length === 1) { const originalFile = state.files[0]; const preFlattenCheckbox = document.getElementById( @@ -125,7 +127,6 @@ document.addEventListener('DOMContentLoaded', () => { if (shouldPreFlatten) { if (!isPyMuPDFAvailable()) { showWasmRequiredDialog('pymupdf'); - hideLoader(); return; } diff --git a/src/js/logic/pdf-to-png-page.ts b/src/js/logic/pdf-to-png-page.ts index 9b33813..7da4258 100644 --- a/src/js/logic/pdf-to-png-page.ts +++ b/src/js/logic/pdf-to-png-page.ts @@ -11,6 +11,7 @@ import JSZip from 'jszip'; import * as pdfjsLib from 'pdfjs-dist'; import { PDFPageProxy } from 'pdfjs-dist'; import { t } from '../i18n/i18n'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -101,10 +102,11 @@ async function convert() { ); return; } - showLoader(t('tools:pdfToPng.loader.converting')); try { - const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) - .promise; + const result = await loadPdfWithPasswordPrompt(files[0], files, 0); + if (!result) return; + showLoader(t('tools:pdfToPng.loader.converting')); + const { pdf } = result; const scaleInput = document.getElementById('png-scale') as HTMLInputElement; const scale = scaleInput ? parseFloat(scaleInput.value) : 2.0; diff --git a/src/js/logic/pdf-to-svg-page.ts b/src/js/logic/pdf-to-svg-page.ts index 6bae490..e802029 100644 --- a/src/js/logic/pdf-to-svg-page.ts +++ b/src/js/logic/pdf-to-svg-page.ts @@ -5,6 +5,7 @@ import JSZip from 'jszip'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; let pymupdf: any = null; let files: File[] = []; @@ -87,6 +88,10 @@ async function convert() { pymupdf = await loadPyMuPDF(); } + hideLoader(); + files = await batchDecryptIfNeeded(files); + showLoader('Converting to SVG...'); + const isSingleFile = files.length === 1; if (isSingleFile) { diff --git a/src/js/logic/pdf-to-text-page.ts b/src/js/logic/pdf-to-text-page.ts index 81da55c..7f41d8d 100644 --- a/src/js/logic/pdf-to-text-page.ts +++ b/src/js/logic/pdf-to-text-page.ts @@ -4,6 +4,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js'; let files: File[] = []; @@ -176,6 +177,10 @@ async function extractText() { try { const mupdf = await ensurePyMuPDF(); + hideLoader(); + files = await batchDecryptIfNeeded(files); + showLoader('Extracting text...'); + if (files.length === 1) { const file = files[0]; showLoader(`Extracting text from ${file.name}...`); diff --git a/src/js/logic/pdf-to-tiff-page.ts b/src/js/logic/pdf-to-tiff-page.ts index 6bd75ee..7d713db 100644 --- a/src/js/logic/pdf-to-tiff-page.ts +++ b/src/js/logic/pdf-to-tiff-page.ts @@ -14,6 +14,7 @@ import { t } from '../i18n/i18n'; import type Vips from 'wasm-vips'; import wasmUrl from 'wasm-vips/vips.wasm?url'; import type { TiffOptions } from '@/types'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -229,8 +230,11 @@ async function convert() { try { const options = getOptions(); - const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) - .promise; + hideLoader(); + const result = await loadPdfWithPasswordPrompt(files[0], files, 0); + if (!result) return; + showLoader(t('tools:pdfToTiff.converting')); + const { pdf } = result; if (options.multiPage && pdf.numPages > 1) { const pages: Vips.Image[] = []; diff --git a/src/js/logic/pdf-to-webp-page.ts b/src/js/logic/pdf-to-webp-page.ts index c991752..85bb82c 100644 --- a/src/js/logic/pdf-to-webp-page.ts +++ b/src/js/logic/pdf-to-webp-page.ts @@ -11,6 +11,7 @@ import JSZip from 'jszip'; import * as pdfjsLib from 'pdfjs-dist'; import { PDFPageProxy } from 'pdfjs-dist'; import { t } from '../i18n/i18n'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -103,10 +104,11 @@ async function convert() { ); return; } - showLoader(t('tools:pdfToWebp.loader.converting')); try { - const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) - .promise; + const result = await loadPdfWithPasswordPrompt(files[0], files, 0); + if (!result) return; + showLoader(t('tools:pdfToWebp.loader.converting')); + const { pdf } = result; const qualityInput = document.getElementById( 'webp-quality' diff --git a/src/js/logic/posterize-page.ts b/src/js/logic/posterize-page.ts index ffc12d9..6233848 100644 --- a/src/js/logic/posterize-page.ts +++ b/src/js/logic/posterize-page.ts @@ -1,393 +1,497 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { downloadFile, parsePageRanges, getPDFDocument, formatBytes } from '../utils/helpers.js'; +import { + downloadFile, + parsePageRanges, + formatBytes, +} from '../utils/helpers.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { PDFDocument, PageSizes } from 'pdf-lib'; import * as pdfjsLib from 'pdfjs-dist'; import { createIcons, icons } from 'lucide'; import { PosterizeState } from '@/types'; -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(); const pageState: PosterizeState = { - file: null, - pdfJsDoc: null, - pdfBytes: null, - pageSnapshots: {}, - currentPage: 1, + file: null, + pdfJsDoc: null, + pdfBytes: null, + pageSnapshots: {}, + currentPage: 1, }; function resetState() { - pageState.file = null; - pageState.pdfJsDoc = null; - pageState.pdfBytes = null; - pageState.pageSnapshots = {}; - pageState.currentPage = 1; + pageState.file = null; + pageState.pdfJsDoc = null; + pageState.pdfBytes = null; + pageState.pageSnapshots = {}; + pageState.currentPage = 1; - 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 fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; - const processBtn = document.getElementById('process-btn') as HTMLButtonElement; - if (processBtn) processBtn.disabled = true; + const processBtn = document.getElementById( + 'process-btn' + ) as HTMLButtonElement; + if (processBtn) processBtn.disabled = true; - const totalPages = document.getElementById('total-pages'); - if (totalPages) totalPages.textContent = '0'; + const totalPages = document.getElementById('total-pages'); + if (totalPages) totalPages.textContent = '0'; } async function renderPosterizePreview(pageNum: number) { - if (!pageState.pdfJsDoc) return; + if (!pageState.pdfJsDoc) return; - pageState.currentPage = pageNum; - showLoader(`Rendering preview for page ${pageNum}...`); + pageState.currentPage = pageNum; + showLoader(`Rendering preview for page ${pageNum}...`); - const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement; - const context = canvas.getContext('2d'); + const canvas = document.getElementById( + 'posterize-preview-canvas' + ) as HTMLCanvasElement; + const context = canvas.getContext('2d'); - if (!context) { - hideLoader(); - return; - } - - if (pageState.pageSnapshots[pageNum]) { - canvas.width = pageState.pageSnapshots[pageNum].width; - canvas.height = pageState.pageSnapshots[pageNum].height; - context.putImageData(pageState.pageSnapshots[pageNum], 0, 0); - } else { - const page = await pageState.pdfJsDoc.getPage(pageNum); - const viewport = page.getViewport({ scale: 1.5 }); - canvas.width = viewport.width; - canvas.height = viewport.height; - await page.render({ canvasContext: context, viewport, canvas }).promise; - pageState.pageSnapshots[pageNum] = context.getImageData(0, 0, canvas.width, canvas.height); - } - - updatePreviewNav(); - drawGridOverlay(); + if (!context) { hideLoader(); + return; + } + + if (pageState.pageSnapshots[pageNum]) { + canvas.width = pageState.pageSnapshots[pageNum].width; + canvas.height = pageState.pageSnapshots[pageNum].height; + context.putImageData(pageState.pageSnapshots[pageNum], 0, 0); + } else { + const page = await pageState.pdfJsDoc.getPage(pageNum); + const viewport = page.getViewport({ scale: 1.5 }); + canvas.width = viewport.width; + canvas.height = viewport.height; + await page.render({ canvasContext: context, viewport, canvas }).promise; + pageState.pageSnapshots[pageNum] = context.getImageData( + 0, + 0, + canvas.width, + canvas.height + ); + } + + updatePreviewNav(); + drawGridOverlay(); + hideLoader(); } function drawGridOverlay() { - if (!pageState.pageSnapshots[pageState.currentPage] || !pageState.pdfJsDoc) return; + if (!pageState.pageSnapshots[pageState.currentPage] || !pageState.pdfJsDoc) + return; - const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement; - const context = canvas.getContext('2d'); + const canvas = document.getElementById( + 'posterize-preview-canvas' + ) as HTMLCanvasElement; + const context = canvas.getContext('2d'); - if (!context) return; + if (!context) return; - context.putImageData(pageState.pageSnapshots[pageState.currentPage], 0, 0); + context.putImageData(pageState.pageSnapshots[pageState.currentPage], 0, 0); - const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value; - const pagesToProcess = parsePageRanges(pageRangeInput, pageState.pdfJsDoc.numPages); + const pageRangeInput = ( + document.getElementById('page-range') as HTMLInputElement + ).value; + const pagesToProcess = parsePageRanges( + pageRangeInput, + pageState.pdfJsDoc.numPages + ); - if (pagesToProcess.includes(pageState.currentPage - 1)) { - const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1; - const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1; + if (pagesToProcess.includes(pageState.currentPage - 1)) { + const rows = + parseInt( + (document.getElementById('posterize-rows') as HTMLInputElement).value + ) || 1; + const cols = + parseInt( + (document.getElementById('posterize-cols') as HTMLInputElement).value + ) || 1; - context.strokeStyle = 'rgba(239, 68, 68, 0.9)'; - context.lineWidth = 2; - context.setLineDash([10, 5]); + context.strokeStyle = 'rgba(239, 68, 68, 0.9)'; + context.lineWidth = 2; + context.setLineDash([10, 5]); - const cellWidth = canvas.width / cols; - const cellHeight = canvas.height / rows; + const cellWidth = canvas.width / cols; + const cellHeight = canvas.height / rows; - for (let i = 1; i < cols; i++) { - context.beginPath(); - context.moveTo(i * cellWidth, 0); - context.lineTo(i * cellWidth, canvas.height); - context.stroke(); - } - - for (let i = 1; i < rows; i++) { - context.beginPath(); - context.moveTo(0, i * cellHeight); - context.lineTo(canvas.width, i * cellHeight); - context.stroke(); - } - - context.setLineDash([]); + for (let i = 1; i < cols; i++) { + context.beginPath(); + context.moveTo(i * cellWidth, 0); + context.lineTo(i * cellWidth, canvas.height); + context.stroke(); } + + for (let i = 1; i < rows; i++) { + context.beginPath(); + context.moveTo(0, i * cellHeight); + context.lineTo(canvas.width, i * cellHeight); + context.stroke(); + } + + context.setLineDash([]); + } } function updatePreviewNav() { - if (!pageState.pdfJsDoc) return; + if (!pageState.pdfJsDoc) return; - const currentPageSpan = document.getElementById('current-preview-page'); - const prevBtn = document.getElementById('prev-preview-page') as HTMLButtonElement; - const nextBtn = document.getElementById('next-preview-page') as HTMLButtonElement; + const currentPageSpan = document.getElementById('current-preview-page'); + const prevBtn = document.getElementById( + 'prev-preview-page' + ) as HTMLButtonElement; + const nextBtn = document.getElementById( + 'next-preview-page' + ) as HTMLButtonElement; - if (currentPageSpan) currentPageSpan.textContent = pageState.currentPage.toString(); - if (prevBtn) prevBtn.disabled = pageState.currentPage <= 1; - if (nextBtn) nextBtn.disabled = pageState.currentPage >= pageState.pdfJsDoc.numPages; + if (currentPageSpan) + currentPageSpan.textContent = pageState.currentPage.toString(); + if (prevBtn) prevBtn.disabled = pageState.currentPage <= 1; + if (nextBtn) + nextBtn.disabled = pageState.currentPage >= pageState.pdfJsDoc.numPages; } async function posterize() { - if (!pageState.pdfJsDoc || !pageState.pdfBytes) { - showAlert('No File', 'Please upload a PDF file first.'); - return; + if (!pageState.pdfJsDoc || !pageState.pdfBytes) { + showAlert('No File', 'Please upload a PDF file first.'); + return; + } + + showLoader('Posterizing PDF...'); + + try { + const rows = + parseInt( + (document.getElementById('posterize-rows') as HTMLInputElement).value + ) || 1; + const cols = + parseInt( + (document.getElementById('posterize-cols') as HTMLInputElement).value + ) || 1; + const pageSizeKey = ( + document.getElementById('output-page-size') as HTMLSelectElement + ).value as keyof typeof PageSizes; + const orientation = ( + document.getElementById('output-orientation') as HTMLSelectElement + ).value; + const scalingMode = ( + document.querySelector( + 'input[name="scaling-mode"]:checked' + ) as HTMLInputElement + ).value; + const overlap = + parseFloat( + (document.getElementById('overlap') as HTMLInputElement).value + ) || 0; + const overlapUnits = ( + document.getElementById('overlap-units') as HTMLSelectElement + ).value; + const pageRangeInput = ( + document.getElementById('page-range') as HTMLInputElement + ).value; + + let overlapInPoints = overlap; + if (overlapUnits === 'in') overlapInPoints = overlap * 72; + else if (overlapUnits === 'mm') overlapInPoints = overlap * (72 / 25.4); + + const newDoc = await PDFDocument.create(); + const totalPages = pageState.pdfJsDoc.numPages; + const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages); + + if (pageIndicesToProcess.length === 0) { + throw new Error('Invalid page range specified.'); } - showLoader('Posterizing PDF...'); + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); - try { - const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1; - const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1; - const pageSizeKey = (document.getElementById('output-page-size') as HTMLSelectElement).value as keyof typeof PageSizes; - const orientation = (document.getElementById('output-orientation') as HTMLSelectElement).value; - const scalingMode = (document.querySelector('input[name="scaling-mode"]:checked') as HTMLInputElement).value; - const overlap = parseFloat((document.getElementById('overlap') as HTMLInputElement).value) || 0; - const overlapUnits = (document.getElementById('overlap-units') as HTMLSelectElement).value; - const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value; - - let overlapInPoints = overlap; - if (overlapUnits === 'in') overlapInPoints = overlap * 72; - else if (overlapUnits === 'mm') overlapInPoints = overlap * (72 / 25.4); - - const newDoc = await PDFDocument.create(); - const totalPages = pageState.pdfJsDoc.numPages; - const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages); - - if (pageIndicesToProcess.length === 0) { - throw new Error('Invalid page range specified.'); - } - - const tempCanvas = document.createElement('canvas'); - const tempCtx = tempCanvas.getContext('2d'); - - if (!tempCtx) { - throw new Error('Could not create canvas context.'); - } - - for (const pageIndex of pageIndicesToProcess) { - const page = await pageState.pdfJsDoc.getPage(Number(pageIndex) + 1); - const viewport = page.getViewport({ scale: 2.0 }); - tempCanvas.width = viewport.width; - tempCanvas.height = viewport.height; - await page.render({ canvasContext: tempCtx, viewport, canvas: tempCanvas }).promise; - - let [targetWidth, targetHeight] = PageSizes[pageSizeKey] || PageSizes.A4; - let currentOrientation = orientation; - - if (currentOrientation === 'auto') { - currentOrientation = viewport.width > viewport.height ? 'landscape' : 'portrait'; - } - - if (currentOrientation === 'landscape' && targetWidth < targetHeight) { - [targetWidth, targetHeight] = [targetHeight, targetWidth]; - } else if (currentOrientation === 'portrait' && targetWidth > targetHeight) { - [targetWidth, targetHeight] = [targetHeight, targetWidth]; - } - - const tileWidth = tempCanvas.width / cols; - const tileHeight = tempCanvas.height / rows; - - for (let r = 0; r < rows; r++) { - for (let c = 0; c < cols; c++) { - const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0); - const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0); - const sWidth = tileWidth + (c > 0 ? overlapInPoints : 0) + (c < cols - 1 ? overlapInPoints : 0); - const sHeight = tileHeight + (r > 0 ? overlapInPoints : 0) + (r < rows - 1 ? overlapInPoints : 0); - - const tileCanvas = document.createElement('canvas'); - tileCanvas.width = sWidth; - tileCanvas.height = sHeight; - const tileCtx = tileCanvas.getContext('2d'); - - if (tileCtx) { - tileCtx.drawImage(tempCanvas, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight); - - const tileImage = await newDoc.embedPng(tileCanvas.toDataURL('image/png')); - const newPage = newDoc.addPage([targetWidth, targetHeight]); - - const scaleX = newPage.getWidth() / sWidth; - const scaleY = newPage.getHeight() / sHeight; - const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY); - - const scaledWidth = sWidth * scale; - const scaledHeight = sHeight * scale; - - newPage.drawImage(tileImage, { - x: (newPage.getWidth() - scaledWidth) / 2, - y: (newPage.getHeight() - scaledHeight) / 2, - width: scaledWidth, - height: scaledHeight, - }); - } - } - } - } - - const newPdfBytes = await newDoc.save(); - downloadFile( - new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), - 'posterized.pdf' - ); - - showAlert('Success', 'Your PDF has been posterized.'); - } catch (e) { - console.error(e); - showAlert('Error', (e as Error).message || 'Could not posterize the PDF.'); - } finally { - hideLoader(); + if (!tempCtx) { + throw new Error('Could not create canvas context.'); } + + for (const pageIndex of pageIndicesToProcess) { + const page = await pageState.pdfJsDoc.getPage(Number(pageIndex) + 1); + const viewport = page.getViewport({ scale: 2.0 }); + tempCanvas.width = viewport.width; + tempCanvas.height = viewport.height; + await page.render({ + canvasContext: tempCtx, + viewport, + canvas: tempCanvas, + }).promise; + + let [targetWidth, targetHeight] = PageSizes[pageSizeKey] || PageSizes.A4; + let currentOrientation = orientation; + + if (currentOrientation === 'auto') { + currentOrientation = + viewport.width > viewport.height ? 'landscape' : 'portrait'; + } + + if (currentOrientation === 'landscape' && targetWidth < targetHeight) { + [targetWidth, targetHeight] = [targetHeight, targetWidth]; + } else if ( + currentOrientation === 'portrait' && + targetWidth > targetHeight + ) { + [targetWidth, targetHeight] = [targetHeight, targetWidth]; + } + + const tileWidth = tempCanvas.width / cols; + const tileHeight = tempCanvas.height / rows; + + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0); + const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0); + const sWidth = + tileWidth + + (c > 0 ? overlapInPoints : 0) + + (c < cols - 1 ? overlapInPoints : 0); + const sHeight = + tileHeight + + (r > 0 ? overlapInPoints : 0) + + (r < rows - 1 ? overlapInPoints : 0); + + const tileCanvas = document.createElement('canvas'); + tileCanvas.width = sWidth; + tileCanvas.height = sHeight; + const tileCtx = tileCanvas.getContext('2d'); + + if (tileCtx) { + tileCtx.drawImage( + tempCanvas, + sx, + sy, + sWidth, + sHeight, + 0, + 0, + sWidth, + sHeight + ); + + const tileImage = await newDoc.embedPng( + tileCanvas.toDataURL('image/png') + ); + const newPage = newDoc.addPage([targetWidth, targetHeight]); + + const scaleX = newPage.getWidth() / sWidth; + const scaleY = newPage.getHeight() / sHeight; + const scale = + scalingMode === 'fit' + ? Math.min(scaleX, scaleY) + : Math.max(scaleX, scaleY); + + const scaledWidth = sWidth * scale; + const scaledHeight = sHeight * scale; + + newPage.drawImage(tileImage, { + x: (newPage.getWidth() - scaledWidth) / 2, + y: (newPage.getHeight() - scaledHeight) / 2, + width: scaledWidth, + height: scaledHeight, + }); + } + } + } + } + + const newPdfBytes = await newDoc.save(); + downloadFile( + new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), + 'posterized.pdf' + ); + + showAlert('Success', 'Your PDF has been posterized.'); + } catch (e) { + console.error(e); + showAlert('Error', (e as Error).message || 'Could not posterize the PDF.'); + } finally { + hideLoader(); + } } async function updateUI() { - const fileDisplayArea = document.getElementById('file-display-area'); - const toolOptions = document.getElementById('tool-options'); - const processBtn = document.getElementById('process-btn') as HTMLButtonElement; + const fileDisplayArea = document.getElementById('file-display-area'); + const toolOptions = document.getElementById('tool-options'); + const processBtn = document.getElementById( + 'process-btn' + ) as HTMLButtonElement; - 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.file) { + 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 = pageState.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(pageState.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 () { + resetState(); + }; - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); - if (toolOptions) toolOptions.classList.remove('hidden'); - if (processBtn) processBtn.disabled = false; - } else { - if (toolOptions) toolOptions.classList.add('hidden'); - } + if (toolOptions) toolOptions.classList.remove('hidden'); + if (processBtn) processBtn.disabled = false; + } else { + if (toolOptions) toolOptions.classList.add('hidden'); + } } async 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; - pageState.pdfBytes = new Uint8Array(await file.arrayBuffer()); - pageState.pdfJsDoc = await getPDFDocument({ data: pageState.pdfBytes }).promise; - pageState.pageSnapshots = {}; - pageState.currentPage = 1; + if (files && files.length > 0) { + const file = files[0]; + if ( + file.type === 'application/pdf' || + file.name.toLowerCase().endsWith('.pdf') + ) { + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; - const totalPagesSpan = document.getElementById('total-pages'); - const totalPreviewPages = document.getElementById('total-preview-pages'); + pageState.file = result.file; + pageState.pdfBytes = new Uint8Array(result.bytes); + pageState.pdfJsDoc = result.pdf; + pageState.pageSnapshots = {}; + pageState.currentPage = 1; - if (totalPagesSpan && pageState.pdfJsDoc) { - totalPagesSpan.textContent = pageState.pdfJsDoc.numPages.toString(); - } - if (totalPreviewPages && pageState.pdfJsDoc) { - totalPreviewPages.textContent = pageState.pdfJsDoc.numPages.toString(); - } + const totalPagesSpan = document.getElementById('total-pages'); + const totalPreviewPages = document.getElementById('total-preview-pages'); - await updateUI(); - await renderPosterizePreview(1); - } + if (totalPagesSpan && pageState.pdfJsDoc) { + totalPagesSpan.textContent = pageState.pdfJsDoc.numPages.toString(); + } + if (totalPreviewPages && pageState.pdfJsDoc) { + totalPreviewPages.textContent = pageState.pdfJsDoc.numPages.toString(); + } + + await updateUI(); + await renderPosterizePreview(1); } + } } document.addEventListener('DOMContentLoaded', function () { - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const processBtn = document.getElementById('process-btn') as HTMLButtonElement; - const backBtn = document.getElementById('back-to-tools'); - const prevBtn = document.getElementById('prev-preview-page'); - const nextBtn = document.getElementById('next-preview-page'); - const rowsInput = document.getElementById('posterize-rows'); - const colsInput = document.getElementById('posterize-cols'); - const pageRangeInput = document.getElementById('page-range'); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const processBtn = document.getElementById( + 'process-btn' + ) as HTMLButtonElement; + const backBtn = document.getElementById('back-to-tools'); + const prevBtn = document.getElementById('prev-preview-page'); + const nextBtn = document.getElementById('next-preview-page'); + const rowsInput = document.getElementById('posterize-rows'); + const colsInput = document.getElementById('posterize-cols'); + const pageRangeInput = document.getElementById('page-range'); - 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); + }); + + 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('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); + } + } + }); - if (fileInput && dropZone) { - fileInput.addEventListener('change', function (e) { - handleFileSelect((e.target as HTMLInputElement).files); - }); + fileInput.addEventListener('click', function () { + fileInput.value = ''; + }); + } - dropZone.addEventListener('dragover', function (e) { - e.preventDefault(); - dropZone.classList.add('bg-gray-700'); - }); + // Preview navigation + if (prevBtn) { + prevBtn.addEventListener('click', function () { + if (pageState.currentPage > 1) { + renderPosterizePreview(pageState.currentPage - 1); + } + }); + } - dropZone.addEventListener('dragleave', function (e) { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - }); + if (nextBtn) { + nextBtn.addEventListener('click', function () { + if ( + pageState.pdfJsDoc && + pageState.currentPage < pageState.pdfJsDoc.numPages + ) { + renderPosterizePreview(pageState.currentPage + 1); + } + }); + } - 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); - } - } - }); + // Grid input changes trigger overlay redraw + if (rowsInput) { + rowsInput.addEventListener('input', drawGridOverlay); + } + if (colsInput) { + colsInput.addEventListener('input', drawGridOverlay); + } + if (pageRangeInput) { + pageRangeInput.addEventListener('input', drawGridOverlay); + } - fileInput.addEventListener('click', function () { - fileInput.value = ''; - }); - } - - // Preview navigation - if (prevBtn) { - prevBtn.addEventListener('click', function () { - if (pageState.currentPage > 1) { - renderPosterizePreview(pageState.currentPage - 1); - } - }); - } - - if (nextBtn) { - nextBtn.addEventListener('click', function () { - if (pageState.pdfJsDoc && pageState.currentPage < pageState.pdfJsDoc.numPages) { - renderPosterizePreview(pageState.currentPage + 1); - } - }); - } - - // Grid input changes trigger overlay redraw - if (rowsInput) { - rowsInput.addEventListener('input', drawGridOverlay); - } - if (colsInput) { - colsInput.addEventListener('input', drawGridOverlay); - } - if (pageRangeInput) { - pageRangeInput.addEventListener('input', drawGridOverlay); - } - - // Process button - if (processBtn) { - processBtn.addEventListener('click', posterize); - } + // Process button + if (processBtn) { + processBtn.addEventListener('click', posterize); + } }); diff --git a/src/js/logic/prepare-pdf-for-ai-page.ts b/src/js/logic/prepare-pdf-for-ai-page.ts index 80fa970..b4ff23b 100644 --- a/src/js/logic/prepare-pdf-for-ai-page.ts +++ b/src/js/logic/prepare-pdf-for-ai-page.ts @@ -9,6 +9,7 @@ import { import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; +import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js'; document.addEventListener('DOMContentLoaded', () => { @@ -104,6 +105,10 @@ document.addEventListener('DOMContentLoaded', () => { showLoader('Loading engine...'); const pymupdf = await loadPyMuPDF(); + hideLoader(); + state.files = await batchDecryptIfNeeded(state.files); + showLoader('Extracting...'); + const total = state.files.length; let completed = 0; let failed = 0; @@ -128,13 +133,13 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } else { - // Multiple files - create ZIP const JSZip = (await import('jszip')).default; const zip = new JSZip(); const usedNames = new Set(); - for (const file of state.files) { + for (let fi = 0; fi < state.files.length; fi++) { try { + const file = state.files[fi]; showLoader( `Extracting ${file.name} for AI (${completed + 1}/${total})...` ); @@ -147,7 +152,7 @@ document.addEventListener('DOMContentLoaded', () => { completed++; } catch (error) { - console.error(`Failed to extract ${file.name}:`, error); + console.error(`Failed to extract ${state.files[fi].name}:`, error); failed++; } } diff --git a/src/js/logic/rasterize-pdf-page.ts b/src/js/logic/rasterize-pdf-page.ts index 56251d8..ea4dd6a 100644 --- a/src/js/logic/rasterize-pdf-page.ts +++ b/src/js/logic/rasterize-pdf-page.ts @@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; +import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js'; document.addEventListener('DOMContentLoaded', () => { @@ -123,6 +124,10 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('rasterize-grayscale') as HTMLInputElement ).checked; + hideLoader(); + state.files = await batchDecryptIfNeeded(state.files); + showLoader('Rasterizing...'); + const total = state.files.length; let completed = 0; let failed = 0; @@ -149,13 +154,13 @@ document.addEventListener('DOMContentLoaded', () => { () => resetState() ); } else { - // Multiple files - create ZIP const JSZip = (await import('jszip')).default; const zip = new JSZip(); const usedNames = new Set(); - for (const file of state.files) { + for (let fi = 0; fi < state.files.length; fi++) { try { + const file = state.files[fi]; showLoader( `Rasterizing ${file.name} (${completed + 1}/${total})...` ); @@ -174,7 +179,10 @@ document.addEventListener('DOMContentLoaded', () => { completed++; } catch (error) { - console.error(`Failed to rasterize ${file.name}:`, error); + console.error( + `Failed to rasterize ${state.files[fi].name}:`, + error + ); failed++; } } diff --git a/src/js/logic/remove-annotations-page.ts b/src/js/logic/remove-annotations-page.ts index 5b55060..5aed913 100644 --- a/src/js/logic/remove-annotations-page.ts +++ b/src/js/logic/remove-annotations-page.ts @@ -1,64 +1,71 @@ import { PDFDocument, PDFName } from 'pdf-lib'; import { createIcons, icons } from 'lucide'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; // State management const pageState: { pdfDoc: PDFDocument | null; file: File | null } = { - pdfDoc: null, - file: null, + pdfDoc: null, + file: null, }; // UI helpers function showLoader(message: string = 'Processing...') { - const loader = document.getElementById('loader-modal'); - const loaderText = document.getElementById('loader-text'); - if (loader) loader.classList.remove('hidden'); - if (loaderText) loaderText.textContent = message; + const loader = document.getElementById('loader-modal'); + const loaderText = document.getElementById('loader-text'); + if (loader) loader.classList.remove('hidden'); + if (loaderText) loaderText.textContent = message; } function hideLoader() { - const loader = document.getElementById('loader-modal'); - if (loader) loader.classList.add('hidden'); + const loader = document.getElementById('loader-modal'); + if (loader) loader.classList.add('hidden'); } -function showAlert(title: string, message: string, type: string = 'error', callback?: () => void) { - const modal = document.getElementById('alert-modal'); - const alertTitle = document.getElementById('alert-title'); - const alertMessage = document.getElementById('alert-message'); - const okBtn = document.getElementById('alert-ok'); +function showAlert( + title: string, + message: string, + type: string = 'error', + callback?: () => void +) { + const modal = document.getElementById('alert-modal'); + const alertTitle = document.getElementById('alert-title'); + const alertMessage = document.getElementById('alert-message'); + const okBtn = document.getElementById('alert-ok'); - if (alertTitle) alertTitle.textContent = title; - if (alertMessage) alertMessage.textContent = message; - if (modal) modal.classList.remove('hidden'); + if (alertTitle) alertTitle.textContent = title; + if (alertMessage) alertMessage.textContent = message; + if (modal) modal.classList.remove('hidden'); - if (okBtn) { - const newOkBtn = okBtn.cloneNode(true) as HTMLElement; - okBtn.replaceWith(newOkBtn); - newOkBtn.addEventListener('click', () => { - modal?.classList.add('hidden'); - if (callback) callback(); - }); - } + if (okBtn) { + const newOkBtn = okBtn.cloneNode(true) as HTMLElement; + okBtn.replaceWith(newOkBtn); + newOkBtn.addEventListener('click', () => { + modal?.classList.add('hidden'); + if (callback) callback(); + }); + } } 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 displayArea = document.getElementById('file-display-area'); - if (!displayArea || !pageState.file || !pageState.pdfDoc) return; + const displayArea = document.getElementById('file-display-area'); + if (!displayArea || !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(); - displayArea.innerHTML = ` + displayArea.innerHTML = `
@@ -72,105 +79,114 @@ function updateFileDisplay() {
`; - createIcons({ icons }); + createIcons({ icons }); - document.getElementById('remove-file')?.addEventListener('click', () => resetState()); + document + .getElementById('remove-file') + ?.addEventListener('click', () => resetState()); } function resetState() { - pageState.pdfDoc = null; - pageState.file = null; - const displayArea = document.getElementById('file-display-area'); - if (displayArea) displayArea.innerHTML = ''; - document.getElementById('options-panel')?.classList.add('hidden'); - const fileInput = document.getElementById('file-input') as HTMLInputElement; - if (fileInput) fileInput.value = ''; + pageState.pdfDoc = null; + pageState.file = null; + const displayArea = document.getElementById('file-display-area'); + if (displayArea) displayArea.innerHTML = ''; + document.getElementById('options-panel')?.classList.add('hidden'); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; } // File handling async function handleFileUpload(file: File) { - if (!file || file.type !== 'application/pdf') { - showAlert('Error', 'Please upload a valid PDF file.'); - return; - } + if (!file || file.type !== 'application/pdf') { + showAlert('Error', 'Please upload a valid PDF file.'); + return; + } + try { + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; showLoader('Loading PDF...'); - try { - const arrayBuffer = await file.arrayBuffer(); - pageState.pdfDoc = await PDFDocument.load(arrayBuffer); - pageState.file = file; - updateFileDisplay(); - document.getElementById('options-panel')?.classList.remove('hidden'); - } catch (error) { - console.error(error); - showAlert('Error', 'Failed to load PDF file.'); - } finally { - hideLoader(); - } + result.pdf.destroy(); + pageState.pdfDoc = await PDFDocument.load(result.bytes); + pageState.file = result.file; + updateFileDisplay(); + document.getElementById('options-panel')?.classList.remove('hidden'); + } catch (error) { + console.error(error); + showAlert('Error', 'Failed to load PDF file.'); + } finally { + hideLoader(); + } } // Process function async function processRemoveAnnotations() { - if (!pageState.pdfDoc) { - showAlert('Error', 'Please upload a PDF file first.'); - return; + if (!pageState.pdfDoc) { + showAlert('Error', 'Please upload a PDF file first.'); + return; + } + + showLoader('Removing annotations...'); + try { + const pages = pageState.pdfDoc.getPages(); + + // Remove all annotations from all pages + for (let i = 0; i < pages.length; i++) { + const page = pages[i]; + const annotRefs = page.node.Annots()?.asArray() || []; + if (annotRefs.length > 0) { + page.node.delete(PDFName.of('Annots')); + } } - showLoader('Removing annotations...'); - try { - const pages = pageState.pdfDoc.getPages(); - - // Remove all annotations from all pages - for (let i = 0; i < pages.length; i++) { - const page = pages[i]; - const annotRefs = page.node.Annots()?.asArray() || []; - if (annotRefs.length > 0) { - page.node.delete(PDFName.of('Annots')); - } - } - - const newPdfBytes = await pageState.pdfDoc.save(); - downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'annotations-removed.pdf'); - showAlert('Success', 'Annotations removed successfully!', 'success', () => { resetState(); }); - } catch (e) { - console.error(e); - showAlert('Error', 'Could not remove annotations.'); - } finally { - hideLoader(); - } + const newPdfBytes = await pageState.pdfDoc.save(); + downloadFile( + new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), + 'annotations-removed.pdf' + ); + showAlert('Success', 'Annotations removed successfully!', 'success', () => { + resetState(); + }); + } catch (e) { + console.error(e); + showAlert('Error', 'Could not remove annotations.'); + } finally { + hideLoader(); + } } // Initialize document.addEventListener('DOMContentLoaded', () => { - 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 backBtn = document.getElementById('back-to-tools'); - fileInput?.addEventListener('change', (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; - if (file) handleFileUpload(file); - }); + fileInput?.addEventListener('change', (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (file) handleFileUpload(file); + }); - 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 file = e.dataTransfer?.files[0]; - if (file) handleFileUpload(file); - }); + dropZone?.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('border-indigo-500'); + const file = e.dataTransfer?.files[0]; + if (file) handleFileUpload(file); + }); - processBtn?.addEventListener('click', processRemoveAnnotations); + processBtn?.addEventListener('click', processRemoveAnnotations); - backBtn?.addEventListener('click', () => { - window.location.href = '../../index.html'; - }); + backBtn?.addEventListener('click', () => { + window.location.href = '../../index.html'; + }); }); diff --git a/src/js/logic/remove-blank-pages-page.ts b/src/js/logic/remove-blank-pages-page.ts index dea1fa8..6ce1379 100644 --- a/src/js/logic/remove-blank-pages-page.ts +++ b/src/js/logic/remove-blank-pages-page.ts @@ -2,6 +2,7 @@ import { PDFDocument } from 'pdf-lib'; import * as pdfjsLib from 'pdfjs-dist'; import { createIcons, icons } from 'lucide'; import { initPagePreview } from '../utils/page-preview.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -116,11 +117,13 @@ async function handleFileUpload(file: File) { 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; + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; + showLoader('Loading PDF...'); + result.pdf.destroy(); + pageState.pdfDoc = await PDFDocument.load(result.bytes); + pageState.file = result.file; pageState.detectedBlankPages = []; updateFileDisplay(); document.getElementById('options-panel')?.classList.remove('hidden'); diff --git a/src/js/logic/remove-metadata-page.ts b/src/js/logic/remove-metadata-page.ts index 336413b..6f21385 100644 --- a/src/js/logic/remove-metadata-page.ts +++ b/src/js/logic/remove-metadata-page.ts @@ -2,202 +2,226 @@ import { showAlert } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { PDFDocument, PDFName } from 'pdf-lib'; import { icons, createIcons } from 'lucide'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; interface PageState { - file: File | null; + file: File | null; } const pageState: PageState = { - file: null, + file: null, }; function removeMetadataFromDoc(pdfDoc: PDFDocument) { - const infoDict = (pdfDoc as any).getInfoDict(); - const allKeys = infoDict.keys(); - allKeys.forEach((key: any) => { - infoDict.delete(key); - }); + // @ts-expect-error getInfoDict is private but accessible at runtime + const infoDict = pdfDoc.getInfoDict(); + const allKeys = infoDict.keys(); + allKeys.forEach((key: { asString: () => string }) => { + infoDict.delete(key); + }); - pdfDoc.setTitle(''); - pdfDoc.setAuthor(''); - pdfDoc.setSubject(''); - pdfDoc.setKeywords([]); - pdfDoc.setCreator(''); - pdfDoc.setProducer(''); + pdfDoc.setTitle(''); + pdfDoc.setAuthor(''); + pdfDoc.setSubject(''); + pdfDoc.setKeywords([]); + pdfDoc.setCreator(''); + pdfDoc.setProducer(''); - try { - const catalogDict = (pdfDoc.catalog as any).dict; - if (catalogDict.has(PDFName.of('Metadata'))) { - catalogDict.delete(PDFName.of('Metadata')); - } - } catch (e: any) { - console.warn('Could not remove XMP metadata:', e.message); + try { + // @ts-expect-error catalog.dict is private but accessible at runtime + const catalogDict = pdfDoc.catalog.dict; + if (catalogDict.has(PDFName.of('Metadata'))) { + catalogDict.delete(PDFName.of('Metadata')); } + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + console.warn('Could not remove XMP metadata:', msg); + } - try { - const context = pdfDoc.context; - if ((context as any).trailerInfo) { - delete (context as any).trailerInfo.ID; - } - } catch (e: any) { - console.warn('Could not remove document IDs:', e.message); + try { + const context = pdfDoc.context; + if (context.trailerInfo) { + delete context.trailerInfo.ID; } + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + console.warn('Could not remove document IDs:', msg); + } - try { - const catalogDict = (pdfDoc.catalog as any).dict; - if (catalogDict.has(PDFName.of('PieceInfo'))) { - catalogDict.delete(PDFName.of('PieceInfo')); - } - } catch (e: any) { - console.warn('Could not remove PieceInfo:', e.message); + try { + // @ts-expect-error catalog.dict is private but accessible at runtime + const catalogDict = pdfDoc.catalog.dict; + if (catalogDict.has(PDFName.of('PieceInfo'))) { + catalogDict.delete(PDFName.of('PieceInfo')); } + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + console.warn('Could not remove PieceInfo:', msg); + } } function resetState() { - pageState.file = null; + pageState.file = null; - 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 fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.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'); - 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.file) { + 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 = pageState.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(pageState.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 () { + resetState(); + }; - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); - if (toolOptions) toolOptions.classList.remove('hidden'); - } else { - if (toolOptions) toolOptions.classList.add('hidden'); - } + if (toolOptions) toolOptions.classList.remove('hidden'); + } else { + if (toolOptions) toolOptions.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 file = files[0]; + if ( + file.type === 'application/pdf' || + file.name.toLowerCase().endsWith('.pdf') + ) { + pageState.file = file; + updateUI(); } + } } async function removeMetadata() { - if (!pageState.file) { - showAlert('No File', 'Please upload a PDF file first.'); - return; - } + if (!pageState.file) { + showAlert('No File', 'Please upload a PDF file first.'); + return; + } - const loaderModal = document.getElementById('loader-modal'); - const loaderText = document.getElementById('loader-text'); + const loaderModal = document.getElementById('loader-modal'); + const loaderText = document.getElementById('loader-text'); + if (loaderModal) loaderModal.classList.remove('hidden'); + if (loaderText) loaderText.textContent = 'Removing all metadata...'; + + try { + if (loaderModal) loaderModal.classList.add('hidden'); + const result = await loadPdfWithPasswordPrompt(pageState.file); + if (!result) { + if (loaderModal) loaderModal.classList.add('hidden'); + return; + } if (loaderModal) loaderModal.classList.remove('hidden'); if (loaderText) loaderText.textContent = 'Removing all metadata...'; + result.pdf.destroy(); + const pdfDoc = await PDFDocument.load(result.bytes); - try { - const arrayBuffer = await pageState.file.arrayBuffer(); - const pdfDoc = await PDFDocument.load(arrayBuffer); + removeMetadataFromDoc(pdfDoc); - removeMetadataFromDoc(pdfDoc); - - const newPdfBytes = await pdfDoc.save(); - downloadFile( - new Blob([newPdfBytes as BlobPart], { type: 'application/pdf' }), - 'metadata-removed.pdf' - ); - showAlert('Success', 'Metadata removed successfully!', 'success', () => { resetState(); }); - } catch (e) { - console.error(e); - showAlert('Error', 'An error occurred while trying to remove metadata.'); - } finally { - if (loaderModal) loaderModal.classList.add('hidden'); - } + const newPdfBytes = await pdfDoc.save(); + downloadFile( + new Blob([newPdfBytes as BlobPart], { type: 'application/pdf' }), + 'metadata-removed.pdf' + ); + showAlert('Success', 'Metadata removed successfully!', 'success', () => { + resetState(); + }); + } catch (e) { + console.error(e); + showAlert('Error', 'An error occurred while trying to remove metadata.'); + } 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 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); + }); + + 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('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); + } + } + }); - if (fileInput && dropZone) { - fileInput.addEventListener('change', function (e) { - handleFileSelect((e.target as HTMLInputElement).files); - }); + fileInput.addEventListener('click', function () { + fileInput.value = ''; + }); + } - 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('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); - } - } - }); - - fileInput.addEventListener('click', function () { - fileInput.value = ''; - }); - } - - if (processBtn) { - processBtn.addEventListener('click', removeMetadata); - } + if (processBtn) { + processBtn.addEventListener('click', removeMetadata); + } }); diff --git a/src/js/logic/repair-pdf.ts b/src/js/logic/repair-pdf.ts index 30046e1..e323af1 100644 --- a/src/js/logic/repair-pdf.ts +++ b/src/js/logic/repair-pdf.ts @@ -7,6 +7,7 @@ import { import { state } from '../state.js'; import JSZip from 'jszip'; import { deduplicateFileName } from '../utils/deduplicate-filename.js'; +import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; export async function repairPdfFile(file: File): Promise { const inputPath = '/input.pdf'; @@ -67,7 +68,9 @@ export async function repairPdf() { const failedRepairs: string[] = []; try { + const decryptedFiles = await batchDecryptIfNeeded(state.files); showLoader('Initializing repair engine...'); + state.files = decryptedFiles; for (let i = 0; i < state.files.length; i++) { const file = state.files[i]; @@ -105,7 +108,9 @@ export async function repairPdf() { if (successfulRepairs.length === 1) { const file = successfulRepairs[0]; - const blob = new Blob([file.data as any], { type: 'application/pdf' }); + const blob = new Blob([new Uint8Array(file.data)], { + type: 'application/pdf', + }); downloadFile(blob, file.name); } else { showLoader('Creating ZIP archive...'); @@ -124,7 +129,7 @@ export async function repairPdf() { if (failedRepairs.length === 0) { showAlert('Success', 'All files repaired successfully!'); } - } catch (error: any) { + } catch (error: unknown) { console.error('Critical error during repair:', error); hideLoader(); showAlert( diff --git a/src/js/logic/reverse-pages-page.ts b/src/js/logic/reverse-pages-page.ts index 22ea9fb..4f0377b 100644 --- a/src/js/logic/reverse-pages-page.ts +++ b/src/js/logic/reverse-pages-page.ts @@ -4,6 +4,7 @@ import { createIcons, icons } from 'lucide'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import JSZip from 'jszip'; import { deduplicateFileName } from '../utils/deduplicate-filename.js'; +import { batchDecryptIfNeeded } from '../utils/password-prompt.js'; interface ReverseState { files: File[]; @@ -76,75 +77,61 @@ function updateUI() { } } +async function reverseSingleFile(file: File): Promise { + const arrayBuffer = await file.arrayBuffer(); + const pdfDoc = await PDFLibDocument.load(arrayBuffer); + + const newPdf = await PDFLibDocument.create(); + const pageCount = pdfDoc.getPageCount(); + const reversedIndices = Array.from({ length: pageCount }, function (_, i) { + return pageCount - 1 - i; + }); + + const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices); + copiedPages.forEach(function (page) { + newPdf.addPage(page); + }); + + return newPdf.save(); +} + async function reversePages() { if (reverseState.files.length === 0) { showAlert('No Files', 'Please select one or more PDF files.'); return; } - showLoader('Reversing page order...'); - try { + const decryptedFiles = await batchDecryptIfNeeded(reverseState.files); + showLoader('Reversing page order...'); + reverseState.files = decryptedFiles; + + const validFiles = reverseState.files.filter(function (f) { + return f !== null; + }); + + if (validFiles.length === 0) { + hideLoader(); + return; + } + const zip = new JSZip(); const usedNames = new Set(); - for (let j = 0; j < reverseState.files.length; j++) { - const file = reverseState.files[j]; - showLoader( - `Processing ${file.name} (${j + 1}/${reverseState.files.length})...` - ); + for (let j = 0; j < validFiles.length; j++) { + const file = validFiles[j]; + showLoader(`Reversing ${file.name} (${j + 1}/${validFiles.length})...`); - const arrayBuffer = await file.arrayBuffer(); - const pdfDoc = await PDFLibDocument.load(arrayBuffer, { - ignoreEncryption: true, - throwOnInvalidObject: false, - }); - - const newPdf = await PDFLibDocument.create(); - const pageCount = pdfDoc.getPageCount(); - const reversedIndices = Array.from( - { length: pageCount }, - function (_, i) { - return pageCount - 1 - i; - } - ); - - const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices); - copiedPages.forEach(function (page) { - newPdf.addPage(page); - }); - - const newPdfBytes = await newPdf.save(); + const newPdfBytes = await reverseSingleFile(file); const originalName = file.name.replace(/\.pdf$/i, ''); const fileName = `${originalName}_reversed.pdf`; const zipEntryName = deduplicateFileName(fileName, usedNames); zip.file(zipEntryName, newPdfBytes); } - if (reverseState.files.length === 1) { - // Single file: download directly - const file = reverseState.files[0]; - const arrayBuffer = await file.arrayBuffer(); - const pdfDoc = await PDFLibDocument.load(arrayBuffer, { - ignoreEncryption: true, - throwOnInvalidObject: false, - }); - - const newPdf = await PDFLibDocument.create(); - const pageCount = pdfDoc.getPageCount(); - const reversedIndices = Array.from( - { length: pageCount }, - function (_, i) { - return pageCount - 1 - i; - } - ); - - const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices); - copiedPages.forEach(function (page) { - newPdf.addPage(page); - }); - - const newPdfBytes = await newPdf.save(); + if (validFiles.length === 1) { + const file = validFiles[0]; + const newPdfBytes = await reverseSingleFile(file); const originalName = file.name.replace(/\.pdf$/i, ''); downloadFile( @@ -152,7 +139,6 @@ async function reversePages() { `${originalName}_reversed.pdf` ); } else { - // Multiple files: download as ZIP const zipBlob = await zip.generateAsync({ type: 'blob' }); downloadFile(zipBlob, 'reversed_pdfs.zip'); } diff --git a/src/js/logic/rotate-custom-page.ts b/src/js/logic/rotate-custom-page.ts index 2d5ebec..459eb28 100644 --- a/src/js/logic/rotate-custom-page.ts +++ b/src/js/logic/rotate-custom-page.ts @@ -1,386 +1,443 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js'; +import { downloadFile, formatBytes } from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; import { PDFDocument as PDFLibDocument, degrees } from 'pdf-lib'; -import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js'; +import { + renderPagesProgressively, + cleanupLazyRendering, +} from '../utils/render-utils.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import * as pdfjsLib from 'pdfjs-dist'; -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(); interface RotateState { - file: File | null; - pdfDoc: PDFLibDocument | null; - pdfJsDoc: pdfjsLib.PDFDocumentProxy | null; - rotations: number[]; + file: File | null; + pdfDoc: PDFLibDocument | null; + pdfJsDoc: pdfjsLib.PDFDocumentProxy | null; + rotations: number[]; } const pageState: RotateState = { - file: null, - pdfDoc: null, - pdfJsDoc: null, - rotations: [], + file: null, + pdfDoc: null, + pdfJsDoc: null, + rotations: [], }; function resetState() { - cleanupLazyRendering(); - pageState.file = null; - pageState.pdfDoc = null; - pageState.pdfJsDoc = null; - pageState.rotations = []; + cleanupLazyRendering(); + pageState.file = null; + pageState.pdfDoc = null; + pageState.pdfJsDoc = null; + pageState.rotations = []; - 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 pageThumbnails = document.getElementById('page-thumbnails'); - if (pageThumbnails) pageThumbnails.innerHTML = ''; + const pageThumbnails = document.getElementById('page-thumbnails'); + if (pageThumbnails) pageThumbnails.innerHTML = ''; - const fileInput = document.getElementById('file-input') as HTMLInputElement; - if (fileInput) fileInput.value = ''; + const fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; - const batchAngle = document.getElementById('batch-custom-angle') as HTMLInputElement; - if (batchAngle) batchAngle.value = '0'; + const batchAngle = document.getElementById( + 'batch-custom-angle' + ) as HTMLInputElement; + if (batchAngle) batchAngle.value = '0'; } function updateAllRotationDisplays() { - for (let i = 0; i < pageState.rotations.length; i++) { - const input = document.getElementById(`page-angle-${i}`) as HTMLInputElement; - if (input) input.value = pageState.rotations[i].toString(); - const container = document.querySelector(`[data-page-index="${i}"]`); - if (container) { - const wrapper = container.querySelector('.thumbnail-wrapper') as HTMLElement; - if (wrapper) wrapper.style.transform = `rotate(${-pageState.rotations[i]}deg)`; - } + for (let i = 0; i < pageState.rotations.length; i++) { + const input = document.getElementById( + `page-angle-${i}` + ) as HTMLInputElement; + if (input) input.value = pageState.rotations[i].toString(); + const container = document.querySelector(`[data-page-index="${i}"]`); + if (container) { + const wrapper = container.querySelector( + '.thumbnail-wrapper' + ) as HTMLElement; + if (wrapper) + wrapper.style.transform = `rotate(${-pageState.rotations[i]}deg)`; } + } } -function createPageWrapper(canvas: HTMLCanvasElement, pageNumber: number): HTMLElement { - const pageIndex = pageNumber - 1; +function createPageWrapper( + canvas: HTMLCanvasElement, + pageNumber: number +): HTMLElement { + const pageIndex = pageNumber - 1; - const container = document.createElement('div'); - container.className = 'page-thumbnail relative bg-gray-700 rounded-lg overflow-hidden'; - container.dataset.pageIndex = pageIndex.toString(); - container.dataset.pageNumber = pageNumber.toString(); + const container = document.createElement('div'); + container.className = + 'page-thumbnail relative bg-gray-700 rounded-lg overflow-hidden'; + container.dataset.pageIndex = pageIndex.toString(); + container.dataset.pageNumber = pageNumber.toString(); - const canvasWrapper = document.createElement('div'); - canvasWrapper.className = 'thumbnail-wrapper flex items-center justify-center p-2 h-36'; - canvasWrapper.style.transition = 'transform 0.3s ease'; - // Apply initial rotation if it exists (negated for canvas display) - const initialRotation = pageState.rotations[pageIndex] || 0; - canvasWrapper.style.transform = `rotate(${-initialRotation}deg)`; + const canvasWrapper = document.createElement('div'); + canvasWrapper.className = + 'thumbnail-wrapper flex items-center justify-center p-2 h-36'; + canvasWrapper.style.transition = 'transform 0.3s ease'; + // Apply initial rotation if it exists (negated for canvas display) + const initialRotation = pageState.rotations[pageIndex] || 0; + canvasWrapper.style.transform = `rotate(${-initialRotation}deg)`; - canvas.className = 'max-w-full max-h-full object-contain'; - canvasWrapper.appendChild(canvas); + canvas.className = 'max-w-full max-h-full object-contain'; + canvasWrapper.appendChild(canvas); - const pageLabel = document.createElement('div'); - pageLabel.className = 'absolute top-1 left-1 bg-black bg-opacity-60 text-white text-xs px-2 py-1 rounded'; - pageLabel.textContent = `${pageNumber}`; + const pageLabel = document.createElement('div'); + pageLabel.className = + 'absolute top-1 left-1 bg-black bg-opacity-60 text-white text-xs px-2 py-1 rounded'; + pageLabel.textContent = `${pageNumber}`; - container.appendChild(canvasWrapper); - container.appendChild(pageLabel); + container.appendChild(canvasWrapper); + container.appendChild(pageLabel); - // Per-page rotation controls - Custom angle input - const controls = document.createElement('div'); - controls.className = 'flex items-center justify-center gap-1 p-2 bg-gray-800'; + // Per-page rotation controls - Custom angle input + const controls = document.createElement('div'); + controls.className = 'flex items-center justify-center gap-1 p-2 bg-gray-800'; - const decrementBtn = document.createElement('button'); - decrementBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-sm'; - decrementBtn.textContent = '-'; - decrementBtn.onclick = function (e) { - e.stopPropagation(); - const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement; - const current = parseInt(input.value) || 0; - input.value = (current - 1).toString(); - }; + const decrementBtn = document.createElement('button'); + decrementBtn.className = + 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-sm'; + decrementBtn.textContent = '-'; + decrementBtn.onclick = function (e) { + e.stopPropagation(); + const input = document.getElementById( + `page-angle-${pageIndex}` + ) as HTMLInputElement; + const current = parseInt(input.value) || 0; + input.value = (current - 1).toString(); + }; - const angleInput = document.createElement('input'); - angleInput.type = 'number'; - angleInput.id = `page-angle-${pageIndex}`; - angleInput.value = pageState.rotations[pageIndex]?.toString() || '0'; - angleInput.className = 'w-12 h-8 text-center bg-gray-700 border border-gray-600 text-white rounded text-xs'; + const angleInput = document.createElement('input'); + angleInput.type = 'number'; + angleInput.id = `page-angle-${pageIndex}`; + angleInput.value = pageState.rotations[pageIndex]?.toString() || '0'; + angleInput.className = + 'w-12 h-8 text-center bg-gray-700 border border-gray-600 text-white rounded text-xs'; - const incrementBtn = document.createElement('button'); - incrementBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-sm'; - incrementBtn.textContent = '+'; - incrementBtn.onclick = function (e) { - e.stopPropagation(); - const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement; - const current = parseInt(input.value) || 0; - input.value = (current + 1).toString(); - }; + const incrementBtn = document.createElement('button'); + incrementBtn.className = + 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-sm'; + incrementBtn.textContent = '+'; + incrementBtn.onclick = function (e) { + e.stopPropagation(); + const input = document.getElementById( + `page-angle-${pageIndex}` + ) as HTMLInputElement; + const current = parseInt(input.value) || 0; + input.value = (current + 1).toString(); + }; - const applyBtn = document.createElement('button'); - applyBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600'; - applyBtn.innerHTML = ''; - applyBtn.onclick = function (e) { - e.stopPropagation(); - const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement; - const angle = parseInt(input.value) || 0; - pageState.rotations[pageIndex] = angle; - const wrapper = container.querySelector('.thumbnail-wrapper') as HTMLElement; - if (wrapper) wrapper.style.transform = `rotate(${-angle}deg)`; - }; + const applyBtn = document.createElement('button'); + applyBtn.className = + 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600'; + applyBtn.innerHTML = ''; + applyBtn.onclick = function (e) { + e.stopPropagation(); + const input = document.getElementById( + `page-angle-${pageIndex}` + ) as HTMLInputElement; + const angle = parseInt(input.value) || 0; + pageState.rotations[pageIndex] = angle; + const wrapper = container.querySelector( + '.thumbnail-wrapper' + ) as HTMLElement; + if (wrapper) wrapper.style.transform = `rotate(${-angle}deg)`; + }; - controls.append(decrementBtn, angleInput, incrementBtn, applyBtn); - container.appendChild(controls); + controls.append(decrementBtn, angleInput, incrementBtn, applyBtn); + container.appendChild(controls); - // Re-create icons for the new element - setTimeout(function () { - createIcons({ icons }); - }, 0); + // Re-create icons for the new element + setTimeout(function () { + createIcons({ icons }); + }, 0); - return container; + return container; } async function renderThumbnails() { - const pageThumbnails = document.getElementById('page-thumbnails'); - if (!pageThumbnails || !pageState.pdfJsDoc) return; + const pageThumbnails = document.getElementById('page-thumbnails'); + if (!pageThumbnails || !pageState.pdfJsDoc) return; - pageThumbnails.innerHTML = ''; + pageThumbnails.innerHTML = ''; - await renderPagesProgressively( - pageState.pdfJsDoc, - pageThumbnails, - createPageWrapper, - { - batchSize: 8, - useLazyLoading: true, - lazyLoadMargin: '200px', - eagerLoadBatches: 2, - onBatchComplete: function () { - createIcons({ icons }); - } - } - ); + await renderPagesProgressively( + pageState.pdfJsDoc, + pageThumbnails, + createPageWrapper, + { + batchSize: 8, + useLazyLoading: true, + lazyLoadMargin: '200px', + eagerLoadBatches: 2, + onBatchComplete: function () { + createIcons({ icons }); + }, + } + ); - createIcons({ icons }); + createIcons({ icons }); } 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'); - 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.file) { + 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 = pageState.file.name; - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`; + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`; - 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 () { + resetState(); + }; - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); - try { - showLoader('Loading PDF...'); - const arrayBuffer = await pageState.file.arrayBuffer(); + try { + const result = await loadPdfWithPasswordPrompt(pageState.file); + if (!result) { + resetState(); + return; + } + showLoader('Loading PDF...'); - pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer.slice(0), { - ignoreEncryption: true, - throwOnInvalidObject: false - }); + pageState.pdfDoc = await PDFLibDocument.load(result.bytes, { + throwOnInvalidObject: false, + }); - pageState.pdfJsDoc = await getPDFDocument({ data: arrayBuffer.slice(0) }).promise; + pageState.pdfJsDoc = result.pdf; - const pageCount = pageState.pdfDoc.getPageCount(); - pageState.rotations = new Array(pageCount).fill(0); + const pageCount = pageState.pdfDoc.getPageCount(); + pageState.rotations = new Array(pageCount).fill(0); - metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`; + metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`; - await renderThumbnails(); - hideLoader(); + await renderThumbnails(); + hideLoader(); - if (toolOptions) toolOptions.classList.remove('hidden'); - } catch (error) { - console.error('Error loading PDF:', error); - hideLoader(); - showAlert('Error', 'Failed to load PDF file.'); - resetState(); - } - } else { - if (toolOptions) toolOptions.classList.add('hidden'); + if (toolOptions) toolOptions.classList.remove('hidden'); + } catch (error) { + console.error('Error loading PDF:', error); + hideLoader(); + showAlert('Error', 'Failed to load PDF file.'); + resetState(); } + } else { + if (toolOptions) toolOptions.classList.add('hidden'); + } } async function applyRotations() { - if (!pageState.pdfDoc || !pageState.file) { - showAlert('Error', 'Please upload a PDF first.'); - return; - } + if (!pageState.pdfDoc || !pageState.file) { + showAlert('Error', 'Please upload a PDF first.'); + return; + } - showLoader('Applying rotations...'); + showLoader('Applying rotations...'); - try { - const pageCount = pageState.pdfDoc.getPageCount(); - const newPdfDoc = await PDFLibDocument.create(); + try { + const pageCount = pageState.pdfDoc.getPageCount(); + const newPdfDoc = await PDFLibDocument.create(); - for (let i = 0; i < pageCount; i++) { - const rotation = pageState.rotations[i] || 0; - const originalPage = pageState.pdfDoc.getPage(i); - const currentRotation = originalPage.getRotation().angle; - const totalRotation = currentRotation + rotation; + for (let i = 0; i < pageCount; i++) { + const rotation = pageState.rotations[i] || 0; + const originalPage = pageState.pdfDoc.getPage(i); + const currentRotation = originalPage.getRotation().angle; + const totalRotation = currentRotation + rotation; - console.log(`Page ${i}: rotation=${rotation}, currentRotation=${currentRotation}, totalRotation=${totalRotation}, applying=${-totalRotation}`); + console.log( + `Page ${i}: rotation=${rotation}, currentRotation=${currentRotation}, totalRotation=${totalRotation}, applying=${-totalRotation}` + ); - if (totalRotation % 90 === 0) { - const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]); - copiedPage.setRotation(degrees(totalRotation)); - newPdfDoc.addPage(copiedPage); - } else { - const embeddedPage = await newPdfDoc.embedPage(originalPage); - const { width, height } = embeddedPage.scale(1); + if (totalRotation % 90 === 0) { + const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]); + copiedPage.setRotation(degrees(totalRotation)); + newPdfDoc.addPage(copiedPage); + } else { + const embeddedPage = await newPdfDoc.embedPage(originalPage); + const { width, height } = embeddedPage.scale(1); - const angleRad = (totalRotation * Math.PI) / 180; - const absCos = Math.abs(Math.cos(angleRad)); - const absSin = Math.abs(Math.sin(angleRad)); + const angleRad = (totalRotation * Math.PI) / 180; + const absCos = Math.abs(Math.cos(angleRad)); + const absSin = Math.abs(Math.sin(angleRad)); - const newWidth = width * absCos + height * absSin; - const newHeight = width * absSin + height * absCos; + const newWidth = width * absCos + height * absSin; + const newHeight = width * absSin + height * absCos; - const newPage = newPdfDoc.addPage([newWidth, newHeight]); + const newPage = newPdfDoc.addPage([newWidth, newHeight]); - const x = newWidth / 2 - (width / 2 * Math.cos(angleRad) - height / 2 * Math.sin(angleRad)); - const y = newHeight / 2 - (width / 2 * Math.sin(angleRad) + height / 2 * Math.cos(angleRad)); + const x = + newWidth / 2 - + ((width / 2) * Math.cos(angleRad) - + (height / 2) * Math.sin(angleRad)); + const y = + newHeight / 2 - + ((width / 2) * Math.sin(angleRad) + + (height / 2) * Math.cos(angleRad)); - newPage.drawPage(embeddedPage, { - x, - y, - width, - height, - rotate: degrees(totalRotation), - }); - } - } - - const rotatedPdfBytes = await newPdfDoc.save(); - const originalName = pageState.file.name.replace(/\.pdf$/i, ''); - - downloadFile( - new Blob([new Uint8Array(rotatedPdfBytes)], { type: 'application/pdf' }), - `${originalName}_rotated.pdf` - ); - - showAlert('Success', 'Rotations applied successfully!', 'success', function () { - resetState(); + newPage.drawPage(embeddedPage, { + x, + y, + width, + height, + rotate: degrees(totalRotation), }); - } catch (e) { - console.error(e); - showAlert('Error', 'Could not apply rotations.'); - } finally { - hideLoader(); + } } + + const rotatedPdfBytes = await newPdfDoc.save(); + const originalName = pageState.file.name.replace(/\.pdf$/i, ''); + + downloadFile( + new Blob([new Uint8Array(rotatedPdfBytes)], { type: 'application/pdf' }), + `${originalName}_rotated.pdf` + ); + + showAlert( + 'Success', + 'Rotations applied successfully!', + 'success', + function () { + resetState(); + } + ); + } catch (e) { + console.error(e); + showAlert('Error', 'Could not apply rotations.'); + } finally { + hideLoader(); + } } 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 file = files[0]; + if ( + file.type === 'application/pdf' || + file.name.toLowerCase().endsWith('.pdf') + ) { + pageState.file = file; + updateUI(); } + } } 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 batchDecrement = document.getElementById('batch-decrement'); - const batchIncrement = document.getElementById('batch-increment'); - const batchApply = document.getElementById('batch-apply'); - const batchAngleInput = document.getElementById('batch-custom-angle') as HTMLInputElement; + 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 batchDecrement = document.getElementById('batch-decrement'); + const batchIncrement = document.getElementById('batch-increment'); + const batchApply = document.getElementById('batch-apply'); + const batchAngleInput = document.getElementById( + 'batch-custom-angle' + ) as HTMLInputElement; - 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 (batchDecrement && batchAngleInput) { + batchDecrement.addEventListener('click', function () { + const current = parseInt(batchAngleInput.value) || 0; + batchAngleInput.value = (current - 1).toString(); + }); + } + + if (batchIncrement && batchAngleInput) { + batchIncrement.addEventListener('click', function () { + const current = parseInt(batchAngleInput.value) || 0; + batchAngleInput.value = (current + 1).toString(); + }); + } + + if (batchApply && batchAngleInput) { + batchApply.addEventListener('click', function () { + const angle = parseInt(batchAngleInput.value) || 0; + for (let i = 0; i < pageState.rotations.length; i++) { + pageState.rotations[i] = angle; + } + updateAllRotationDisplays(); + }); + } + + 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('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); + } + } + }); - if (batchDecrement && batchAngleInput) { - batchDecrement.addEventListener('click', function () { - const current = parseInt(batchAngleInput.value) || 0; - batchAngleInput.value = (current - 1).toString(); - }); - } + fileInput.addEventListener('click', function () { + fileInput.value = ''; + }); + } - if (batchIncrement && batchAngleInput) { - batchIncrement.addEventListener('click', function () { - const current = parseInt(batchAngleInput.value) || 0; - batchAngleInput.value = (current + 1).toString(); - }); - } - - if (batchApply && batchAngleInput) { - batchApply.addEventListener('click', function () { - const angle = parseInt(batchAngleInput.value) || 0; - for (let i = 0; i < pageState.rotations.length; i++) { - pageState.rotations[i] = angle; - } - updateAllRotationDisplays(); - }); - } - - 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('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); - } - } - }); - - fileInput.addEventListener('click', function () { - fileInput.value = ''; - }); - } - - if (processBtn) { - processBtn.addEventListener('click', applyRotations); - } + if (processBtn) { + processBtn.addEventListener('click', applyRotations); + } }); diff --git a/src/js/logic/rotate-pdf-page.ts b/src/js/logic/rotate-pdf-page.ts index 1c347b7..3d3d43a 100644 --- a/src/js/logic/rotate-pdf-page.ts +++ b/src/js/logic/rotate-pdf-page.ts @@ -1,5 +1,5 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js'; +import { downloadFile, formatBytes } from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { @@ -7,6 +7,7 @@ import { cleanupLazyRendering, } from '../utils/render-utils.js'; import { rotatePdfPages } from '../utils/pdf-operations.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import * as pdfjsLib from 'pdfjs-dist'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( @@ -199,16 +200,18 @@ async function updateUI() { createIcons({ icons }); try { + const result = await loadPdfWithPasswordPrompt(pageState.file); + if (!result) { + resetState(); + return; + } showLoader('Loading PDF...'); - const arrayBuffer = await pageState.file.arrayBuffer(); - pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer.slice(0), { - ignoreEncryption: true, + pageState.pdfDoc = await PDFLibDocument.load(result.bytes, { throwOnInvalidObject: false, }); - pageState.pdfJsDoc = await getPDFDocument({ data: arrayBuffer.slice(0) }) - .promise; + pageState.pdfJsDoc = result.pdf; const pageCount = pageState.pdfDoc.getPageCount(); pageState.rotations = new Array(pageCount).fill(0); diff --git a/src/js/logic/sanitize-pdf-page.ts b/src/js/logic/sanitize-pdf-page.ts index 76ea679..d7ae9fb 100644 --- a/src/js/logic/sanitize-pdf-page.ts +++ b/src/js/logic/sanitize-pdf-page.ts @@ -3,6 +3,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js'; import { icons, createIcons } from 'lucide'; import { SanitizePdfState } from '@/types'; import { sanitizePdf } from '../utils/sanitize.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; const pageState: SanitizePdfState = { file: null, @@ -132,8 +133,17 @@ async function runSanitize() { return; } - const arrayBuffer = await pageState.file.arrayBuffer(); - const result = await sanitizePdf(new Uint8Array(arrayBuffer), options); + if (loaderModal) loaderModal.classList.add('hidden'); + const loaded = await loadPdfWithPasswordPrompt(pageState.file); + if (!loaded) { + if (loaderModal) loaderModal.classList.add('hidden'); + return; + } + if (loaderModal) loaderModal.classList.remove('hidden'); + if (loaderText) loaderText.textContent = 'Sanitizing PDF...'; + loaded.pdf.destroy(); + pageState.file = loaded.file; + const result = await sanitizePdf(new Uint8Array(loaded.bytes), options); downloadFile( new Blob([new Uint8Array(result.bytes)], { type: 'application/pdf' }), @@ -147,9 +157,10 @@ async function runSanitize() { resetState(); } ); - } catch (e: any) { + } catch (e: unknown) { console.error('Sanitization Error:', e); - showAlert('Error', `An error occurred during sanitization: ${e.message}`); + const msg = e instanceof Error ? e.message : String(e); + showAlert('Error', `An error occurred during sanitization: ${msg}`); } finally { if (loaderModal) loaderModal.classList.add('hidden'); } diff --git a/src/js/logic/scanner-effect-page.ts b/src/js/logic/scanner-effect-page.ts index bae63b6..5f995fe 100644 --- a/src/js/logic/scanner-effect-page.ts +++ b/src/js/logic/scanner-effect-page.ts @@ -11,6 +11,7 @@ import { applyScannerEffect } from '../utils/image-effects.js'; import * as pdfjsLib from 'pdfjs-dist'; import type { ScanSettings } from '../types/scanner-effect-type.js'; import { t } from '../i18n/i18n'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -402,13 +403,13 @@ document.addEventListener('DOMContentLoaded', () => { return; } - files = [validFiles[0]]; - updateUI(); - - showLoader('Loading preview...'); try { - const buffer = await readFileAsArrayBuffer(validFiles[0]); - pdfjsDoc = await getPDFDocument({ data: buffer }).promise; + const result = await loadPdfWithPasswordPrompt(validFiles[0]); + if (!result) return; + showLoader('Loading preview...'); + files = [result.file]; + updateUI(); + pdfjsDoc = result.pdf; await renderPreview(); } catch (e) { console.error(e); diff --git a/src/js/logic/sign-pdf-page.ts b/src/js/logic/sign-pdf-page.ts index 633e8fd..8f9415a 100644 --- a/src/js/logic/sign-pdf-page.ts +++ b/src/js/logic/sign-pdf-page.ts @@ -1,313 +1,358 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; -import { readFileAsArrayBuffer, formatBytes, downloadFile, getPDFDocument } from '../utils/helpers.js'; +import { + readFileAsArrayBuffer, + formatBytes, + downloadFile, +} from '../utils/helpers.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { PDFDocument } from 'pdf-lib'; import { t } from '../i18n/i18n'; interface SignState { - file: File | null; - pdfDoc: any; - viewerIframe: HTMLIFrameElement | null; - viewerReady: boolean; - blobUrl: string | null; + file: File | null; + pdfDoc: any; + viewerIframe: HTMLIFrameElement | null; + viewerReady: boolean; + blobUrl: string | null; } const signState: SignState = { - file: null, - pdfDoc: null, - viewerIframe: null, - viewerReady: false, - blobUrl: null, + file: null, + pdfDoc: null, + viewerIframe: null, + viewerReady: false, + blobUrl: null, }; if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializePage); + document.addEventListener('DOMContentLoaded', initializePage); } else { - initializePage(); + initializePage(); } function initializePage() { - createIcons({ icons }); + createIcons({ icons }); - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const processBtn = document.getElementById('process-btn'); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const processBtn = document.getElementById('process-btn'); - if (fileInput) { - fileInput.addEventListener('change', handleFileUpload); - } + if (fileInput) { + fileInput.addEventListener('change', handleFileUpload); + } - if (dropZone) { - dropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - dropZone.classList.add('bg-gray-700'); - }); - - dropZone.addEventListener('dragleave', () => { - dropZone.classList.remove('bg-gray-700'); - }); - - dropZone.addEventListener('drop', (e) => { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - const droppedFiles = e.dataTransfer?.files; - if (droppedFiles && droppedFiles.length > 0) { - handleFile(droppedFiles[0]); - } - }); - - // Clear value on click to allow re-selecting the same file - fileInput?.addEventListener('click', () => { - if (fileInput) fileInput.value = ''; - }); - } - - if (processBtn) { - processBtn.addEventListener('click', applyAndSaveSignatures); - } - - document.getElementById('back-to-tools')?.addEventListener('click', () => { - cleanup(); - window.location.href = import.meta.env.BASE_URL; + if (dropZone) { + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); }); + + dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('bg-gray-700'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + const droppedFiles = e.dataTransfer?.files; + if (droppedFiles && droppedFiles.length > 0) { + handleFile(droppedFiles[0]); + } + }); + + // Clear value on click to allow re-selecting the same file + fileInput?.addEventListener('click', () => { + if (fileInput) fileInput.value = ''; + }); + } + + if (processBtn) { + processBtn.addEventListener('click', applyAndSaveSignatures); + } + + document.getElementById('back-to-tools')?.addEventListener('click', () => { + cleanup(); + window.location.href = import.meta.env.BASE_URL; + }); } function handleFileUpload(e: Event) { - const input = e.target as HTMLInputElement; - if (input.files && input.files.length > 0) { - handleFile(input.files[0]); - } + const input = e.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + handleFile(input.files[0]); + } } function handleFile(file: File) { - if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) { - showAlert('Invalid File', 'Please select a PDF file.'); - return; - } + if ( + file.type !== 'application/pdf' && + !file.name.toLowerCase().endsWith('.pdf') + ) { + showAlert('Invalid File', 'Please select a PDF file.'); + return; + } - signState.file = file; - updateFileDisplay(); - setupSignTool(); + signState.file = file; + updateFileDisplay(); + setupSignTool(); } async function updateFileDisplay() { - const fileDisplayArea = document.getElementById('file-display-area'); + const fileDisplayArea = document.getElementById('file-display-area'); - if (!fileDisplayArea || !signState.file) return; + if (!fileDisplayArea || !signState.file) return; + fileDisplayArea.innerHTML = ''; + + const fileDiv = document.createElement('div'); + fileDiv.className = + 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; + + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col flex-1 min-w-0'; + + const nameSpan = document.createElement('div'); + nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; + nameSpan.textContent = signState.file.name; + + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = `${formatBytes(signState.file.size)} • ${t('common.loadingPageCount')}`; + + 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 = () => { + signState.file = null; + signState.pdfDoc = null; fileDisplayArea.innerHTML = ''; + document.getElementById('signature-editor')?.classList.add('hidden'); + }; - const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); - const infoContainer = document.createElement('div'); - infoContainer.className = 'flex flex-col flex-1 min-w-0'; - - const nameSpan = document.createElement('div'); - nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; - nameSpan.textContent = signState.file.name; - - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(signState.file.size)} • ${t('common.loadingPageCount')}`; - - 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 = () => { - signState.file = null; - signState.pdfDoc = null; - fileDisplayArea.innerHTML = ''; - document.getElementById('signature-editor')?.classList.add('hidden'); - }; - - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); - - // Load page count - try { - const arrayBuffer = await readFileAsArrayBuffer(signState.file); - const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise; - metaSpan.textContent = `${formatBytes(signState.file.size)} • ${pdfDoc.numPages} pages`; - } catch (error) { - console.error('Error loading PDF:', error); - } + const result = await loadPdfWithPasswordPrompt(signState.file); + if (!result) { + signState.file = null; + signState.pdfDoc = null; + fileDisplayArea.innerHTML = ''; + document.getElementById('signature-editor')?.classList.add('hidden'); + return; + } + signState.file = result.file; + nameSpan.textContent = result.file.name; + metaSpan.textContent = `${formatBytes(result.file.size)} • ${result.pdf.numPages} pages`; + result.pdf.destroy(); } async function setupSignTool() { - const signatureEditor = document.getElementById('signature-editor'); - if (signatureEditor) { - signatureEditor.classList.remove('hidden'); - } + const signatureEditor = document.getElementById('signature-editor'); + if (signatureEditor) { + signatureEditor.classList.remove('hidden'); + } - showLoader('Loading PDF viewer...'); + showLoader('Loading PDF viewer...'); - const container = document.getElementById('canvas-container-sign'); - if (!container) { - console.error('Sign tool canvas container not found'); - hideLoader(); - return; - } + const container = document.getElementById('canvas-container-sign'); + if (!container) { + console.error('Sign tool canvas container not found'); + hideLoader(); + return; + } - if (!signState.file) { - console.error('No file loaded for signing'); - hideLoader(); - return; - } + if (!signState.file) { + console.error('No file loaded for signing'); + hideLoader(); + return; + } - container.textContent = ''; - const iframe = document.createElement('iframe'); - iframe.style.width = '100%'; - iframe.style.height = '100%'; - iframe.style.border = 'none'; - container.appendChild(iframe); - signState.viewerIframe = iframe; + container.textContent = ''; + const iframe = document.createElement('iframe'); + iframe.style.width = '100%'; + iframe.style.height = '100%'; + iframe.style.border = 'none'; + container.appendChild(iframe); + signState.viewerIframe = iframe; - const pdfBytes = await readFileAsArrayBuffer(signState.file); - const blob = new Blob([pdfBytes as BlobPart], { type: 'application/pdf' }); - signState.blobUrl = URL.createObjectURL(blob); + const pdfBytes = await readFileAsArrayBuffer(signState.file); + const blob = new Blob([pdfBytes as BlobPart], { type: 'application/pdf' }); + signState.blobUrl = URL.createObjectURL(blob); - try { - const existingPrefsRaw = localStorage.getItem('pdfjs.preferences'); - const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {}; - delete (existingPrefs as any).annotationEditorMode; - const newPrefs = { - ...existingPrefs, - enableSignatureEditor: true, - enablePermissions: false, - }; - localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs)); - } catch { } - - const viewerUrl = new URL(`${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html`, window.location.origin); - const query = new URLSearchParams({ file: signState.blobUrl }); - iframe.src = `${viewerUrl.toString()}?${query.toString()}`; - - iframe.onload = () => { - hideLoader(); - signState.viewerReady = true; - try { - const viewerWindow: any = iframe.contentWindow; - if (viewerWindow && viewerWindow.PDFViewerApplication) { - const app = viewerWindow.PDFViewerApplication; - const doc = viewerWindow.document; - const eventBus = app.eventBus; - eventBus?._on('annotationeditoruimanager', () => { - const editorModeButtons = doc.getElementById('editorModeButtons'); - editorModeButtons?.classList.remove('hidden'); - const editorSignature = doc.getElementById('editorSignature'); - editorSignature?.removeAttribute('hidden'); - const editorSignatureButton = doc.getElementById('editorSignatureButton') as HTMLButtonElement | null; - if (editorSignatureButton) { - editorSignatureButton.disabled = false; - } - const editorStamp = doc.getElementById('editorStamp'); - editorStamp?.removeAttribute('hidden'); - const editorStampButton = doc.getElementById('editorStampButton') as HTMLButtonElement | null; - if (editorStampButton) { - editorStampButton.disabled = false; - } - try { - const highlightBtn = doc.getElementById('editorHighlightButton') as HTMLButtonElement | null; - highlightBtn?.click(); - } catch { } - }); - } - } catch (e) { - console.error('Could not initialize PDF.js viewer for signing:', e); - } - - const saveBtn = document.getElementById('process-btn') as HTMLButtonElement | null; - if (saveBtn) { - saveBtn.style.display = ''; - } + try { + const existingPrefsRaw = localStorage.getItem('pdfjs.preferences'); + const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {}; + delete (existingPrefs as any).annotationEditorMode; + const newPrefs = { + ...existingPrefs, + enableSignatureEditor: true, + enablePermissions: false, }; + localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs)); + } catch {} + + const viewerUrl = new URL( + `${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html`, + window.location.origin + ); + const query = new URLSearchParams({ file: signState.blobUrl }); + iframe.src = `${viewerUrl.toString()}?${query.toString()}`; + + iframe.onload = () => { + hideLoader(); + signState.viewerReady = true; + try { + const viewerWindow: any = iframe.contentWindow; + if (viewerWindow && viewerWindow.PDFViewerApplication) { + const app = viewerWindow.PDFViewerApplication; + const doc = viewerWindow.document; + const eventBus = app.eventBus; + eventBus?._on('annotationeditoruimanager', () => { + const editorModeButtons = doc.getElementById('editorModeButtons'); + editorModeButtons?.classList.remove('hidden'); + const editorSignature = doc.getElementById('editorSignature'); + editorSignature?.removeAttribute('hidden'); + const editorSignatureButton = doc.getElementById( + 'editorSignatureButton' + ) as HTMLButtonElement | null; + if (editorSignatureButton) { + editorSignatureButton.disabled = false; + } + const editorStamp = doc.getElementById('editorStamp'); + editorStamp?.removeAttribute('hidden'); + const editorStampButton = doc.getElementById( + 'editorStampButton' + ) as HTMLButtonElement | null; + if (editorStampButton) { + editorStampButton.disabled = false; + } + try { + const highlightBtn = doc.getElementById( + 'editorHighlightButton' + ) as HTMLButtonElement | null; + highlightBtn?.click(); + } catch {} + }); + } + } catch (e) { + console.error('Could not initialize PDF.js viewer for signing:', e); + } + + const saveBtn = document.getElementById( + 'process-btn' + ) as HTMLButtonElement | null; + if (saveBtn) { + saveBtn.style.display = ''; + } + }; } async function applyAndSaveSignatures() { - if (!signState.viewerReady || !signState.viewerIframe) { - showAlert('Viewer not ready', 'Please wait for the PDF viewer to load.'); - return; + if (!signState.viewerReady || !signState.viewerIframe) { + showAlert('Viewer not ready', 'Please wait for the PDF viewer to load.'); + return; + } + + try { + const viewerWindow: any = signState.viewerIframe.contentWindow; + if (!viewerWindow || !viewerWindow.PDFViewerApplication) { + showAlert('Viewer not ready', 'The PDF viewer is still initializing.'); + return; } - try { - const viewerWindow: any = signState.viewerIframe.contentWindow; - if (!viewerWindow || !viewerWindow.PDFViewerApplication) { - showAlert('Viewer not ready', 'The PDF viewer is still initializing.'); - return; + const app = viewerWindow.PDFViewerApplication; + const flattenCheckbox = document.getElementById( + 'flatten-signature-toggle' + ) as HTMLInputElement | null; + const shouldFlatten = flattenCheckbox?.checked; + + if (shouldFlatten) { + showLoader('Flattening and saving PDF...'); + + const rawPdfBytes = await app.pdfDocument.saveDocument( + app.pdfDocument.annotationStorage + ); + const pdfBytes = new Uint8Array(rawPdfBytes); + const pdfDoc = await PDFDocument.load(pdfBytes); + pdfDoc.getForm().flatten(); + const flattenedPdfBytes = await pdfDoc.save(); + + const blob = new Blob([flattenedPdfBytes as BlobPart], { + type: 'application/pdf', + }); + downloadFile( + blob, + `signed_flattened_${signState.file?.name || 'document.pdf'}` + ); + + hideLoader(); + showAlert('Success', 'Signed PDF saved successfully!', 'success', () => { + resetState(); + }); + } else { + app.eventBus?.dispatch('download', { source: app }); + showAlert( + 'Success', + 'Signed PDF downloaded successfully!', + 'success', + () => { + resetState(); } - - const app = viewerWindow.PDFViewerApplication; - const flattenCheckbox = document.getElementById('flatten-signature-toggle') as HTMLInputElement | null; - const shouldFlatten = flattenCheckbox?.checked; - - if (shouldFlatten) { - showLoader('Flattening and saving PDF...'); - - const rawPdfBytes = await app.pdfDocument.saveDocument(app.pdfDocument.annotationStorage); - const pdfBytes = new Uint8Array(rawPdfBytes); - const pdfDoc = await PDFDocument.load(pdfBytes); - pdfDoc.getForm().flatten(); - const flattenedPdfBytes = await pdfDoc.save(); - - const blob = new Blob([flattenedPdfBytes as BlobPart], { type: 'application/pdf' }); - downloadFile(blob, `signed_flattened_${signState.file?.name || 'document.pdf'}`); - - hideLoader(); - showAlert('Success', 'Signed PDF saved successfully!', 'success', () => { - resetState(); - }); - } else { - app.eventBus?.dispatch('download', { source: app }); - showAlert('Success', 'Signed PDF downloaded successfully!', 'success', () => { - resetState(); - }); - } - } catch (error) { - console.error('Failed to export the signed PDF:', error); - hideLoader(); - showAlert('Export failed', 'Could not export the signed PDF. Please try again.'); + ); } + } catch (error) { + console.error('Failed to export the signed PDF:', error); + hideLoader(); + showAlert( + 'Export failed', + 'Could not export the signed PDF. Please try again.' + ); + } } function resetState() { - cleanup(); - signState.file = null; - signState.viewerIframe = null; - signState.viewerReady = false; + cleanup(); + signState.file = null; + signState.viewerIframe = null; + signState.viewerReady = false; - const signatureEditor = document.getElementById('signature-editor'); - if (signatureEditor) { - signatureEditor.classList.add('hidden'); - } + const signatureEditor = document.getElementById('signature-editor'); + if (signatureEditor) { + signatureEditor.classList.add('hidden'); + } - const container = document.getElementById('canvas-container-sign'); - if (container) { - container.textContent = ''; - } + const container = document.getElementById('canvas-container-sign'); + if (container) { + container.textContent = ''; + } - const fileDisplayArea = document.getElementById('file-display-area'); - if (fileDisplayArea) { - fileDisplayArea.innerHTML = ''; - } + const fileDisplayArea = document.getElementById('file-display-area'); + if (fileDisplayArea) { + fileDisplayArea.innerHTML = ''; + } - const processBtn = document.getElementById('process-btn') as HTMLButtonElement | null; - if (processBtn) { - processBtn.style.display = 'none'; - } + const processBtn = document.getElementById( + 'process-btn' + ) as HTMLButtonElement | null; + if (processBtn) { + processBtn.style.display = 'none'; + } - const flattenCheckbox = document.getElementById('flatten-signature-toggle') as HTMLInputElement | null; - if (flattenCheckbox) { - flattenCheckbox.checked = false; - } + const flattenCheckbox = document.getElementById( + 'flatten-signature-toggle' + ) as HTMLInputElement | null; + if (flattenCheckbox) { + flattenCheckbox.checked = false; + } } function cleanup() { - if (signState.blobUrl) { - URL.revokeObjectURL(signState.blobUrl); - signState.blobUrl = null; - } + if (signState.blobUrl) { + URL.revokeObjectURL(signState.blobUrl); + signState.blobUrl = null; + } } diff --git a/src/js/logic/split-pdf-page.ts b/src/js/logic/split-pdf-page.ts index a130815..d344a27 100644 --- a/src/js/logic/split-pdf-page.ts +++ b/src/js/logic/split-pdf-page.ts @@ -2,12 +2,8 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { t } from '../i18n/i18n'; import { createIcons, icons } from 'lucide'; import * as pdfjsLib from 'pdfjs-dist'; -import { - downloadFile, - getPDFDocument, - readFileAsArrayBuffer, - formatBytes, -} from '../utils/helpers.js'; +import { downloadFile, getPDFDocument, formatBytes } from '../utils/helpers.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { state } from '../state.js'; import { renderPagesProgressively, @@ -94,12 +90,15 @@ document.addEventListener('DOMContentLoaded', () => { // Load PDF Document try { if (!state.pdfDoc) { - showLoader('Loading PDF...'); - const arrayBuffer = (await readFileAsArrayBuffer( - file - )) as ArrayBuffer; - state.pdfDoc = await PDFLibDocument.load(arrayBuffer); - hideLoader(); + const result = await loadPdfWithPasswordPrompt(file); + if (!result) { + state.files = []; + updateUI(); + return; + } + result.pdf.destroy(); + state.files[0] = result.file; + state.pdfDoc = await PDFLibDocument.load(result.bytes); } // Update page count metaSpan.textContent = `${formatBytes(file.size)} • ${state.pdfDoc.getPageCount()} pages`; @@ -139,10 +138,16 @@ document.addEventListener('DOMContentLoaded', () => { // If pdfDoc is not loaded yet (e.g. page refresh), try to load it from the first file if (state.files.length > 0) { const file = state.files[0]; - const arrayBuffer = (await readFileAsArrayBuffer( - file - )) as ArrayBuffer; - state.pdfDoc = await PDFLibDocument.load(arrayBuffer); + hideLoader(); + const result = await loadPdfWithPasswordPrompt(file); + if (!result) { + showLoader('Rendering page previews...'); + throw new Error('No PDF document loaded'); + } + result.pdf.destroy(); + state.files[0] = result.file; + state.pdfDoc = await PDFLibDocument.load(result.bytes); + showLoader('Rendering page previews...'); } else { throw new Error('No PDF document loaded'); } diff --git a/src/js/logic/table-of-contents.ts b/src/js/logic/table-of-contents.ts index 1aa46fa..eb4b4b5 100644 --- a/src/js/logic/table-of-contents.ts +++ b/src/js/logic/table-of-contents.ts @@ -5,6 +5,7 @@ import { showWasmRequiredDialog, WasmProvider, } from '../utils/wasm-provider.js'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; const worker = new Worker( import.meta.env.BASE_URL + 'workers/table-of-contents.worker.js' @@ -95,15 +96,18 @@ function renderFileDisplay(file: File) { fileDisplayArea.appendChild(fileDiv); } -function handleFileSelect(file: File) { +async function handleFileSelect(file: File) { if (file.type !== 'application/pdf') { showStatus('Please select a PDF file.', 'error'); return; } - pdfFile = file; + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; + result.pdf.destroy(); + pdfFile = result.file; generateBtn.disabled = false; - renderFileDisplay(file); + renderFileDisplay(pdfFile); } dropZone.addEventListener('dragover', (e) => { diff --git a/src/js/logic/text-color-page.ts b/src/js/logic/text-color-page.ts index a471ca7..e889190 100644 --- a/src/js/logic/text-color-page.ts +++ b/src/js/logic/text-color-page.ts @@ -1,135 +1,199 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; -import { downloadFile, hexToRgb, formatBytes, getPDFDocument, readFileAsArrayBuffer } from '../utils/helpers.js'; +import { + downloadFile, + hexToRgb, + formatBytes, + getPDFDocument, + readFileAsArrayBuffer, +} from '../utils/helpers.js'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import * as pdfjsLib from 'pdfjs-dist'; import { TextColorState } from '@/types'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; -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(); const pageState: TextColorState = { file: null, pdfDoc: null }; -if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializePage); } -else { initializePage(); } - -function initializePage() { - createIcons({ icons }); - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const backBtn = document.getElementById('back-to-tools'); - const processBtn = document.getElementById('process-btn'); - - if (fileInput) { - fileInput.addEventListener('change', handleFileUpload); - fileInput.addEventListener('click', () => { fileInput.value = ''; }); - } - if (dropZone) { - dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-indigo-500'); }); - dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('border-indigo-500'); }); - dropZone.addEventListener('drop', (e) => { - e.preventDefault(); dropZone.classList.remove('border-indigo-500'); - if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files); - }); - } - if (backBtn) backBtn.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; }); - if (processBtn) processBtn.addEventListener('click', changeTextColor); +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializePage); +} else { + initializePage(); } -function handleFileUpload(e: Event) { const input = e.target as HTMLInputElement; if (input.files?.length) handleFiles(input.files); } +function initializePage() { + createIcons({ icons }); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const backBtn = document.getElementById('back-to-tools'); + const processBtn = document.getElementById('process-btn'); + + if (fileInput) { + fileInput.addEventListener('change', handleFileUpload); + fileInput.addEventListener('click', () => { + fileInput.value = ''; + }); + } + if (dropZone) { + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('border-indigo-500'); + }); + dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('border-indigo-500'); + }); + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('border-indigo-500'); + if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files); + }); + } + if (backBtn) + backBtn.addEventListener('click', () => { + window.location.href = import.meta.env.BASE_URL; + }); + if (processBtn) processBtn.addEventListener('click', changeTextColor); +} + +function handleFileUpload(e: Event) { + const input = e.target as HTMLInputElement; + if (input.files?.length) handleFiles(input.files); +} async function handleFiles(files: FileList) { - const file = files[0]; - if (!file || file.type !== 'application/pdf') { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; } + const file = files[0]; + if (!file || file.type !== 'application/pdf') { + showAlert('Invalid File', 'Please upload a valid PDF file.'); + return; + } + try { + const result = await loadPdfWithPasswordPrompt(file); + if (!result) return; showLoader('Loading PDF...'); - try { - const arrayBuffer = await file.arrayBuffer(); - pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer); - pageState.file = file; - updateFileDisplay(); - document.getElementById('options-panel')?.classList.remove('hidden'); - } catch (error) { console.error(error); showAlert('Error', 'Failed to load PDF file.'); } - finally { hideLoader(); } + result.pdf.destroy(); + pageState.pdfDoc = await PDFLibDocument.load(result.bytes); + pageState.file = result.file; + updateFileDisplay(); + document.getElementById('options-panel')?.classList.remove('hidden'); + } catch (error) { + console.error(error); + showAlert('Error', 'Failed to load PDF file.'); + } finally { + hideLoader(); + } } function updateFileDisplay() { - const fileDisplayArea = document.getElementById('file-display-area'); - if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return; - fileDisplayArea.innerHTML = ''; - const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; - const infoContainer = document.createElement('div'); - infoContainer.className = 'flex flex-col flex-1 min-w-0'; - const nameSpan = document.createElement('div'); - nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; - nameSpan.textContent = pageState.file.name; - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`; - 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 = resetState; - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return; + fileDisplayArea.innerHTML = ''; + const fileDiv = document.createElement('div'); + fileDiv.className = + 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col flex-1 min-w-0'; + const nameSpan = document.createElement('div'); + nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; + nameSpan.textContent = pageState.file.name; + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`; + 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 = resetState; + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); } function resetState() { - pageState.file = null; pageState.pdfDoc = null; - const fileDisplayArea = document.getElementById('file-display-area'); - if (fileDisplayArea) fileDisplayArea.innerHTML = ''; - document.getElementById('options-panel')?.classList.add('hidden'); - const fileInput = document.getElementById('file-input') as HTMLInputElement; - if (fileInput) fileInput.value = ''; + pageState.file = null; + pageState.pdfDoc = null; + const fileDisplayArea = document.getElementById('file-display-area'); + if (fileDisplayArea) fileDisplayArea.innerHTML = ''; + document.getElementById('options-panel')?.classList.add('hidden'); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; } async function changeTextColor() { - if (!pageState.pdfDoc || !pageState.file) { showAlert('Error', 'Please upload a PDF file first.'); return; } - const colorHex = (document.getElementById('text-color-input') as HTMLInputElement).value; - const { r, g, b } = hexToRgb(colorHex); - const darknessThreshold = 120; - showLoader('Changing text color...'); - try { - const newPdfDoc = await PDFLibDocument.create(); - const pdf = await getPDFDocument(await readFileAsArrayBuffer(pageState.file)).promise; + if (!pageState.pdfDoc || !pageState.file) { + showAlert('Error', 'Please upload a PDF file first.'); + return; + } + const colorHex = ( + document.getElementById('text-color-input') as HTMLInputElement + ).value; + const { r, g, b } = hexToRgb(colorHex); + const darknessThreshold = 120; + showLoader('Changing text color...'); + try { + const newPdfDoc = await PDFLibDocument.create(); + const pdf = await getPDFDocument( + await readFileAsArrayBuffer(pageState.file) + ).promise; - for (let i = 1; i <= pdf.numPages; i++) { - showLoader(`Processing page ${i} of ${pdf.numPages}...`); - const page = await pdf.getPage(i); - const viewport = page.getViewport({ scale: 2.0 }); - const canvas = document.createElement('canvas'); - canvas.width = viewport.width; - canvas.height = viewport.height; - const context = canvas.getContext('2d')!; - await page.render({ canvasContext: context, viewport, canvas }).promise; + for (let i = 1; i <= pdf.numPages; i++) { + showLoader(`Processing page ${i} of ${pdf.numPages}...`); + const page = await pdf.getPage(i); + const viewport = page.getViewport({ scale: 2.0 }); + const canvas = document.createElement('canvas'); + canvas.width = viewport.width; + canvas.height = viewport.height; + const context = canvas.getContext('2d')!; + await page.render({ canvasContext: context, viewport, canvas }).promise; - const imageData = context.getImageData(0, 0, canvas.width, canvas.height); - const data = imageData.data; - for (let j = 0; j < data.length; j += 4) { - if (data[j] < darknessThreshold && data[j + 1] < darknessThreshold && data[j + 2] < darknessThreshold) { - data[j] = r * 255; - data[j + 1] = g * 255; - data[j + 2] = b * 255; - } - } - context.putImageData(imageData, 0, 0); - - const pngImageBytes = await new Promise((resolve) => - canvas.toBlob((blob) => { - const reader = new FileReader(); - reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer)); - reader.readAsArrayBuffer(blob!); - }, 'image/png') - ); - - const pngImage = await newPdfDoc.embedPng(pngImageBytes); - const newPage = newPdfDoc.addPage([viewport.width, viewport.height]); - newPage.drawImage(pngImage, { x: 0, y: 0, width: viewport.width, height: viewport.height }); + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + for (let j = 0; j < data.length; j += 4) { + if ( + data[j] < darknessThreshold && + data[j + 1] < darknessThreshold && + data[j + 2] < darknessThreshold + ) { + data[j] = r * 255; + data[j + 1] = g * 255; + data[j + 2] = b * 255; } - const newPdfBytes = await newPdfDoc.save(); - downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'text-color-changed.pdf'); - showAlert('Success', 'Text color changed successfully!', 'success', () => { resetState(); }); - } catch (e) { console.error(e); showAlert('Error', 'Could not change text color.'); } - finally { hideLoader(); } + } + context.putImageData(imageData, 0, 0); + + const pngImageBytes = await new Promise((resolve) => + canvas.toBlob((blob) => { + const reader = new FileReader(); + reader.onload = () => + resolve(new Uint8Array(reader.result as ArrayBuffer)); + reader.readAsArrayBuffer(blob!); + }, 'image/png') + ); + + const pngImage = await newPdfDoc.embedPng(pngImageBytes); + const newPage = newPdfDoc.addPage([viewport.width, viewport.height]); + newPage.drawImage(pngImage, { + x: 0, + y: 0, + width: viewport.width, + height: viewport.height, + }); + } + const newPdfBytes = await newPdfDoc.save(); + downloadFile( + new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), + 'text-color-changed.pdf' + ); + showAlert('Success', 'Text color changed successfully!', 'success', () => { + resetState(); + }); + } catch (e) { + console.error(e); + showAlert('Error', 'Could not change text color.'); + } finally { + hideLoader(); + } } diff --git a/src/js/logic/view-metadata-page.ts b/src/js/logic/view-metadata-page.ts index 240dcce..17999fe 100644 --- a/src/js/logic/view-metadata-page.ts +++ b/src/js/logic/view-metadata-page.ts @@ -1,360 +1,404 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { formatBytes, formatIsoDate, getPDFDocument } from '../utils/helpers.js'; +import { formatBytes, formatIsoDate } from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; import { ViewMetadataState } from '@/types'; +import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; const pageState: ViewMetadataState = { - file: null, - metadata: {}, + file: null, + metadata: {}, }; function resetState() { - pageState.file = null; - pageState.metadata = {}; + pageState.file = null; + pageState.metadata = {}; - 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 metadataDisplay = document.getElementById('metadata-display'); - if (metadataDisplay) metadataDisplay.innerHTML = ''; + const metadataDisplay = document.getElementById('metadata-display'); + if (metadataDisplay) metadataDisplay.innerHTML = ''; - const fileInput = document.getElementById('file-input') as HTMLInputElement; - if (fileInput) fileInput.value = ''; + const fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; } -function createSection(title: string): { wrapper: HTMLDivElement; ul: HTMLUListElement } { - const wrapper = document.createElement('div'); - wrapper.className = 'mb-6'; - const h3 = document.createElement('h3'); - h3.className = 'text-lg font-semibold text-white mb-2'; - h3.textContent = title; - const ul = document.createElement('ul'); - ul.className = 'space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700'; - wrapper.append(h3, ul); - return { wrapper, ul }; +function createSection(title: string): { + wrapper: HTMLDivElement; + ul: HTMLUListElement; +} { + const wrapper = document.createElement('div'); + wrapper.className = 'mb-6'; + const h3 = document.createElement('h3'); + h3.className = 'text-lg font-semibold text-white mb-2'; + h3.textContent = title; + const ul = document.createElement('ul'); + ul.className = + 'space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700'; + wrapper.append(h3, ul); + return { wrapper, ul }; } function createListItem(key: string, value: string): HTMLLIElement { - const li = document.createElement('li'); - li.className = 'flex flex-col sm:flex-row'; - const strong = document.createElement('strong'); - strong.className = 'w-40 flex-shrink-0 text-gray-400'; - strong.textContent = key; - const div = document.createElement('div'); - div.className = 'flex-grow text-white break-all'; - div.textContent = value; - li.append(strong, div); - return li; + const li = document.createElement('li'); + li.className = 'flex flex-col sm:flex-row'; + const strong = document.createElement('strong'); + strong.className = 'w-40 flex-shrink-0 text-gray-400'; + strong.textContent = key; + const div = document.createElement('div'); + div.className = 'flex-grow text-white break-all'; + div.textContent = value; + li.append(strong, div); + return li; } function parsePdfDate(pdfDate: string | unknown): string { - if (!pdfDate || typeof pdfDate !== 'string' || !pdfDate.startsWith('D:')) { - return String(pdfDate || ''); - } - try { - const year = pdfDate.substring(2, 6); - const month = pdfDate.substring(6, 8); - const day = pdfDate.substring(8, 10); - const hour = pdfDate.substring(10, 12); - const minute = pdfDate.substring(12, 14); - const second = pdfDate.substring(14, 16); - return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`).toLocaleString(); - } catch { - return pdfDate; - } + if (!pdfDate || typeof pdfDate !== 'string' || !pdfDate.startsWith('D:')) { + return String(pdfDate || ''); + } + try { + const year = pdfDate.substring(2, 6); + const month = pdfDate.substring(6, 8); + const day = pdfDate.substring(8, 10); + const hour = pdfDate.substring(10, 12); + const minute = pdfDate.substring(12, 14); + const second = pdfDate.substring(14, 16); + return new Date( + `${year}-${month}-${day}T${hour}:${minute}:${second}Z` + ).toLocaleString(); + } catch { + return pdfDate; + } } -function createXmpListItem(key: string, value: string, indent: number = 0): HTMLLIElement { - const li = document.createElement('li'); - li.className = 'flex flex-col sm:flex-row'; +function createXmpListItem( + key: string, + value: string, + indent: number = 0 +): HTMLLIElement { + const li = document.createElement('li'); + li.className = 'flex flex-col sm:flex-row'; - const strong = document.createElement('strong'); - strong.className = 'w-56 flex-shrink-0 text-gray-400'; - strong.textContent = key; - strong.style.paddingLeft = `${indent * 1.2}rem`; + const strong = document.createElement('strong'); + strong.className = 'w-56 flex-shrink-0 text-gray-400'; + strong.textContent = key; + strong.style.paddingLeft = `${indent * 1.2}rem`; - const div = document.createElement('div'); - div.className = 'flex-grow text-white break-all'; - div.textContent = value; + const div = document.createElement('div'); + div.className = 'flex-grow text-white break-all'; + div.textContent = value; - li.append(strong, div); - return li; + li.append(strong, div); + return li; } function createXmpHeaderItem(key: string, indent: number = 0): HTMLLIElement { - const li = document.createElement('li'); - li.className = 'flex pt-2'; - const strong = document.createElement('strong'); - strong.className = 'w-full flex-shrink-0 text-gray-300 font-medium'; - strong.textContent = key; - strong.style.paddingLeft = `${indent * 1.2}rem`; - li.append(strong); - return li; + const li = document.createElement('li'); + li.className = 'flex pt-2'; + const strong = document.createElement('strong'); + strong.className = 'w-full flex-shrink-0 text-gray-300 font-medium'; + strong.textContent = key; + strong.style.paddingLeft = `${indent * 1.2}rem`; + li.append(strong); + return li; } -function appendXmpNodes(xmlNode: Element, ulElement: HTMLUListElement, indentLevel: number) { - const xmpDateKeys = ['xap:CreateDate', 'xap:ModifyDate', 'xap:MetadataDate']; +function appendXmpNodes( + xmlNode: Element, + ulElement: HTMLUListElement, + indentLevel: number +) { + const xmpDateKeys = ['xap:CreateDate', 'xap:ModifyDate', 'xap:MetadataDate']; - const childNodes = Array.from(xmlNode.children); + const childNodes = Array.from(xmlNode.children); - for (const child of childNodes) { - if (child.nodeType !== 1) continue; + for (const child of childNodes) { + if (child.nodeType !== 1) continue; - let key = child.tagName; - const elementChildren = Array.from(child.children).filter(function (c) { - return c.nodeType === 1; - }); + let key = child.tagName; + const elementChildren = Array.from(child.children).filter(function (c) { + return c.nodeType === 1; + }); - if (key === 'rdf:li') { - appendXmpNodes(child, ulElement, indentLevel); - continue; - } - if (key === 'rdf:Alt') { - key = '(alt container)'; - } - - if (child.getAttribute('rdf:parseType') === 'Resource' && elementChildren.length === 0) { - ulElement.appendChild(createXmpListItem(key, '(Empty Resource)', indentLevel)); - continue; - } - - if (elementChildren.length > 0) { - ulElement.appendChild(createXmpHeaderItem(key, indentLevel)); - appendXmpNodes(child, ulElement, indentLevel + 1); - } else { - let value = (child.textContent || '').trim(); - if (value) { - if (xmpDateKeys.includes(key)) { - value = formatIsoDate(value); - } - ulElement.appendChild(createXmpListItem(key, value, indentLevel)); - } - } + if (key === 'rdf:li') { + appendXmpNodes(child, ulElement, indentLevel); + continue; } + if (key === 'rdf:Alt') { + key = '(alt container)'; + } + + if ( + child.getAttribute('rdf:parseType') === 'Resource' && + elementChildren.length === 0 + ) { + ulElement.appendChild( + createXmpListItem(key, '(Empty Resource)', indentLevel) + ); + continue; + } + + if (elementChildren.length > 0) { + ulElement.appendChild(createXmpHeaderItem(key, indentLevel)); + appendXmpNodes(child, ulElement, indentLevel + 1); + } else { + let value = (child.textContent || '').trim(); + if (value) { + if (xmpDateKeys.includes(key)) { + value = formatIsoDate(value); + } + ulElement.appendChild(createXmpListItem(key, value, indentLevel)); + } + } + } } async function displayMetadata() { - const metadataDisplay = document.getElementById('metadata-display'); - if (!metadataDisplay || !pageState.file) return; + const metadataDisplay = document.getElementById('metadata-display'); + if (!metadataDisplay || !pageState.file) return; - metadataDisplay.innerHTML = ''; - pageState.metadata = {}; + metadataDisplay.innerHTML = ''; + pageState.metadata = {}; + try { + const result = await loadPdfWithPasswordPrompt(pageState.file); + if (!result) return; showLoader('Analyzing full PDF metadata...'); + const { pdf: pdfjsDoc, file: currentFile } = result; + pageState.file = currentFile; - try { - const pdfBytes = await pageState.file.arrayBuffer(); - const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise; + const [metadataResult, fieldObjects] = await Promise.all([ + pdfjsDoc.getMetadata(), + pdfjsDoc.getFieldObjects(), + ]); - const [metadataResult, fieldObjects] = await Promise.all([ - pdfjsDoc.getMetadata(), - pdfjsDoc.getFieldObjects(), - ]); + const { info, metadata } = metadataResult; + const rawXmpString = metadata ? metadata.getRaw() : null; - const { info, metadata } = metadataResult; - const rawXmpString = metadata ? metadata.getRaw() : null; + // Info Dictionary Section + const infoSection = createSection('Info Dictionary'); + if (info && Object.keys(info).length > 0) { + for (const key in info) { + const value = (info as Record)[key]; + let displayValue: string; - // Info Dictionary Section - const infoSection = createSection('Info Dictionary'); - if (info && Object.keys(info).length > 0) { - for (const key in info) { - const value = (info as Record)[key]; - let displayValue: string; - - if (value === null || typeof value === 'undefined') { - displayValue = '- Not Set -'; - } else if (typeof value === 'object' && value !== null && 'name' in value) { - displayValue = String((value as { name: string }).name); - } else if (typeof value === 'object') { - try { - displayValue = JSON.stringify(value); - } catch { - displayValue = '[object Object]'; - } - } else if ((key === 'CreationDate' || key === 'ModDate') && typeof value === 'string') { - displayValue = parsePdfDate(value); - } else { - displayValue = String(value); - } - - pageState.metadata[key] = displayValue; - infoSection.ul.appendChild(createListItem(key, displayValue)); - } + if (value === null || typeof value === 'undefined') { + displayValue = '- Not Set -'; + } else if ( + typeof value === 'object' && + value !== null && + 'name' in value + ) { + displayValue = String((value as { name: string }).name); + } else if (typeof value === 'object') { + try { + displayValue = JSON.stringify(value); + } catch { + displayValue = '[object Object]'; + } + } else if ( + (key === 'CreationDate' || key === 'ModDate') && + typeof value === 'string' + ) { + displayValue = parsePdfDate(value); } else { - infoSection.ul.innerHTML = `
  • - No Info Dictionary data found -
  • `; + displayValue = String(value); } - metadataDisplay.appendChild(infoSection.wrapper); - // Interactive Form Fields Section - const fieldsSection = createSection('Interactive Form Fields'); - if (fieldObjects && Object.keys(fieldObjects).length > 0) { - for (const fieldName in fieldObjects) { - const field = (fieldObjects as Record>)[fieldName][0]; - const value = field.fieldValue || '- Not Set -'; - fieldsSection.ul.appendChild(createListItem(fieldName, String(value))); - } - } else { - fieldsSection.ul.innerHTML = `
  • - No interactive form fields found -
  • `; - } - metadataDisplay.appendChild(fieldsSection.wrapper); - - // XMP Metadata Section - const xmpSection = createSection('XMP Metadata'); - if (rawXmpString) { - try { - const parser = new DOMParser(); - const xmlDoc = parser.parseFromString(rawXmpString, 'application/xml'); - - const descriptions = xmlDoc.getElementsByTagName('rdf:Description'); - if (descriptions.length > 0) { - for (let i = 0; i < descriptions.length; i++) { - appendXmpNodes(descriptions[i], xmpSection.ul, 0); - } - } else { - appendXmpNodes(xmlDoc.documentElement, xmpSection.ul, 0); - } - - if (xmpSection.ul.children.length === 0) { - xmpSection.ul.innerHTML = `
  • - No parseable XMP properties found -
  • `; - } - } catch (xmlError) { - console.error('Failed to parse XMP XML:', xmlError); - xmpSection.ul.innerHTML = `
  • - Error parsing XMP XML. Displaying raw. -
  • `; - const pre = document.createElement('pre'); - pre.className = 'text-xs text-gray-300 whitespace-pre-wrap break-all'; - pre.textContent = rawXmpString; - xmpSection.ul.appendChild(pre); - } - } else { - xmpSection.ul.innerHTML = `
  • - No XMP metadata found -
  • `; - } - metadataDisplay.appendChild(xmpSection.wrapper); - - createIcons({ icons }); - } catch (e) { - console.error('Failed to view metadata or fields:', e); - showAlert('Error', 'Could not fully analyze the PDF. It may be corrupted or have an unusual structure.'); - } finally { - hideLoader(); + pageState.metadata[key] = displayValue; + infoSection.ul.appendChild(createListItem(key, displayValue)); + } + } else { + infoSection.ul.innerHTML = `
  • - No Info Dictionary data found -
  • `; } + metadataDisplay.appendChild(infoSection.wrapper); + + // Interactive Form Fields Section + const fieldsSection = createSection('Interactive Form Fields'); + if (fieldObjects && Object.keys(fieldObjects).length > 0) { + for (const fieldName in fieldObjects) { + const field = ( + fieldObjects as Record> + )[fieldName][0]; + const value = field.fieldValue || '- Not Set -'; + fieldsSection.ul.appendChild(createListItem(fieldName, String(value))); + } + } else { + fieldsSection.ul.innerHTML = `
  • - No interactive form fields found -
  • `; + } + metadataDisplay.appendChild(fieldsSection.wrapper); + + // XMP Metadata Section + const xmpSection = createSection('XMP Metadata'); + if (rawXmpString) { + try { + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(rawXmpString, 'application/xml'); + + const descriptions = xmlDoc.getElementsByTagName('rdf:Description'); + if (descriptions.length > 0) { + for (let i = 0; i < descriptions.length; i++) { + appendXmpNodes(descriptions[i], xmpSection.ul, 0); + } + } else { + appendXmpNodes(xmlDoc.documentElement, xmpSection.ul, 0); + } + + if (xmpSection.ul.children.length === 0) { + xmpSection.ul.innerHTML = `
  • - No parseable XMP properties found -
  • `; + } + } catch (xmlError) { + console.error('Failed to parse XMP XML:', xmlError); + xmpSection.ul.innerHTML = `
  • - Error parsing XMP XML. Displaying raw. -
  • `; + const pre = document.createElement('pre'); + pre.className = 'text-xs text-gray-300 whitespace-pre-wrap break-all'; + pre.textContent = rawXmpString; + xmpSection.ul.appendChild(pre); + } + } else { + xmpSection.ul.innerHTML = `
  • - No XMP metadata found -
  • `; + } + metadataDisplay.appendChild(xmpSection.wrapper); + + pdfjsDoc.destroy(); + createIcons({ icons }); + } catch (e) { + console.error('Failed to view metadata or fields:', e); + showAlert( + 'Error', + 'Could not fully analyze the PDF. It may be corrupted or have an unusual structure.' + ); + } finally { + hideLoader(); + } } 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'); - 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.file) { + 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 = pageState.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(pageState.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 () { + resetState(); + }; - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); - await displayMetadata(); + await displayMetadata(); - if (toolOptions) toolOptions.classList.remove('hidden'); - } else { - if (toolOptions) toolOptions.classList.add('hidden'); - } + if (toolOptions) toolOptions.classList.remove('hidden'); + } else { + if (toolOptions) toolOptions.classList.add('hidden'); + } } function copyMetadataAsJson() { - const jsonString = JSON.stringify(pageState.metadata, null, 2); - navigator.clipboard.writeText(jsonString).then(function () { - showAlert('Copied', 'Metadata copied to clipboard as JSON.'); - }).catch(function (err) { - console.error('Failed to copy:', err); - showAlert('Error', 'Failed to copy metadata to clipboard.'); + const jsonString = JSON.stringify(pageState.metadata, null, 2); + navigator.clipboard + .writeText(jsonString) + .then(function () { + showAlert('Copied', 'Metadata copied to clipboard as JSON.'); + }) + .catch(function (err) { + console.error('Failed to copy:', err); + showAlert('Error', 'Failed to copy metadata to clipboard.'); }); } 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 file = files[0]; + if ( + file.type === 'application/pdf' || + file.name.toLowerCase().endsWith('.pdf') + ) { + pageState.file = file; + updateUI(); } + } } document.addEventListener('DOMContentLoaded', function () { - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const copyBtn = document.getElementById('copy-metadata'); - const backBtn = document.getElementById('back-to-tools'); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const copyBtn = document.getElementById('copy-metadata'); + 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); + }); + + 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('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); + } + } + }); - if (fileInput && dropZone) { - fileInput.addEventListener('change', function (e) { - handleFileSelect((e.target as HTMLInputElement).files); - }); + fileInput.addEventListener('click', function () { + fileInput.value = ''; + }); + } - 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('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); - } - } - }); - - fileInput.addEventListener('click', function () { - fileInput.value = ''; - }); - } - - if (copyBtn) { - copyBtn.addEventListener('click', copyMetadataAsJson); - } + if (copyBtn) { + copyBtn.addEventListener('click', copyMetadataAsJson); + } }); diff --git a/src/js/types/index.ts b/src/js/types/index.ts index 3840c91..b859335 100644 --- a/src/js/types/index.ts +++ b/src/js/types/index.ts @@ -54,3 +54,4 @@ export * from './page-preview-type.ts'; export * from './add-page-labels-type.ts'; export * from './pdf-to-tiff-type.ts'; export * from './pdf-to-cbz-type.ts'; +export * from './password-prompt-type.ts'; diff --git a/src/js/types/password-prompt-type.ts b/src/js/types/password-prompt-type.ts new file mode 100644 index 0000000..8f05298 --- /dev/null +++ b/src/js/types/password-prompt-type.ts @@ -0,0 +1,7 @@ +import type { PDFDocumentProxy } from 'pdfjs-dist'; + +export interface LoadedPdf { + pdf: PDFDocumentProxy; + bytes: ArrayBuffer; + file: File; +} diff --git a/src/js/utils/password-prompt.ts b/src/js/utils/password-prompt.ts new file mode 100644 index 0000000..3b201f8 --- /dev/null +++ b/src/js/utils/password-prompt.ts @@ -0,0 +1,847 @@ +import { decryptPdfBytes } from './pdf-decrypt.js'; +import { readFileAsArrayBuffer, getPDFDocument } from './helpers.js'; +import { createIcons, icons } from 'lucide'; +import { PasswordResponses } from 'pdfjs-dist'; +import type { LoadedPdf } from '@/types'; + +let cachedPassword: string | null = null; +let activeModalPromise: Promise | null = null; + +function getEl(id: string): T | null { + return document.getElementById(id) as T | null; +} + +function esc(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function ensureSingleModal(): HTMLDivElement { + let modal = getEl('password-modal'); + if (modal) return modal; + + modal = document.createElement('div'); + modal.id = 'password-modal'; + modal.className = + 'fixed inset-0 bg-black/70 backdrop-blur-sm z-[100] hidden items-center justify-center p-4'; + modal.innerHTML = ` +
    +
    +
    +
    + +
    +
    +

    Password Required

    +

    +
    +
    +
    +
    + + +
    + + +
    +
    +
    + + +
    +
    `; + document.body.appendChild(modal); + return modal; +} + +function ensureBatchModal(): HTMLDivElement { + let modal = getEl('password-batch-modal'); + if (modal) return modal; + + modal = document.createElement('div'); + modal.id = 'password-batch-modal'; + modal.className = + 'fixed inset-0 bg-black/70 backdrop-blur-sm z-[100] hidden items-center justify-center p-4'; + modal.innerHTML = ` +
    +
    +
    +
    + +
    +
    +

    +

    Enter passwords for each encrypted file

    +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + +
    +
    + + +
    +
    `; + document.body.appendChild(modal); + return modal; +} + +function validatePasswordWithPdfjs( + pdfBytes: ArrayBuffer, + password: string +): Promise { + return new Promise((resolve) => { + let settled = false; + + const task = getPDFDocument({ + data: pdfBytes.slice(0), + password, + }); + + task.onPassword = ( + _callback: (password: string) => void, + reason: number + ) => { + if (settled) return; + settled = true; + resolve(reason !== PasswordResponses.INCORRECT_PASSWORD); + task.destroy().catch(() => {}); + }; + + task.promise + .then((doc) => { + doc.destroy(); + if (!settled) { + settled = true; + resolve(true); + } + }) + .catch(() => { + if (!settled) { + settled = true; + resolve(false); + } + }); + }); +} + +async function isFileEncrypted(file: File): Promise { + const bytes = (await readFileAsArrayBuffer(file)) as ArrayBuffer; + return new Promise((resolve) => { + let settled = false; + const task = getPDFDocument({ data: bytes.slice(0) }); + + task.onPassword = () => { + if (!settled) { + settled = true; + resolve(true); + task.destroy().catch(() => {}); + } + }; + + task.promise + .then((doc) => { + doc.destroy(); + if (!settled) { + settled = true; + resolve(false); + } + }) + .catch(() => { + if (!settled) { + settled = true; + resolve(false); + } + }); + }); +} + +async function decryptFileWithPassword( + file: File, + password: string +): Promise { + const fileBytes = (await readFileAsArrayBuffer(file)) as ArrayBuffer; + const inputBytes = new Uint8Array(fileBytes); + const result = await decryptPdfBytes(inputBytes, password); + return new File([new Uint8Array(result.bytes)], file.name, { + type: 'application/pdf', + }); +} + +export async function promptAndDecryptFile(file: File): Promise { + if (activeModalPromise) { + await activeModalPromise; + } + + const fileBytes = (await readFileAsArrayBuffer(file)) as ArrayBuffer; + + if (cachedPassword) { + const valid = await validatePasswordWithPdfjs(fileBytes, cachedPassword); + if (valid) { + try { + return await decryptFileWithPassword(file, cachedPassword); + } catch { + cachedPassword = null; + } + } else { + cachedPassword = null; + } + } + + const modal = ensureSingleModal(); + const input = getEl('password-modal-input'); + const titleEl = getEl('password-modal-title'); + const subtitleEl = getEl('password-modal-subtitle'); + const errorEl = getEl('password-modal-error'); + const progressEl = getEl('password-modal-progress'); + const submitBtn = getEl('password-modal-submit'); + const cancelBtn = getEl('password-modal-cancel'); + const toggleBtn = getEl('password-modal-toggle'); + + if (!input || !submitBtn || !cancelBtn) return null; + + if (titleEl) titleEl.textContent = 'Password Required'; + if (subtitleEl) subtitleEl.textContent = file.name; + if (errorEl) { + errorEl.textContent = ''; + errorEl.classList.add('hidden'); + } + if (progressEl) { + progressEl.textContent = ''; + progressEl.classList.add('hidden'); + } + input.value = ''; + input.type = 'password'; + submitBtn.disabled = false; + submitBtn.textContent = 'Unlock'; + submitBtn.dataset.originalText = 'Unlock'; + cancelBtn.disabled = false; + cancelBtn.textContent = 'Skip'; + + modal.classList.remove('hidden'); + modal.classList.add('flex'); + createIcons({ icons }); + setTimeout(() => input.focus(), 100); + + const modalPromise = new Promise((resolve) => { + let resolved = false; + let busy = false; + + function cleanup() { + modal.classList.add('hidden'); + modal.classList.remove('flex'); + submitBtn.removeEventListener('click', onSubmit); + cancelBtn.removeEventListener('click', onCancel); + input.removeEventListener('keydown', onKeydown); + if (toggleBtn) toggleBtn.removeEventListener('click', onToggle); + } + + function finish(result: File | null) { + if (resolved) return; + resolved = true; + cleanup(); + resolve(result); + } + + async function onSubmit() { + if (busy) return; + const password = input.value; + if (!password) { + if (errorEl) { + errorEl.textContent = 'Please enter a password'; + errorEl.classList.remove('hidden'); + } + return; + } + + if (errorEl) errorEl.classList.add('hidden'); + busy = true; + submitBtn.disabled = true; + cancelBtn.disabled = true; + if (progressEl) { + progressEl.textContent = 'Validating...'; + progressEl.classList.remove('hidden'); + } + + const valid = await validatePasswordWithPdfjs(fileBytes, password); + + if (!valid) { + busy = false; + submitBtn.disabled = false; + cancelBtn.disabled = false; + if (progressEl) progressEl.classList.add('hidden'); + input.value = ''; + input.focus(); + if (errorEl) { + errorEl.textContent = 'Incorrect password. Please try again.'; + errorEl.classList.remove('hidden'); + } + return; + } + + if (progressEl) progressEl.textContent = 'Decrypting...'; + + try { + const decrypted = await decryptFileWithPassword(file, password); + cachedPassword = password; + if (progressEl) progressEl.classList.add('hidden'); + finish(decrypted); + } catch { + busy = false; + submitBtn.disabled = false; + cancelBtn.disabled = false; + if (progressEl) progressEl.classList.add('hidden'); + if (errorEl) { + errorEl.textContent = + 'Failed to decrypt. Try the Decrypt tool instead.'; + errorEl.classList.remove('hidden'); + } + } + } + + function onCancel() { + if (busy) return; + finish(null); + } + + function onKeydown(e: KeyboardEvent) { + if (e.key === 'Enter') { + e.preventDefault(); + onSubmit(); + } else if (e.key === 'Escape') { + e.preventDefault(); + onCancel(); + } + } + + function onToggle() { + const isPassword = input.type === 'password'; + input.type = isPassword ? 'text' : 'password'; + const icon = toggleBtn?.querySelector('i[data-lucide]'); + if (icon) + icon.setAttribute('data-lucide', isPassword ? 'eye-off' : 'eye'); + createIcons({ icons }); + } + + submitBtn.addEventListener('click', onSubmit); + cancelBtn.addEventListener('click', onCancel); + input.addEventListener('keydown', onKeydown); + if (toggleBtn) toggleBtn.addEventListener('click', onToggle); + }); + + activeModalPromise = modalPromise; + modalPromise.finally(() => { + if (activeModalPromise === modalPromise) activeModalPromise = null; + }); + return modalPromise; +} + +export async function promptAndDecryptBatch( + files: File[], + encryptedIndices: number[] +): Promise> { + if (activeModalPromise) { + await activeModalPromise; + } + + const decryptedFiles = new Map(); + if (encryptedIndices.length === 0) return decryptedFiles; + + if (encryptedIndices.length === 1) { + const idx = encryptedIndices[0]; + const decrypted = await promptAndDecryptFile(files[idx]); + if (decrypted) decryptedFiles.set(idx, decrypted); + return decryptedFiles; + } + + if (cachedPassword) { + let allValid = true; + for (const idx of encryptedIndices) { + const bytes = (await readFileAsArrayBuffer(files[idx])) as ArrayBuffer; + const valid = await validatePasswordWithPdfjs(bytes, cachedPassword); + if (!valid) { + allValid = false; + cachedPassword = null; + break; + } + } + + if (allValid && cachedPassword) { + const tempMap = new Map(); + let allDecrypted = true; + for (const idx of encryptedIndices) { + try { + tempMap.set( + idx, + await decryptFileWithPassword(files[idx], cachedPassword) + ); + } catch { + cachedPassword = null; + allDecrypted = false; + break; + } + } + if (allDecrypted && tempMap.size === encryptedIndices.length) { + for (const [k, v] of tempMap) decryptedFiles.set(k, v); + return decryptedFiles; + } + } + } + + const fileNames = encryptedIndices.map((i) => files[i].name); + + const modal = ensureBatchModal(); + const titleEl = getEl('batch-modal-title'); + const samePwCheckbox = getEl('batch-modal-same-pw'); + const sharedSection = getEl('batch-modal-shared'); + const sharedInput = getEl('batch-modal-shared-input'); + const sharedToggle = getEl('batch-modal-shared-toggle'); + const filelistEl = getEl('batch-modal-filelist'); + const errorEl = getEl('batch-modal-error'); + const progressEl = getEl('batch-modal-progress'); + const submitBtn = getEl('batch-modal-submit'); + const cancelBtn = getEl('batch-modal-cancel'); + + if ( + !submitBtn || + !cancelBtn || + !samePwCheckbox || + !sharedInput || + !filelistEl + ) { + return decryptedFiles; + } + + if (titleEl) + titleEl.textContent = `${fileNames.length} Files Need a Password`; + + samePwCheckbox.checked = true; + sharedInput.value = ''; + sharedInput.type = 'password'; + if (sharedSection) sharedSection.classList.remove('hidden'); + filelistEl.classList.add('hidden'); + + filelistEl.innerHTML = fileNames + .map( + (name, i) => + `
    + + ${esc(name)} +
    + + +
    +
    ` + ) + .join(''); + + if (errorEl) { + errorEl.textContent = ''; + errorEl.classList.add('hidden'); + } + if (progressEl) { + progressEl.textContent = ''; + progressEl.classList.add('hidden'); + } + submitBtn.disabled = false; + submitBtn.textContent = 'Unlock All'; + submitBtn.dataset.originalText = 'Unlock All'; + cancelBtn.disabled = false; + + modal.classList.remove('hidden'); + modal.classList.add('flex'); + createIcons({ icons }); + setTimeout(() => sharedInput.focus(), 100); + + const batchPromise = new Promise>((resolve) => { + let resolved = false; + let busy = false; + const skippedSet = new Set(); + const succeededSet = new Set(); + + function getRemainingCount(): number { + let count = 0; + for (let i = 0; i < fileNames.length; i++) { + if (!skippedSet.has(i) && !succeededSet.has(i)) count++; + } + return count; + } + + function updateButtons(autoClose = false) { + const hasSucceeded = succeededSet.size > 0; + cancelBtn.textContent = hasSucceeded ? 'Skip Remaining' : 'Skip All'; + + const remaining = getRemainingCount(); + if (autoClose && remaining === 0) { + finish(decryptedFiles); + return; + } + + if (hasSucceeded) { + submitBtn.textContent = `Unlock Remaining (${remaining})`; + submitBtn.dataset.originalText = submitBtn.textContent; + } + } + + function toggleMode() { + const useSame = samePwCheckbox.checked; + if (sharedSection) sharedSection.classList.toggle('hidden', !useSame); + filelistEl.classList.toggle('hidden', useSame); + if (useSame) { + setTimeout(() => sharedInput.focus(), 50); + } else { + const firstInput = filelistEl.querySelector( + 'input[data-pw-idx]:not(:disabled)' + ); + if (firstInput) setTimeout(() => firstInput.focus(), 50); + } + } + + function markRowSuccess(localIdx: number) { + succeededSet.add(localIdx); + const row = filelistEl.querySelector( + `[data-file-idx="${localIdx}"]` + ); + if (!row) return; + row.classList.remove('border', 'border-red-500/50'); + row.classList.add('opacity-50', 'border', 'border-green-500/50'); + const pwInput = row.querySelector('input[data-pw-idx]'); + if (pwInput) pwInput.disabled = true; + const skipBtn = row.querySelector('[data-skip-idx]'); + if (skipBtn) skipBtn.classList.add('hidden'); + const iconEl = row.querySelector( + `[data-icon-idx="${localIdx}"]` + ); + if (iconEl) { + iconEl.setAttribute('data-lucide', 'check-circle'); + iconEl.classList.remove('text-indigo-400'); + iconEl.classList.add('text-green-400'); + } + createIcons({ icons }); + } + + function markRowFailed(localIdx: number) { + const row = filelistEl.querySelector( + `[data-file-idx="${localIdx}"]` + ); + if (!row) return; + row.classList.remove('border-green-500/50'); + row.classList.add('border', 'border-red-500/50'); + const pwInput = row.querySelector('input[data-pw-idx]'); + if (pwInput) { + pwInput.value = ''; + pwInput.focus(); + pwInput.classList.add('border-red-500'); + setTimeout(() => pwInput.classList.remove('border-red-500'), 2000); + } + } + + function onSkipFile(e: Event) { + if (busy) return; + const btn = (e.target as HTMLElement).closest( + '[data-skip-idx]' + ); + if (!btn || btn.dataset.skipIdx === undefined) return; + const idx = parseInt(btn.dataset.skipIdx, 10); + if (isNaN(idx) || succeededSet.has(idx)) return; + const row = filelistEl.querySelector( + `[data-file-idx="${idx}"]` + ); + if (!row) return; + + if (skippedSet.has(idx)) { + skippedSet.delete(idx); + row.classList.remove('opacity-40'); + const pwInput = + row.querySelector('input[data-pw-idx]'); + if (pwInput) pwInput.disabled = false; + btn.title = 'Skip this file'; + } else { + skippedSet.add(idx); + row.classList.add('opacity-40'); + row.classList.remove('border', 'border-red-500/50'); + const pwInput = + row.querySelector('input[data-pw-idx]'); + if (pwInput) pwInput.disabled = true; + btn.title = 'Include this file'; + } + updateButtons(true); + } + + function cleanup() { + modal.classList.add('hidden'); + modal.classList.remove('flex'); + submitBtn.removeEventListener('click', onSubmit); + cancelBtn.removeEventListener('click', onCancel); + samePwCheckbox.removeEventListener('change', toggleMode); + filelistEl.removeEventListener('click', onSkipFile); + if (sharedToggle) + sharedToggle.removeEventListener('click', onSharedToggle); + sharedInput.removeEventListener('keydown', onKeydown); + } + + function finish(result: Map) { + if (resolved) return; + resolved = true; + cleanup(); + resolve(result); + } + + async function onSubmit() { + if (busy) return; + const useSame = samePwCheckbox.checked; + + const toProcess: { localIdx: number; password: string }[] = []; + + for (let i = 0; i < fileNames.length; i++) { + if (skippedSet.has(i) || succeededSet.has(i)) continue; + + let password: string; + if (useSame) { + password = sharedInput.value; + } else { + const pwInput = filelistEl.querySelector( + `input[data-pw-idx="${i}"]` + ); + password = pwInput?.value || ''; + } + + if (!password) { + if (errorEl) { + const msg = useSame + ? 'Please enter a password' + : `Please enter a password for ${fileNames[i]} or skip it`; + errorEl.textContent = msg; + errorEl.classList.remove('hidden'); + } + return; + } + + toProcess.push({ localIdx: i, password }); + } + + if (toProcess.length === 0) { + finish(decryptedFiles); + return; + } + + if (errorEl) errorEl.classList.add('hidden'); + busy = true; + submitBtn.disabled = true; + cancelBtn.disabled = true; + + const failedNames: string[] = []; + + for (let i = 0; i < toProcess.length; i++) { + const { localIdx, password } = toProcess[i]; + const realIdx = encryptedIndices[localIdx]; + + if (progressEl) { + progressEl.textContent = `Validating ${i + 1} of ${toProcess.length}: ${files[realIdx].name}`; + progressEl.classList.remove('hidden'); + } + + const bytes = (await readFileAsArrayBuffer( + files[realIdx] + )) as ArrayBuffer; + const valid = await validatePasswordWithPdfjs(bytes, password); + + if (!valid) { + failedNames.push(files[realIdx].name); + markRowFailed(localIdx); + continue; + } + + if (progressEl) { + progressEl.textContent = `Decrypting ${i + 1} of ${toProcess.length}: ${files[realIdx].name}`; + } + + try { + const decrypted = await decryptFileWithPassword( + files[realIdx], + password + ); + decryptedFiles.set(realIdx, decrypted); + markRowSuccess(localIdx); + } catch { + failedNames.push(files[realIdx].name); + markRowFailed(localIdx); + } + } + + if (progressEl) progressEl.classList.add('hidden'); + busy = false; + submitBtn.disabled = false; + cancelBtn.disabled = false; + + if (failedNames.length > 0) { + if (errorEl) { + errorEl.textContent = `Wrong password for: ${failedNames.join(', ')}`; + errorEl.classList.remove('hidden'); + } + if (!samePwCheckbox.checked) { + const firstFailed = filelistEl.querySelector( + 'input[data-pw-idx]:not(:disabled)' + ); + if (firstFailed) firstFailed.focus(); + } else { + sharedInput.value = ''; + sharedInput.focus(); + } + updateButtons(); + submitBtn.textContent = 'Retry Failed'; + return; + } + + if (useSame && toProcess.length > 0) { + cachedPassword = toProcess[0].password; + } + + updateButtons(); + if (!resolved) finish(decryptedFiles); + } + + function onCancel() { + if (busy) return; + finish(decryptedFiles); + } + + function onKeydown(e: KeyboardEvent) { + if (e.key === 'Enter') { + e.preventDefault(); + onSubmit(); + } else if (e.key === 'Escape') { + e.preventDefault(); + onCancel(); + } + } + + function onSharedToggle() { + const isPassword = sharedInput.type === 'password'; + sharedInput.type = isPassword ? 'text' : 'password'; + const icon = sharedToggle?.querySelector('i[data-lucide]'); + if (icon) + icon.setAttribute('data-lucide', isPassword ? 'eye-off' : 'eye'); + createIcons({ icons }); + } + + samePwCheckbox.addEventListener('change', toggleMode); + submitBtn.addEventListener('click', onSubmit); + cancelBtn.addEventListener('click', onCancel); + filelistEl.addEventListener('click', onSkipFile); + sharedInput.addEventListener('keydown', onKeydown); + if (sharedToggle) sharedToggle.addEventListener('click', onSharedToggle); + }); + + activeModalPromise = batchPromise; + batchPromise.finally(() => { + if (activeModalPromise === batchPromise) activeModalPromise = null; + }); + return batchPromise; +} + +export type { LoadedPdf }; + +export async function loadPdfWithPasswordPrompt( + file: File, + files?: File[], + index?: number +): Promise { + let bytes = (await readFileAsArrayBuffer(file)) as ArrayBuffer; + let currentFile = file; + + try { + const pdf = await getPDFDocument(bytes).promise; + return { pdf, bytes, file: currentFile }; + } catch (err: unknown) { + if ( + err && + typeof err === 'object' && + 'name' in err && + (err as { name: string }).name === 'PasswordException' + ) { + const decryptedFile = await promptAndDecryptFile(currentFile); + if (!decryptedFile) return null; + currentFile = decryptedFile; + if (files && index !== undefined) { + files[index] = decryptedFile; + } + bytes = (await readFileAsArrayBuffer(decryptedFile)) as ArrayBuffer; + const pdf = await getPDFDocument(bytes).promise; + return { pdf, bytes, file: currentFile }; + } + throw err; + } +} + +export async function batchDecryptIfNeeded(files: File[]): Promise { + const encryptedIndices: number[] = []; + + for (let i = 0; i < files.length; i++) { + const encrypted = await isFileEncrypted(files[i]); + if (encrypted) encryptedIndices.push(i); + } + + if (encryptedIndices.length === 0) return [...files]; + + const decryptedMap = await promptAndDecryptBatch(files, encryptedIndices); + const skippedSet = new Set( + encryptedIndices.filter((idx) => !decryptedMap.has(idx)) + ); + + const result: File[] = []; + for (let i = 0; i < files.length; i++) { + if (skippedSet.has(i)) continue; + result.push(decryptedMap.get(i) ?? files[i]); + } + + return result; +} + +export async function handleEncryptedFiles( + files: File[], + encryptedIndices: number[] +): Promise> { + return promptAndDecryptBatch(files, encryptedIndices); +} diff --git a/src/tests/password-prompt.test.ts b/src/tests/password-prompt.test.ts new file mode 100644 index 0000000..393c5a5 --- /dev/null +++ b/src/tests/password-prompt.test.ts @@ -0,0 +1,1272 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type { Mock } from 'vitest'; + +let mockGetPDFDocument: Mock; +let mockReadFileAsArrayBuffer: Mock; +let mockDecryptPdfBytes: Mock; + +vi.mock('@/js/utils/helpers.js', () => ({ + getPDFDocument: (...args: unknown[]) => mockGetPDFDocument(...args), + readFileAsArrayBuffer: (...args: unknown[]) => + mockReadFileAsArrayBuffer(...args), +})); + +vi.mock('@/js/utils/pdf-decrypt.js', () => ({ + decryptPdfBytes: (...args: unknown[]) => mockDecryptPdfBytes(...args), +})); + +vi.mock('lucide', () => ({ + createIcons: vi.fn(), + icons: {}, +})); + +vi.mock('pdfjs-dist', () => ({ + PasswordResponses: { + NEED_PASSWORD: 1, + INCORRECT_PASSWORD: 2, + }, +})); + +function createMockFile(name: string, size = 100): File { + const content = new Uint8Array(size); + return new File([content], name, { type: 'application/pdf' }); +} + +interface MockLoadingTask { + promise: Promise<{ destroy: () => Promise }>; + onPassword: ((callback: (pw: string) => void, reason: number) => void) | null; + destroy: () => Promise; +} + +function mockDestroy() { + return vi.fn().mockReturnValue(Promise.resolve()); +} + +function createNonEncryptedTask(): MockLoadingTask { + const mockDoc = { destroy: mockDestroy() }; + const task: MockLoadingTask = { + promise: Promise.resolve(mockDoc), + onPassword: null, + destroy: mockDestroy(), + }; + return task; +} + +function createEncryptedTask(): MockLoadingTask { + let rejectFn: (err: unknown) => void; + const task: MockLoadingTask = { + promise: new Promise((_resolve, reject) => { + rejectFn = reject; + }), + onPassword: null, + destroy: mockDestroy(), + }; + + setTimeout(() => { + if (task.onPassword) { + task.onPassword(() => {}, 1); + } + rejectFn({ name: 'PasswordException', message: 'No password given' }); + }, 0); + + return task; +} + +function createPasswordValidationTask(isValid: boolean): MockLoadingTask { + if (isValid) { + const mockDoc = { destroy: mockDestroy() }; + return { + promise: Promise.resolve(mockDoc), + onPassword: null, + destroy: mockDestroy(), + }; + } + + let rejectFn: (err: unknown) => void; + const task: MockLoadingTask = { + promise: new Promise((_resolve, reject) => { + rejectFn = reject; + }), + onPassword: null, + destroy: mockDestroy(), + }; + + setTimeout(() => { + if (task.onPassword) { + task.onPassword(() => {}, 2); + } + rejectFn({ name: 'PasswordException', message: 'Incorrect password' }); + }, 0); + + return task; +} + +let batchDecryptIfNeeded: typeof import('@/js/utils/password-prompt').batchDecryptIfNeeded; +let loadPdfWithPasswordPrompt: typeof import('@/js/utils/password-prompt').loadPdfWithPasswordPrompt; +let handleEncryptedFiles: typeof import('@/js/utils/password-prompt').handleEncryptedFiles; +let promptAndDecryptFile: typeof import('@/js/utils/password-prompt').promptAndDecryptFile; + +beforeEach(async () => { + vi.resetModules(); + + mockGetPDFDocument = vi.fn(); + mockReadFileAsArrayBuffer = vi.fn(); + mockDecryptPdfBytes = vi.fn(); + + mockReadFileAsArrayBuffer.mockImplementation((file: File) => { + return file.arrayBuffer(); + }); + + const mod = await import('@/js/utils/password-prompt'); + batchDecryptIfNeeded = mod.batchDecryptIfNeeded; + loadPdfWithPasswordPrompt = mod.loadPdfWithPasswordPrompt; + handleEncryptedFiles = mod.handleEncryptedFiles; + promptAndDecryptFile = mod.promptAndDecryptFile; +}); + +describe('batchDecryptIfNeeded', () => { + it('should return empty array for empty input', async () => { + const result = await batchDecryptIfNeeded([]); + expect(result).toEqual([]); + }); + + it('should return same files when none are encrypted', async () => { + mockGetPDFDocument.mockImplementation(() => createNonEncryptedTask()); + + const files = [ + createMockFile('a.pdf'), + createMockFile('b.pdf'), + createMockFile('c.pdf'), + ]; + + const result = await batchDecryptIfNeeded(files); + expect(result).toHaveLength(3); + expect(result[0].name).toBe('a.pdf'); + expect(result[1].name).toBe('b.pdf'); + expect(result[2].name).toBe('c.pdf'); + }); + + it('should return a new array copy when none are encrypted', async () => { + mockGetPDFDocument.mockImplementation(() => createNonEncryptedTask()); + + const files = [createMockFile('a.pdf')]; + const result = await batchDecryptIfNeeded(files); + expect(result).not.toBe(files); + }); + + it('should detect encrypted files via getPDFDocument onPassword callback', async () => { + const files = [createMockFile('encrypted.pdf')]; + + mockGetPDFDocument.mockImplementation(() => createEncryptedTask()); + + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([1, 2, 3]), + engine: 'cpdf', + }); + + const modalPromise = batchDecryptIfNeeded(files); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + const input = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + + mockGetPDFDocument.mockImplementation(() => + createPasswordValidationTask(true) + ); + + input.value = 'secret'; + submitBtn.click(); + + const result = await modalPromise; + expect(result).toHaveLength(1); + expect(result[0].name).toBe('encrypted.pdf'); + }); + + it('should skip encrypted files when user cancels modal', async () => { + const files = [createMockFile('encrypted.pdf')]; + + mockGetPDFDocument.mockImplementation(() => createEncryptedTask()); + + const modalPromise = batchDecryptIfNeeded(files); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + const cancelBtn = document.getElementById( + 'password-modal-cancel' + ) as HTMLButtonElement; + cancelBtn.click(); + + const result = await modalPromise; + expect(result).toHaveLength(0); + }); + + it('should pass through non-encrypted and skip cancelled encrypted files', async () => { + const files = [createMockFile('plain.pdf'), createMockFile('locked.pdf')]; + + let callCount = 0; + mockGetPDFDocument.mockImplementation(() => { + callCount++; + if (callCount <= 1) return createNonEncryptedTask(); + if (callCount === 2) return createEncryptedTask(); + return createNonEncryptedTask(); + }); + + const modalPromise = batchDecryptIfNeeded(files); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + const cancelBtn = document.getElementById( + 'password-modal-cancel' + ) as HTMLButtonElement; + cancelBtn.click(); + + const result = await modalPromise; + expect(result).toHaveLength(1); + expect(result[0].name).toBe('plain.pdf'); + }); + + it('should pass through non-encrypted and include decrypted files', async () => { + const files = [createMockFile('plain.pdf'), createMockFile('locked.pdf')]; + + let callCount = 0; + mockGetPDFDocument.mockImplementation(() => { + callCount++; + if (callCount <= 1) return createNonEncryptedTask(); + if (callCount === 2) return createEncryptedTask(); + return createPasswordValidationTask(true); + }); + + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([10, 20, 30]), + engine: 'cpdf', + }); + + const modalPromise = batchDecryptIfNeeded(files); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + const input = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + input.value = 'pass123'; + submitBtn.click(); + + const result = await modalPromise; + expect(result).toHaveLength(2); + expect(result[0].name).toBe('plain.pdf'); + expect(result[1].name).toBe('locked.pdf'); + }); + + it('should handle single encrypted file via promptAndDecryptFile', async () => { + const files = [createMockFile('single.pdf')]; + + mockGetPDFDocument.mockImplementation(() => createEncryptedTask()); + + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([5, 6, 7]), + engine: 'cpdf', + }); + + const modalPromise = batchDecryptIfNeeded(files); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + mockGetPDFDocument.mockImplementation(() => + createPasswordValidationTask(true) + ); + + const input = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + input.value = 'test'; + submitBtn.click(); + + const result = await modalPromise; + expect(result).toHaveLength(1); + expect(result[0].name).toBe('single.pdf'); + }); + + it('should handle file with 0 bytes', async () => { + const files = [createMockFile('empty.pdf', 0)]; + + mockGetPDFDocument.mockImplementation(() => createNonEncryptedTask()); + + const result = await batchDecryptIfNeeded(files); + expect(result).toHaveLength(1); + expect(result[0].name).toBe('empty.pdf'); + }); + + it('should handle multiple files with same name', async () => { + mockGetPDFDocument.mockImplementation(() => createNonEncryptedTask()); + + const files = [ + createMockFile('report.pdf'), + createMockFile('report.pdf'), + createMockFile('report.pdf'), + ]; + + const result = await batchDecryptIfNeeded(files); + expect(result).toHaveLength(3); + expect(result[0].name).toBe('report.pdf'); + expect(result[1].name).toBe('report.pdf'); + expect(result[2].name).toBe('report.pdf'); + }); + + it('should handle getPDFDocument rejecting with non-password error', async () => { + const task: MockLoadingTask = { + promise: Promise.reject(new Error('Corrupted PDF')), + onPassword: null, + destroy: mockDestroy(), + }; + mockGetPDFDocument.mockImplementation(() => task); + + const files = [createMockFile('corrupt.pdf')]; + const result = await batchDecryptIfNeeded(files); + expect(result).toHaveLength(1); + expect(result[0].name).toBe('corrupt.pdf'); + }); +}); + +describe('batchDecryptIfNeeded with cached password', () => { + it('should use cached password without showing modal', async () => { + const files = [createMockFile('file1.pdf')]; + + mockGetPDFDocument.mockImplementation(() => createEncryptedTask()); + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([1, 2, 3]), + engine: 'cpdf', + }); + + const firstPromise = batchDecryptIfNeeded(files); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + mockGetPDFDocument.mockImplementation(() => + createPasswordValidationTask(true) + ); + + const input = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + input.value = 'cached-pw'; + submitBtn.click(); + + await firstPromise; + + const files2 = [createMockFile('file2.pdf')]; + + let getPDFCallCount = 0; + mockGetPDFDocument.mockImplementation(() => { + getPDFCallCount++; + if (getPDFCallCount === 1) return createEncryptedTask(); + return createPasswordValidationTask(true); + }); + + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([4, 5, 6]), + engine: 'cpdf', + }); + + const result = await batchDecryptIfNeeded(files2); + expect(result).toHaveLength(1); + expect(result[0].name).toBe('file2.pdf'); + + const modal = document.getElementById('password-modal'); + expect(!modal || modal.classList.contains('hidden')).toBe(true); + }); + + it('should clear cache and show modal when cached password fails validation', async () => { + const files = [createMockFile('first.pdf')]; + + mockGetPDFDocument.mockImplementation(() => createEncryptedTask()); + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([1]), + engine: 'cpdf', + }); + + const firstPromise = batchDecryptIfNeeded(files); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + mockGetPDFDocument.mockImplementation(() => + createPasswordValidationTask(true) + ); + + const input = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + input.value = 'old-pw'; + submitBtn.click(); + + await firstPromise; + + const files2 = [createMockFile('second.pdf')]; + + let getPDFCallCount = 0; + mockGetPDFDocument.mockImplementation(() => { + getPDFCallCount++; + if (getPDFCallCount === 1) return createEncryptedTask(); + if (getPDFCallCount === 2) return createPasswordValidationTask(false); + return createPasswordValidationTask(true); + }); + + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([2]), + engine: 'cpdf', + }); + + const secondPromise = batchDecryptIfNeeded(files2); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + const input2 = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn2 = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + + mockGetPDFDocument.mockImplementation(() => + createPasswordValidationTask(true) + ); + + input2.value = 'new-pw'; + submitBtn2.click(); + + const result = await secondPromise; + expect(result).toHaveLength(1); + expect(result[0].name).toBe('second.pdf'); + }); +}); + +describe('batchDecryptIfNeeded with batch modal (multiple encrypted)', () => { + it('should show batch modal for multiple encrypted files', async () => { + const files = [createMockFile('enc1.pdf'), createMockFile('enc2.pdf')]; + + mockGetPDFDocument.mockImplementation(() => createEncryptedTask()); + + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([1, 2]), + engine: 'cpdf', + }); + + const modalPromise = batchDecryptIfNeeded(files); + + await vi.waitFor(() => { + const modal = document.getElementById('password-batch-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Batch modal not visible yet'); + } + }); + + mockGetPDFDocument.mockImplementation(() => + createPasswordValidationTask(true) + ); + + const sharedInput = document.getElementById( + 'batch-modal-shared-input' + ) as HTMLInputElement; + const submitBtn = document.getElementById( + 'batch-modal-submit' + ) as HTMLButtonElement; + sharedInput.value = 'shared-pw'; + submitBtn.click(); + + const result = await modalPromise; + expect(result).toHaveLength(2); + expect(result[0].name).toBe('enc1.pdf'); + expect(result[1].name).toBe('enc2.pdf'); + }); + + it('should skip all encrypted files when user cancels batch modal', async () => { + const files = [ + createMockFile('plain.pdf'), + createMockFile('enc1.pdf'), + createMockFile('enc2.pdf'), + ]; + + let callCount = 0; + mockGetPDFDocument.mockImplementation(() => { + callCount++; + if (callCount === 1) return createNonEncryptedTask(); + return createEncryptedTask(); + }); + + const modalPromise = batchDecryptIfNeeded(files); + + await vi.waitFor(() => { + const modal = document.getElementById('password-batch-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Batch modal not visible yet'); + } + }); + + const cancelBtn = document.getElementById( + 'batch-modal-cancel' + ) as HTMLButtonElement; + cancelBtn.click(); + + const result = await modalPromise; + expect(result).toHaveLength(1); + expect(result[0].name).toBe('plain.pdf'); + }); + + it('should use cached password for multiple encrypted files without showing modal', async () => { + const files1 = [createMockFile('setup.pdf')]; + + mockGetPDFDocument.mockImplementation(() => createEncryptedTask()); + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([1]), + engine: 'cpdf', + }); + + const firstPromise = batchDecryptIfNeeded(files1); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + mockGetPDFDocument.mockImplementation(() => + createPasswordValidationTask(true) + ); + + const input = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + input.value = 'shared-secret'; + submitBtn.click(); + + await firstPromise; + + const files2 = [createMockFile('enc-a.pdf'), createMockFile('enc-b.pdf')]; + + let callIdx = 0; + mockGetPDFDocument.mockImplementation(() => { + callIdx++; + if (callIdx <= 2) return createEncryptedTask(); + return createPasswordValidationTask(true); + }); + + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([10]), + engine: 'cpdf', + }); + + const result = await batchDecryptIfNeeded(files2); + expect(result).toHaveLength(2); + expect(result[0].name).toBe('enc-a.pdf'); + expect(result[1].name).toBe('enc-b.pdf'); + }); +}); + +describe('loadPdfWithPasswordPrompt', () => { + it('should return pdf, bytes, and file for non-encrypted file', async () => { + const file = createMockFile('test.pdf', 50); + const mockDoc = { destroy: vi.fn() }; + + mockGetPDFDocument.mockImplementation(() => ({ + promise: Promise.resolve(mockDoc), + onPassword: null, + destroy: mockDestroy(), + })); + + const result = await loadPdfWithPasswordPrompt(file); + expect(result).not.toBeNull(); + expect(result!.pdf).toBe(mockDoc); + expect(result!.file).toBe(file); + expect(result!.bytes).toBeInstanceOf(ArrayBuffer); + }); + + it('should prompt for encrypted file and return decrypted result', async () => { + const file = createMockFile('locked.pdf', 50); + + let firstCall = true; + mockGetPDFDocument.mockImplementation(() => { + if (firstCall) { + firstCall = false; + const err = new Error('Password required'); + (err as { name: string }).name = 'PasswordException'; + return { + promise: Promise.reject(err), + onPassword: null, + destroy: mockDestroy(), + }; + } + return createNonEncryptedTask(); + }); + + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([1, 2, 3]), + engine: 'cpdf', + }); + + const resultPromise = loadPdfWithPasswordPrompt(file); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + mockGetPDFDocument.mockImplementation(() => + createPasswordValidationTask(true) + ); + + const input = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + input.value = 'unlock'; + + mockGetPDFDocument.mockImplementation(() => createNonEncryptedTask()); + + submitBtn.click(); + + const result = await resultPromise; + expect(result).not.toBeNull(); + expect(result!.file.name).toBe('locked.pdf'); + }); + + it('should return null when user skips encrypted file', async () => { + const file = createMockFile('locked.pdf', 50); + + mockGetPDFDocument.mockImplementation(() => { + const err = new Error('Password required'); + (err as { name: string }).name = 'PasswordException'; + return { + promise: Promise.reject(err), + onPassword: null, + destroy: mockDestroy(), + }; + }); + + const resultPromise = loadPdfWithPasswordPrompt(file); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + const cancelBtn = document.getElementById( + 'password-modal-cancel' + ) as HTMLButtonElement; + cancelBtn.click(); + + const result = await resultPromise; + expect(result).toBeNull(); + }); + + it('should rethrow non-password errors', async () => { + const file = createMockFile('bad.pdf', 50); + + mockGetPDFDocument.mockImplementation(() => ({ + promise: Promise.reject(new Error('Corrupted file')), + onPassword: null, + destroy: mockDestroy(), + })); + + await expect(loadPdfWithPasswordPrompt(file)).rejects.toThrow( + 'Corrupted file' + ); + }); + + it('should update files array and index when provided', async () => { + const file = createMockFile('locked.pdf', 50); + const filesArr = [createMockFile('other.pdf'), file]; + + let firstCall = true; + mockGetPDFDocument.mockImplementation(() => { + if (firstCall) { + firstCall = false; + const err = new Error('Password required'); + (err as { name: string }).name = 'PasswordException'; + return { + promise: Promise.reject(err), + onPassword: null, + destroy: mockDestroy(), + }; + } + return createNonEncryptedTask(); + }); + + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([7, 8, 9]), + engine: 'cpdf', + }); + + const resultPromise = loadPdfWithPasswordPrompt(file, filesArr, 1); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + mockGetPDFDocument.mockImplementation(() => + createPasswordValidationTask(true) + ); + + const input = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + input.value = 'pw'; + + mockGetPDFDocument.mockImplementation(() => createNonEncryptedTask()); + + submitBtn.click(); + + const result = await resultPromise; + expect(result).not.toBeNull(); + expect(filesArr[1].name).toBe('locked.pdf'); + expect(filesArr[1]).not.toBe(file); + }); +}); + +describe('promptAndDecryptFile', () => { + it('should show modal and decrypt file on valid password', async () => { + const file = createMockFile('test.pdf', 50); + + mockGetPDFDocument.mockImplementation(() => + createPasswordValidationTask(true) + ); + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([1, 2, 3]), + engine: 'cpdf', + }); + + const resultPromise = promptAndDecryptFile(file); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + const input = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + input.value = 'correct'; + submitBtn.click(); + + const result = await resultPromise; + expect(result).not.toBeNull(); + expect(result!.name).toBe('test.pdf'); + expect(result!.type).toBe('application/pdf'); + }); + + it('should return null when user clicks cancel', async () => { + const file = createMockFile('test.pdf'); + + const resultPromise = promptAndDecryptFile(file); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + const cancelBtn = document.getElementById( + 'password-modal-cancel' + ) as HTMLButtonElement; + cancelBtn.click(); + + const result = await resultPromise; + expect(result).toBeNull(); + }); + + it('should show error when submitting empty password', async () => { + const file = createMockFile('test.pdf'); + + promptAndDecryptFile(file); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + const input = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + const errorEl = document.getElementById( + 'password-modal-error' + ) as HTMLParagraphElement; + + input.value = ''; + submitBtn.click(); + + expect(errorEl.textContent).toBe('Please enter a password'); + expect(errorEl.classList.contains('hidden')).toBe(false); + + const cancelBtn = document.getElementById( + 'password-modal-cancel' + ) as HTMLButtonElement; + cancelBtn.click(); + }); + + it('should show error for incorrect password and allow retry', async () => { + const file = createMockFile('test.pdf'); + + let callCount = 0; + mockGetPDFDocument.mockImplementation(() => { + callCount++; + if (callCount <= 1) return createPasswordValidationTask(false); + return createPasswordValidationTask(true); + }); + + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([1]), + engine: 'cpdf', + }); + + const resultPromise = promptAndDecryptFile(file); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + const input = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + + input.value = 'wrong'; + submitBtn.click(); + + await vi.waitFor(() => { + const errorEl = document.getElementById('password-modal-error'); + if (!errorEl || errorEl.classList.contains('hidden')) { + throw new Error('Error not visible yet'); + } + }); + + const errorEl = document.getElementById( + 'password-modal-error' + ) as HTMLParagraphElement; + expect(errorEl.textContent).toBe('Incorrect password. Please try again.'); + + input.value = 'correct'; + submitBtn.click(); + + const result = await resultPromise; + expect(result).not.toBeNull(); + expect(result!.name).toBe('test.pdf'); + }); + + it('should handle decryptPdfBytes throwing error', async () => { + const file = createMockFile('test.pdf'); + + mockGetPDFDocument.mockImplementation(() => + createPasswordValidationTask(true) + ); + mockDecryptPdfBytes.mockRejectedValue(new Error('Decryption failed')); + + const resultPromise = promptAndDecryptFile(file); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + const input = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + input.value = 'pass'; + submitBtn.click(); + + await vi.waitFor(() => { + const errorEl = document.getElementById('password-modal-error'); + if (!errorEl || errorEl.classList.contains('hidden')) { + throw new Error('Error not visible yet'); + } + }); + + const errorEl = document.getElementById( + 'password-modal-error' + ) as HTMLParagraphElement; + expect(errorEl.textContent).toBe( + 'Failed to decrypt. Try the Decrypt tool instead.' + ); + + const cancelBtn = document.getElementById( + 'password-modal-cancel' + ) as HTMLButtonElement; + cancelBtn.click(); + + const result = await resultPromise; + expect(result).toBeNull(); + }); + + it('should use cached password without showing modal', async () => { + const file1 = createMockFile('first.pdf'); + + mockGetPDFDocument.mockImplementation(() => + createPasswordValidationTask(true) + ); + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([1]), + engine: 'cpdf', + }); + + const firstPromise = promptAndDecryptFile(file1); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + const input = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + input.value = 'reuse-me'; + submitBtn.click(); + + await firstPromise; + + const file2 = createMockFile('second.pdf'); + + mockGetPDFDocument.mockImplementation(() => + createPasswordValidationTask(true) + ); + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([2]), + engine: 'cpdf', + }); + + const result = await promptAndDecryptFile(file2); + expect(result).not.toBeNull(); + expect(result!.name).toBe('second.pdf'); + }); + + it('should clear cache and show modal when cached password is invalid', async () => { + const file1 = createMockFile('first.pdf'); + + mockGetPDFDocument.mockImplementation(() => + createPasswordValidationTask(true) + ); + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([1]), + engine: 'cpdf', + }); + + const firstPromise = promptAndDecryptFile(file1); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + const input = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + input.value = 'old-pass'; + submitBtn.click(); + + await firstPromise; + + const file2 = createMockFile('second.pdf'); + + let validationCallCount = 0; + mockGetPDFDocument.mockImplementation(() => { + validationCallCount++; + if (validationCallCount === 1) return createPasswordValidationTask(false); + return createPasswordValidationTask(true); + }); + + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([2]), + engine: 'cpdf', + }); + + const secondPromise = promptAndDecryptFile(file2); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + const input2 = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn2 = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + input2.value = 'new-pass'; + submitBtn2.click(); + + const result = await secondPromise; + expect(result).not.toBeNull(); + expect(result!.name).toBe('second.pdf'); + }); + + it('should clear cache when decryptPdfBytes throws with cached password', async () => { + const file1 = createMockFile('first.pdf'); + + mockGetPDFDocument.mockImplementation(() => + createPasswordValidationTask(true) + ); + mockDecryptPdfBytes.mockResolvedValueOnce({ + bytes: new Uint8Array([1]), + engine: 'cpdf', + }); + + const firstPromise = promptAndDecryptFile(file1); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + const input = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + input.value = 'will-fail-later'; + submitBtn.click(); + + await firstPromise; + + const file2 = createMockFile('second.pdf'); + + mockGetPDFDocument.mockImplementation(() => + createPasswordValidationTask(true) + ); + mockDecryptPdfBytes.mockRejectedValueOnce(new Error('Decrypt fail')); + mockDecryptPdfBytes.mockResolvedValueOnce({ + bytes: new Uint8Array([2]), + engine: 'cpdf', + }); + + const secondPromise = promptAndDecryptFile(file2); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + const input2 = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn2 = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + input2.value = 'new-pw'; + submitBtn2.click(); + + const result = await secondPromise; + expect(result).not.toBeNull(); + expect(result!.name).toBe('second.pdf'); + }); +}); + +describe('handleEncryptedFiles', () => { + it('should return empty map when no encrypted indices', async () => { + const files = [createMockFile('a.pdf')]; + const result = await handleEncryptedFiles(files, []); + expect(result.size).toBe(0); + }); + + it('should delegate to promptAndDecryptBatch for single encrypted file', async () => { + const files = [createMockFile('enc.pdf')]; + + mockGetPDFDocument.mockImplementation(() => + createPasswordValidationTask(true) + ); + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([1]), + engine: 'cpdf', + }); + + const resultPromise = handleEncryptedFiles(files, [0]); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + const input = document.getElementById( + 'password-modal-input' + ) as HTMLInputElement; + const submitBtn = document.getElementById( + 'password-modal-submit' + ) as HTMLButtonElement; + input.value = 'pass'; + submitBtn.click(); + + const result = await resultPromise; + expect(result.size).toBe(1); + expect(result.has(0)).toBe(true); + expect(result.get(0)!.name).toBe('enc.pdf'); + }); + + it('should delegate to promptAndDecryptBatch for multiple encrypted files', async () => { + const files = [ + createMockFile('plain.pdf'), + createMockFile('enc1.pdf'), + createMockFile('enc2.pdf'), + ]; + + mockGetPDFDocument.mockImplementation(() => + createPasswordValidationTask(true) + ); + mockDecryptPdfBytes.mockResolvedValue({ + bytes: new Uint8Array([1]), + engine: 'cpdf', + }); + + const resultPromise = handleEncryptedFiles(files, [1, 2]); + + await vi.waitFor(() => { + const modal = document.getElementById('password-batch-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Batch modal not visible yet'); + } + }); + + const sharedInput = document.getElementById( + 'batch-modal-shared-input' + ) as HTMLInputElement; + const submitBtn = document.getElementById( + 'batch-modal-submit' + ) as HTMLButtonElement; + sharedInput.value = 'shared'; + submitBtn.click(); + + const result = await resultPromise; + expect(result.size).toBe(2); + expect(result.has(1)).toBe(true); + expect(result.has(2)).toBe(true); + }); + + it('should return empty map when user skips all files', async () => { + const files = [createMockFile('enc.pdf')]; + + const resultPromise = handleEncryptedFiles(files, [0]); + + await vi.waitFor(() => { + const modal = document.getElementById('password-modal'); + if (!modal || modal.classList.contains('hidden')) { + throw new Error('Modal not visible yet'); + } + }); + + const cancelBtn = document.getElementById( + 'password-modal-cancel' + ) as HTMLButtonElement; + cancelBtn.click(); + + const result = await resultPromise; + expect(result.size).toBe(0); + }); +});