@@ -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 = `
+
+
+
+
+
+
+
`;
+ 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