Add password prompt functionality and tests while uploading encrypted PDF

This commit is contained in:
alam00000
2026-03-26 12:11:12 +05:30
parent f88f872162
commit 1dbf907eeb
79 changed files with 7869 additions and 4419 deletions

View File

@@ -863,6 +863,73 @@
</div> </div>
</div> </div>
<div
id="password-modal"
class="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 hidden items-center justify-center p-4"
>
<div
class="bg-gray-800 rounded-xl border border-gray-700 shadow-2xl max-w-md w-full overflow-hidden"
>
<div class="p-6">
<div class="flex items-start gap-4 mb-4">
<div
class="w-12 h-12 flex items-center justify-center rounded-full bg-indigo-500/10 flex-shrink-0"
>
<i data-lucide="lock" class="w-6 h-6 text-indigo-400"></i>
</div>
<div class="flex-1">
<h3
id="password-modal-title"
class="text-xl font-bold text-white mb-1"
>
Password Required
</h3>
<p
id="password-modal-filename"
class="text-gray-400 text-sm truncate"
></p>
</div>
</div>
<div class="mt-4">
<div class="relative">
<input
type="password"
id="password-modal-input"
class="w-full bg-gray-700 border border-gray-600 text-gray-200 rounded-lg px-4 py-2.5 pr-10 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
placeholder="Enter password"
autocomplete="off"
/>
<button
id="password-modal-toggle"
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-200"
>
<i data-lucide="eye" class="w-4 h-4"></i>
</button>
</div>
<p
id="password-modal-error"
class="text-xs text-red-400 mt-2 hidden"
></p>
</div>
</div>
<div class="flex gap-3 p-4 border-t border-gray-700">
<button
id="password-modal-cancel"
class="flex-1 px-4 py-2.5 bg-gray-700 hover:bg-gray-600 text-white rounded-lg font-medium transition-colors"
>
Skip
</button>
<button
id="password-modal-submit"
class="flex-1 px-4 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg font-medium transition-colors"
>
Unlock
</button>
</div>
</div>
</div>
<div class="section-divider hide-section mb-20 mt-10"></div> <div class="section-divider hide-section mb-20 mt-10"></div>
<!-- COMPLIANCE SECTION START --> <!-- COMPLIANCE SECTION START -->

View File

@@ -19,6 +19,10 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { makeUniqueFileKey } from '../utils/deduplicate-filename.js'; import { makeUniqueFileKey } from '../utils/deduplicate-filename.js';
import {
promptAndDecryptFile,
handleEncryptedFiles,
} from '../utils/password-prompt.js';
import { import {
multiFileTools, multiFileTools,
simpleTools, simpleTools,
@@ -31,7 +35,6 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
import.meta.url import.meta.url
).toString(); ).toString();
// Re-export rotation state utilities
export { export {
getRotationState, getRotationState,
updateRotationState, updateRotationState,
@@ -43,10 +46,9 @@ const rotationState: number[] = [];
let imageSortableInstance: Sortable | null = null; let imageSortableInstance: Sortable | null = null;
const activeImageUrls = new Map<File, string>(); const activeImageUrls = new Map<File, string>();
async function handleSinglePdfUpload(toolId, file) { async function handleSinglePdfUpload(toolId: string, file: File) {
showLoader('Loading PDF...'); showLoader('Loading PDF...');
try { try {
// For form-filler, bypass pdf-lib (can't handle XFA) and use PDF.js
if (toolId === 'form-filler') { if (toolId === 'form-filler') {
hideLoader(); hideLoader();
@@ -80,13 +82,15 @@ async function handleSinglePdfUpload(toolId, file) {
toolId !== 'change-permissions' && toolId !== 'change-permissions' &&
toolId !== 'remove-restrictions' toolId !== 'remove-restrictions'
) { ) {
showAlert( const decryptedFile = await promptAndDecryptFile(file);
'Protected PDF', if (!decryptedFile) {
'This PDF is password-protected. Please use the Decrypt or Change Permissions tool first.'
);
switchView('grid'); switchView('grid');
return; return;
} }
const decryptedBytes = await readFileAsArrayBuffer(decryptedFile);
state.pdfDoc = await PDFLibDocument.load(decryptedBytes as ArrayBuffer);
state.files = [decryptedFile];
}
const optionsDiv = document.querySelector( const optionsDiv = document.querySelector(
'[id$="-options"], [id$="-preview"], [id$="-organizer"], [id$="-rotator"], [id$="-editor"]' '[id$="-options"], [id$="-preview"], [id$="-organizer"], [id$="-rotator"], [id$="-editor"]'
@@ -127,7 +131,6 @@ async function handleSinglePdfUpload(toolId, file) {
await renderPageThumbnails(toolId, state.pdfDoc); await renderPageThumbnails(toolId, state.pdfDoc);
if (toolId === 'rotate') { if (toolId === 'rotate') {
// Initialize rotation state for all pages
rotationState.length = 0; rotationState.length = 0;
for (let i = 0; i < state.pdfDoc.getPageCount(); i++) { for (let i = 0; i < state.pdfDoc.getPageCount(); i++) {
rotationState.push(0); rotationState.push(0);
@@ -157,12 +160,10 @@ async function handleSinglePdfUpload(toolId, file) {
createIcons({ icons }); createIcons({ icons });
const rotateAll = (angle: number) => { const rotateAll = (angle: number) => {
// Update rotation state for ALL pages (including unrendered ones)
for (let i = 0; i < rotationState.length; i++) { for (let i = 0; i < rotationState.length; i++) {
rotationState[i] = rotationState[i] + angle; rotationState[i] = rotationState[i] + angle;
} }
// Update DOM for currently rendered pages
document.querySelectorAll('.page-rotator-item').forEach((item) => { document.querySelectorAll('.page-rotator-item').forEach((item) => {
const pageIndex = parseInt( const pageIndex = parseInt(
(item as HTMLElement).dataset.pageIndex || '0' (item as HTMLElement).dataset.pageIndex || '0'
@@ -236,7 +237,7 @@ async function handleSinglePdfUpload(toolId, file) {
resultsDiv.textContent = ''; // Clear safely resultsDiv.textContent = ''; // Clear safely
const createSection = (title) => { const createSection = (title: string) => {
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.className = 'mb-4'; wrapper.className = 'mb-4';
const h3 = document.createElement('h3'); const h3 = document.createElement('h3');
@@ -249,7 +250,7 @@ async function handleSinglePdfUpload(toolId, file) {
return { wrapper, ul }; return { wrapper, ul };
}; };
const createListItem = (key, value) => { const createListItem = (key: string, value: string) => {
const li = document.createElement('li'); const li = document.createElement('li');
li.className = 'flex flex-col sm:flex-row'; li.className = 'flex flex-col sm:flex-row';
const strong = document.createElement('strong'); const strong = document.createElement('strong');
@@ -262,13 +263,8 @@ async function handleSinglePdfUpload(toolId, file) {
return li; return li;
}; };
const parsePdfDate = (pdfDate) => { const parsePdfDate = (pdfDate: string): string => {
if ( if (!pdfDate || !pdfDate.startsWith('D:')) return pdfDate;
!pdfDate ||
typeof pdfDate !== 'string' ||
!pdfDate.startsWith('D:')
)
return pdfDate;
try { try {
const year = pdfDate.substring(2, 6); const year = pdfDate.substring(2, 6);
const month = pdfDate.substring(6, 8); const month = pdfDate.substring(6, 8);
@@ -319,8 +315,8 @@ async function handleSinglePdfUpload(toolId, file) {
const fieldsSection = createSection('Interactive Form Fields'); const fieldsSection = createSection('Interactive Form Fields');
if (fieldObjects && Object.keys(fieldObjects).length > 0) { if (fieldObjects && Object.keys(fieldObjects).length > 0) {
for (const fieldName in fieldObjects) { for (const fieldName in fieldObjects) {
const field = fieldObjects[fieldName][0]; const field = fieldObjects[fieldName][0] as Record<string, unknown>;
const value = (field as any).fieldValue || '- Not Set -'; const value = field.fieldValue || '- Not Set -';
fieldsSection.ul.appendChild( fieldsSection.ul.appendChild(
createListItem(fieldName, String(value)) createListItem(fieldName, String(value))
); );
@@ -330,7 +326,7 @@ async function handleSinglePdfUpload(toolId, file) {
} }
resultsDiv.appendChild(fieldsSection.wrapper); resultsDiv.appendChild(fieldsSection.wrapper);
const createXmpListItem = (key, value, indent = 0) => { const createXmpListItem = (key: string, value: string, indent = 0) => {
const li = document.createElement('li'); const li = document.createElement('li');
li.className = 'flex flex-col sm:flex-row'; li.className = 'flex flex-col sm:flex-row';
@@ -347,7 +343,7 @@ async function handleSinglePdfUpload(toolId, file) {
return li; return li;
}; };
const createXmpHeaderItem = (key, indent = 0) => { const createXmpHeaderItem = (key: string, indent = 0) => {
const li = document.createElement('li'); const li = document.createElement('li');
li.className = 'flex pt-2'; li.className = 'flex pt-2';
const strong = document.createElement('strong'); const strong = document.createElement('strong');
@@ -358,7 +354,11 @@ async function handleSinglePdfUpload(toolId, file) {
return li; return li;
}; };
const appendXmpNodes = (xmlNode, ulElement, indentLevel) => { const appendXmpNodes = (
xmlNode: Element,
ulElement: HTMLUListElement,
indentLevel: number
) => {
const xmpDateKeys = [ const xmpDateKeys = [
'xap:CreateDate', 'xap:CreateDate',
'xap:ModifyDate', 'xap:ModifyDate',
@@ -368,12 +368,12 @@ async function handleSinglePdfUpload(toolId, file) {
const childNodes = Array.from(xmlNode.children); const childNodes = Array.from(xmlNode.children);
for (const child of childNodes) { for (const child of childNodes) {
if ((child as Element).nodeType !== 1) continue; if (child.nodeType !== 1) continue;
let key = (child as Element).tagName; let key = child.tagName;
const elementChildren = Array.from( const elementChildren = Array.from(child.children).filter(
(child as Element).children (c) => c.nodeType === 1
).filter((c) => c.nodeType === 1); );
if (key === 'rdf:li') { if (key === 'rdf:li') {
appendXmpNodes(child, ulElement, indentLevel); appendXmpNodes(child, ulElement, indentLevel);
@@ -384,7 +384,7 @@ async function handleSinglePdfUpload(toolId, file) {
} }
if ( if (
(child as Element).getAttribute('rdf:parseType') === 'Resource' && child.getAttribute('rdf:parseType') === 'Resource' &&
elementChildren.length === 0 elementChildren.length === 0
) { ) {
ulElement.appendChild( ulElement.appendChild(
@@ -397,7 +397,7 @@ async function handleSinglePdfUpload(toolId, file) {
ulElement.appendChild(createXmpHeaderItem(key, indentLevel)); ulElement.appendChild(createXmpHeaderItem(key, indentLevel));
appendXmpNodes(child, ulElement, indentLevel + 1); appendXmpNodes(child, ulElement, indentLevel + 1);
} else { } else {
let value = (child as Element).textContent.trim(); let value = (child.textContent ?? '').trim();
if (value) { if (value) {
if (xmpDateKeys.includes(key)) { if (xmpDateKeys.includes(key)) {
value = formatIsoDate(value); value = formatIsoDate(value);
@@ -462,9 +462,9 @@ async function handleSinglePdfUpload(toolId, file) {
const container = document.getElementById('custom-metadata-container'); const container = document.getElementById('custom-metadata-container');
const addBtn = document.getElementById('add-custom-meta-btn'); const addBtn = document.getElementById('add-custom-meta-btn');
const formatDateForInput = (date) => { const formatDateForInput = (date: Date | undefined) => {
if (!date) return ''; if (!date) return '';
const pad = (num) => num.toString().padStart(2, '0'); const pad = (num: number) => num.toString().padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}; };
@@ -529,7 +529,6 @@ async function handleSinglePdfUpload(toolId, file) {
toolLogic['page-dimensions'](); toolLogic['page-dimensions']();
} }
// Setup quality sliders for image conversion tools
if (toolId === 'pdf-to-jpg') { if (toolId === 'pdf-to-jpg') {
const qualitySlider = document.getElementById( const qualitySlider = document.getElementById(
'jpg-quality' 'jpg-quality'
@@ -585,7 +584,7 @@ async function handleSinglePdfUpload(toolId, file) {
} }
} }
async function handleMultiFileUpload(toolId) { async function handleMultiFileUpload(toolId: string) {
if ( if (
toolId === 'merge' || toolId === 'merge' ||
toolId === 'alternate-merge' || toolId === 'alternate-merge' ||
@@ -615,25 +614,45 @@ async function handleMultiFileUpload(toolId) {
}) })
); );
const foundEncryptedPDFs = pdfFilesLoaded.filter( const encryptedIndices: number[] = [];
(pdf) => pdf.pdfDoc.isEncrypted pdfFilesLoaded.forEach((pdf, index) => {
); if (pdf.pdfDoc.isEncrypted) {
encryptedIndices.push(index);
if (foundEncryptedPDFs.length > 0) { }
const encryptedPDFFileNames = [];
foundEncryptedPDFs.forEach((encryptedPDF) => {
encryptedPDFFileNames.push(encryptedPDF.file.name);
}); });
const errorMessage = `PDFs found that are password-protected\n\nPlease use the Decrypt or Change Permissions tool on these files first:\n\n${encryptedPDFFileNames.join('\n')}`; if (encryptedIndices.length > 0) {
hideLoader();
const decryptedFiles = await handleEncryptedFiles(
pdfFilesUnloaded,
encryptedIndices
);
hideLoader(); // Hide loader before showing alert for (const [index, decryptedFile] of decryptedFiles) {
showAlert('Protected PDFs', errorMessage); const originalIndex = state.files.indexOf(pdfFilesUnloaded[index]);
if (originalIndex !== -1) {
state.files[originalIndex] = decryptedFile;
}
}
const skippedFiles = new Set(
encryptedIndices
.filter((i) => !decryptedFiles.has(i))
.map((i) => pdfFilesUnloaded[i])
);
if (skippedFiles.size > 0) {
state.files = state.files.filter((f) => !skippedFiles.has(f));
}
if (
state.files.filter((f) => f.type === 'application/pdf').length === 0
) {
switchView('grid'); switchView('grid');
return; return;
} }
showLoader('Loading PDF documents...');
}
} }
const processBtn = document.getElementById('process-btn'); const processBtn = document.getElementById('process-btn');
@@ -646,10 +665,6 @@ async function handleMultiFileUpload(toolId) {
} }
} }
// if (toolId === 'merge') {
// toolLogic.merge.setup();
// }
if (toolId === 'alternate-merge') { if (toolId === 'alternate-merge') {
toolLogic['alternate-merge'].setup(); toolLogic['alternate-merge'].setup();
} else if (toolId === 'image-to-pdf') { } else if (toolId === 'image-to-pdf') {
@@ -791,12 +806,12 @@ async function handleMultiFileUpload(toolId) {
} }
} }
export function setupFileInputHandler(toolId) { export function setupFileInputHandler(toolId: string) {
const fileInput = document.getElementById('file-input'); const fileInput = document.getElementById('file-input');
const isMultiFileTool = multiFileTools.includes(toolId); const isMultiFileTool = multiFileTools.includes(toolId);
let isFirstUpload = true; let isFirstUpload = true;
const processFiles = async (newFiles) => { const processFiles = async (newFiles: File[]) => {
if (newFiles.length === 0) return; if (newFiles.length === 0) return;
if (toolId === 'image-to-pdf') { if (toolId === 'image-to-pdf') {

View File

@@ -8,6 +8,7 @@ import {
showWasmRequiredDialog, showWasmRequiredDialog,
WasmProvider, WasmProvider,
} from '../utils/wasm-provider.js'; } from '../utils/wasm-provider.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
const worker = new Worker( const worker = new Worker(
import.meta.env.BASE_URL + 'workers/add-attachments.worker.js' import.meta.env.BASE_URL + 'workers/add-attachments.worker.js'
@@ -129,13 +130,16 @@ async function updateUI() {
createIcons({ icons }); createIcons({ icons });
try { try {
const result = await loadPdfWithPasswordPrompt(pageState.file);
if (!result) {
resetState();
return;
}
result.pdf.destroy();
pageState.file = result.file;
showLoader('Loading PDF...'); showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, { pageState.pdfDoc = await PDFLibDocument.load(result.bytes);
ignoreEncryption: true,
throwOnInvalidObject: false,
});
const pageCount = pageState.pdfDoc.getPageCount(); const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`; metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
@@ -269,10 +273,13 @@ async function addAttachments() {
const transferables = [pdfBuffer, ...attachmentBuffers]; const transferables = [pdfBuffer, ...attachmentBuffers];
worker.postMessage(message, transferables); worker.postMessage(message, transferables);
} catch (error: any) { } catch (error) {
console.error('Error attaching files:', error); console.error('Error attaching files:', error);
hideLoader(); hideLoader();
showAlert('Error', `Failed to attach files: ${error.message}`); showAlert(
'Error',
`Failed to attach files: ${error instanceof Error ? error.message : 'Unknown error'}`
);
} }
} }

View File

@@ -2,9 +2,9 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js'; import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import { AddBlankPageState } from '@/types'; import { AddBlankPageState } from '@/types';
const pageState: AddBlankPageState = { const pageState: AddBlankPageState = {
file: null, file: null,
pdfDoc: null, pdfDoc: null,
@@ -23,10 +23,14 @@ function resetState() {
const fileInput = document.getElementById('file-input') as HTMLInputElement; const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = ''; if (fileInput) fileInput.value = '';
const pagePositionInput = document.getElementById('page-position') as HTMLInputElement; const pagePositionInput = document.getElementById(
'page-position'
) as HTMLInputElement;
if (pagePositionInput) pagePositionInput.value = '0'; if (pagePositionInput) pagePositionInput.value = '0';
const pageCountInput = document.getElementById('page-count') as HTMLInputElement; const pageCountInput = document.getElementById(
'page-count'
) as HTMLInputElement;
if (pageCountInput) pageCountInput.value = '1'; if (pageCountInput) pageCountInput.value = '1';
} }
@@ -34,7 +38,9 @@ async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area'); const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options'); const toolOptions = document.getElementById('tool-options');
const pagePositionHint = document.getElementById('page-position-hint'); const pagePositionHint = document.getElementById('page-position-hint');
const pagePositionInput = document.getElementById('page-position') as HTMLInputElement; const pagePositionInput = document.getElementById(
'page-position'
) as HTMLInputElement;
if (!fileDisplayArea) return; if (!fileDisplayArea) return;
@@ -42,7 +48,8 @@ async function updateUI() {
if (pageState.file) { if (pageState.file) {
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden'; infoContainer.className = 'flex flex-col overflow-hidden';
@@ -70,12 +77,17 @@ async function updateUI() {
// Load PDF document // Load PDF document
try { try {
const result = await loadPdfWithPasswordPrompt(pageState.file);
if (!result) {
resetState();
return;
}
showLoader('Loading PDF...'); showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer(); pageState.file = result.file;
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, { pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
ignoreEncryption: true, throwOnInvalidObject: false,
throwOnInvalidObject: false
}); });
result.pdf.destroy();
hideLoader(); hideLoader();
const pageCount = pageState.pdfDoc.getPageCount(); const pageCount = pageState.pdfDoc.getPageCount();
@@ -106,36 +118,52 @@ async function addBlankPages() {
return; return;
} }
const pagePositionInput = document.getElementById('page-position') as HTMLInputElement; const pagePositionInput = document.getElementById(
const pageCountInput = document.getElementById('page-count') as HTMLInputElement; 'page-position'
) as HTMLInputElement;
const pageCountInput = document.getElementById(
'page-count'
) as HTMLInputElement;
const position = parseInt(pagePositionInput.value); const position = parseInt(pagePositionInput.value);
const insertCount = parseInt(pageCountInput.value); const insertCount = parseInt(pageCountInput.value);
const totalPages = pageState.pdfDoc.getPageCount(); const totalPages = pageState.pdfDoc.getPageCount();
if (isNaN(position) || position < 0 || position > totalPages) { if (isNaN(position) || position < 0 || position > totalPages) {
showAlert('Invalid Input', `Please enter a number between 0 and ${totalPages}.`); showAlert(
'Invalid Input',
`Please enter a number between 0 and ${totalPages}.`
);
return; return;
} }
if (isNaN(insertCount) || insertCount < 1) { if (isNaN(insertCount) || insertCount < 1) {
showAlert('Invalid Input', 'Please enter a valid number of pages (1 or more).'); showAlert(
'Invalid Input',
'Please enter a valid number of pages (1 or more).'
);
return; return;
} }
showLoader(`Adding ${insertCount} blank page${insertCount > 1 ? 's' : ''}...`); showLoader(
`Adding ${insertCount} blank page${insertCount > 1 ? 's' : ''}...`
);
try { try {
const newPdf = await PDFLibDocument.create(); const newPdf = await PDFLibDocument.create();
const { width, height } = pageState.pdfDoc.getPage(0).getSize(); const { width, height } = pageState.pdfDoc.getPage(0).getSize();
const allIndices = Array.from({ length: totalPages }, function (_, i) { return i; }); const allIndices = Array.from({ length: totalPages }, function (_, i) {
return i;
});
const indicesBefore = allIndices.slice(0, position); const indicesBefore = allIndices.slice(0, position);
const indicesAfter = allIndices.slice(position); const indicesAfter = allIndices.slice(position);
if (indicesBefore.length > 0) { if (indicesBefore.length > 0) {
const copied = await newPdf.copyPages(pageState.pdfDoc, indicesBefore); const copied = await newPdf.copyPages(pageState.pdfDoc, indicesBefore);
copied.forEach(function (p) { newPdf.addPage(p); }); copied.forEach(function (p) {
newPdf.addPage(p);
});
} }
// Add the specified number of blank pages // Add the specified number of blank pages
@@ -145,7 +173,9 @@ async function addBlankPages() {
if (indicesAfter.length > 0) { if (indicesAfter.length > 0) {
const copied = await newPdf.copyPages(pageState.pdfDoc, indicesAfter); const copied = await newPdf.copyPages(pageState.pdfDoc, indicesAfter);
copied.forEach(function (p) { newPdf.addPage(p); }); copied.forEach(function (p) {
newPdf.addPage(p);
});
} }
const newPdfBytes = await newPdf.save(); const newPdfBytes = await newPdf.save();
@@ -156,12 +186,20 @@ async function addBlankPages() {
`${originalName}_blank-pages-added.pdf` `${originalName}_blank-pages-added.pdf`
); );
showAlert('Success', `Added ${insertCount} blank page${insertCount > 1 ? 's' : ''} successfully!`, 'success', function () { showAlert(
'Success',
`Added ${insertCount} blank page${insertCount > 1 ? 's' : ''} successfully!`,
'success',
function () {
resetState(); resetState();
}); }
);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showAlert('Error', `Could not add blank page${insertCount > 1 ? 's' : ''}.`); showAlert(
'Error',
`Could not add blank page${insertCount > 1 ? 's' : ''}.`
);
} finally { } finally {
hideLoader(); hideLoader();
} }
@@ -170,7 +208,10 @@ async function addBlankPages() {
function handleFileSelect(files: FileList | null) { function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) { if (files && files.length > 0) {
const file = files[0]; const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file; pageState.file = file;
updateUI(); updateUI();
} }
@@ -210,7 +251,10 @@ document.addEventListener('DOMContentLoaded', function () {
const files = e.dataTransfer?.files; const files = e.dataTransfer?.files;
if (files && files.length > 0) { if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) { const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'); return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
}); });
if (pdfFiles.length > 0) { if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer(); const dataTransfer = new DataTransfer();

View File

@@ -1,290 +1,342 @@
import { formatBytes, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers' import {
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js' formatBytes,
import { createIcons, icons } from 'lucide' readFileAsArrayBuffer,
getPDFDocument,
} from '../utils/helpers';
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
import { createIcons, icons } from 'lucide';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
let selectedFile: File | null = null let selectedFile: File | null = null;
let viewerIframe: HTMLIFrameElement | null = null let viewerIframe: HTMLIFrameElement | null = null;
let viewerReady = false let viewerReady = false;
let currentBlobUrl: string | null = null let currentBlobUrl: string | null = null;
const pdfInput = document.getElementById('pdfFile') as HTMLInputElement const pdfInput = document.getElementById('pdfFile') as HTMLInputElement;
const fileListDiv = document.getElementById('fileList') as HTMLDivElement const fileListDiv = document.getElementById('fileList') as HTMLDivElement;
const viewerContainer = document.getElementById('stamp-viewer-container') as HTMLDivElement const viewerContainer = document.getElementById(
const viewerCard = document.getElementById('viewer-card') as HTMLDivElement | null 'stamp-viewer-container'
const saveStampedBtn = document.getElementById('save-stamped-btn') as HTMLButtonElement ) as HTMLDivElement;
const backToToolsBtn = document.getElementById('back-to-tools') as HTMLButtonElement | null const viewerCard = document.getElementById(
const toolUploader = document.getElementById('tool-uploader') as HTMLDivElement | null 'viewer-card'
const usernameInput = document.getElementById('stamp-username') as HTMLInputElement | null ) as HTMLDivElement | null;
const saveStampedBtn = document.getElementById(
'save-stamped-btn'
) as HTMLButtonElement;
const backToToolsBtn = document.getElementById(
'back-to-tools'
) as HTMLButtonElement | null;
const toolUploader = document.getElementById(
'tool-uploader'
) as HTMLDivElement | null;
const usernameInput = document.getElementById(
'stamp-username'
) as HTMLInputElement | null;
function resetState() { function resetState() {
selectedFile = null selectedFile = null;
if (currentBlobUrl) { if (currentBlobUrl) {
URL.revokeObjectURL(currentBlobUrl) URL.revokeObjectURL(currentBlobUrl);
currentBlobUrl = null currentBlobUrl = null;
} }
if (viewerIframe && viewerContainer && viewerIframe.parentElement === viewerContainer) { if (
viewerContainer.removeChild(viewerIframe) viewerIframe &&
viewerContainer &&
viewerIframe.parentElement === viewerContainer
) {
viewerContainer.removeChild(viewerIframe);
} }
viewerIframe = null viewerIframe = null;
viewerReady = false viewerReady = false;
if (viewerCard) viewerCard.classList.add('hidden') if (viewerCard) viewerCard.classList.add('hidden');
if (saveStampedBtn) saveStampedBtn.classList.add('hidden') if (saveStampedBtn) saveStampedBtn.classList.add('hidden');
if (viewerContainer) { if (viewerContainer) {
viewerContainer.style.height = '' viewerContainer.style.height = '';
viewerContainer.style.aspectRatio = '' viewerContainer.style.aspectRatio = '';
} }
const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false' const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
if (toolUploader && !isFullWidth) { if (toolUploader && !isFullWidth) {
toolUploader.classList.remove('max-w-6xl') toolUploader.classList.remove('max-w-6xl');
toolUploader.classList.add('max-w-2xl') toolUploader.classList.add('max-w-2xl');
} }
updateFileList() updateFileList();
if (pdfInput) pdfInput.value = '' if (pdfInput) pdfInput.value = '';
} }
function updateFileList() { function updateFileList() {
if (!selectedFile) { if (!selectedFile) {
fileListDiv.classList.add('hidden') fileListDiv.classList.add('hidden');
fileListDiv.innerHTML = '' fileListDiv.innerHTML = '';
return return;
} }
fileListDiv.classList.remove('hidden') fileListDiv.classList.remove('hidden');
fileListDiv.innerHTML = '' fileListDiv.innerHTML = '';
// Expand container width for viewer if NOT in full width mode (default to true if not set) // Expand container width for viewer if NOT in full width mode (default to true if not set)
const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false' const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
if (toolUploader && !isFullWidth) { if (toolUploader && !isFullWidth) {
toolUploader.classList.remove('max-w-2xl') toolUploader.classList.remove('max-w-2xl');
toolUploader.classList.add('max-w-6xl') toolUploader.classList.add('max-w-6xl');
} }
const wrapper = document.createElement('div') const wrapper = document.createElement('div');
wrapper.className = 'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors' wrapper.className =
'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors';
const innerDiv = document.createElement('div') const innerDiv = document.createElement('div');
innerDiv.className = 'flex items-center justify-between' innerDiv.className = 'flex items-center justify-between';
const infoDiv = document.createElement('div') const infoDiv = document.createElement('div');
infoDiv.className = 'flex-1 min-w-0' infoDiv.className = 'flex-1 min-w-0';
const nameSpan = document.createElement('p') const nameSpan = document.createElement('p');
nameSpan.className = 'truncate font-medium text-white' nameSpan.className = 'truncate font-medium text-white';
nameSpan.textContent = selectedFile.name nameSpan.textContent = selectedFile.name;
const sizeSpan = document.createElement('p') const sizeSpan = document.createElement('p');
sizeSpan.className = 'text-gray-400 text-sm' sizeSpan.className = 'text-gray-400 text-sm';
sizeSpan.textContent = formatBytes(selectedFile.size) sizeSpan.textContent = formatBytes(selectedFile.size);
infoDiv.append(nameSpan, sizeSpan) infoDiv.append(nameSpan, sizeSpan);
const deleteBtn = document.createElement('button') const deleteBtn = document.createElement('button');
deleteBtn.className = 'text-red-400 hover:text-red-300 p-2 flex-shrink-0 ml-2' deleteBtn.className =
deleteBtn.title = 'Remove file' 'text-red-400 hover:text-red-300 p-2 flex-shrink-0 ml-2';
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>' deleteBtn.title = 'Remove file';
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
deleteBtn.onclick = (e) => { deleteBtn.onclick = (e) => {
e.stopPropagation() e.stopPropagation();
resetState() resetState();
} };
innerDiv.append(infoDiv, deleteBtn) innerDiv.append(infoDiv, deleteBtn);
wrapper.appendChild(innerDiv) wrapper.appendChild(innerDiv);
fileListDiv.appendChild(wrapper) fileListDiv.appendChild(wrapper);
createIcons({ icons }) createIcons({ icons });
} }
async function adjustViewerHeight(file: File) { async function adjustViewerHeight(file: File) {
if (!viewerContainer) return if (!viewerContainer) return;
try { try {
const arrayBuffer = await file.arrayBuffer() const arrayBuffer = await file.arrayBuffer();
const loadingTask = getPDFDocument({ data: arrayBuffer }) const loadingTask = getPDFDocument({ data: arrayBuffer });
const pdf = await loadingTask.promise const pdf = await loadingTask.promise;
const page = await pdf.getPage(1) const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 1 }) const viewport = page.getViewport({ scale: 1 });
// Add ~50px for toolbar height relative to page height // Add ~50px for toolbar height relative to page height
const aspectRatio = viewport.width / (viewport.height + 50) const aspectRatio = viewport.width / (viewport.height + 50);
viewerContainer.style.height = 'auto' viewerContainer.style.height = 'auto';
viewerContainer.style.aspectRatio = `${aspectRatio}` viewerContainer.style.aspectRatio = `${aspectRatio}`;
} catch (e) { } catch (e) {
console.error('Error adjusting viewer height:', e) console.error('Error adjusting viewer height:', e);
// Fallback if calculation fails // Fallback if calculation fails
viewerContainer.style.height = '70vh' viewerContainer.style.height = '70vh';
} }
} }
async function loadPdfInViewer(file: File) { async function loadPdfInViewer(file: File) {
if (!viewerContainer) return if (!viewerContainer) return;
if (viewerCard) { if (viewerCard) {
viewerCard.classList.remove('hidden') viewerCard.classList.remove('hidden');
} }
// Clear existing iframe and blob URL // Clear existing iframe and blob URL
if (viewerIframe && viewerIframe.parentElement === viewerContainer) { if (viewerIframe && viewerIframe.parentElement === viewerContainer) {
viewerContainer.removeChild(viewerIframe) viewerContainer.removeChild(viewerIframe);
} }
if (currentBlobUrl) { if (currentBlobUrl) {
URL.revokeObjectURL(currentBlobUrl) URL.revokeObjectURL(currentBlobUrl);
currentBlobUrl = null currentBlobUrl = null;
} }
viewerIframe = null viewerIframe = null;
viewerReady = false viewerReady = false;
// Calculate and apply dynamic height // Calculate and apply dynamic height
await adjustViewerHeight(file) await adjustViewerHeight(file);
const arrayBuffer = await readFileAsArrayBuffer(file) const arrayBuffer = await readFileAsArrayBuffer(file);
const blob = new Blob([arrayBuffer as BlobPart], { type: 'application/pdf' }) const blob = new Blob([arrayBuffer as BlobPart], { type: 'application/pdf' });
currentBlobUrl = URL.createObjectURL(blob) currentBlobUrl = URL.createObjectURL(blob);
try { try {
const existingPrefsRaw = localStorage.getItem('pdfjs.preferences') const existingPrefsRaw = localStorage.getItem('pdfjs.preferences');
const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {} const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {};
delete (existingPrefs as any).annotationEditorMode delete (existingPrefs as any).annotationEditorMode;
const newPrefs = { const newPrefs = {
...existingPrefs, ...existingPrefs,
enablePermissions: false, enablePermissions: false,
} };
localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs)) localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs));
} catch {} } catch {}
const iframe = document.createElement('iframe') const iframe = document.createElement('iframe');
iframe.className = 'w-full h-full border-0' iframe.className = 'w-full h-full border-0';
iframe.allowFullscreen = true iframe.allowFullscreen = true;
const viewerUrl = new URL(import.meta.env.BASE_URL + 'pdfjs-annotation-viewer/web/viewer.html', window.location.origin) const viewerUrl = new URL(
const stampUserName = usernameInput?.value?.trim() || '' import.meta.env.BASE_URL + 'pdfjs-annotation-viewer/web/viewer.html',
window.location.origin
);
const stampUserName = usernameInput?.value?.trim() || '';
// ae_username is the hash parameter used by pdfjs-annotation-extension to set the username // ae_username is the hash parameter used by pdfjs-annotation-extension to set the username
const hashParams = stampUserName ? `#ae_username=${encodeURIComponent(stampUserName)}` : '' const hashParams = stampUserName
iframe.src = `${viewerUrl.toString()}?file=${encodeURIComponent(currentBlobUrl)}${hashParams}` ? `#ae_username=${encodeURIComponent(stampUserName)}`
: '';
iframe.src = `${viewerUrl.toString()}?file=${encodeURIComponent(currentBlobUrl)}${hashParams}`;
iframe.addEventListener('load', () => { iframe.addEventListener('load', () => {
setupAnnotationViewer(iframe) setupAnnotationViewer(iframe);
}) });
viewerContainer.appendChild(iframe) viewerContainer.appendChild(iframe);
viewerIframe = iframe viewerIframe = iframe;
} }
function setupAnnotationViewer(iframe: HTMLIFrameElement) { function setupAnnotationViewer(iframe: HTMLIFrameElement) {
try { try {
const win = iframe.contentWindow as any const win = iframe.contentWindow as any;
const doc = win?.document as Document | null const doc = win?.document as Document | null;
if (!win || !doc) return if (!win || !doc) return;
const initialize = async () => { const initialize = async () => {
try { try {
const app = win.PDFViewerApplication const app = win.PDFViewerApplication;
if (app?.initializedPromise) { if (app?.initializedPromise) {
await app.initializedPromise await app.initializedPromise;
} }
const eventBus = app?.eventBus const eventBus = app?.eventBus;
if (eventBus && typeof eventBus._on === 'function') { if (eventBus && typeof eventBus._on === 'function') {
eventBus._on('annotationeditoruimanager', () => { eventBus._on('annotationeditoruimanager', () => {
try { try {
const stampBtn = doc.getElementById('editorStampButton') as HTMLButtonElement | null const stampBtn = doc.getElementById(
stampBtn?.click() 'editorStampButton'
) as HTMLButtonElement | null;
stampBtn?.click();
} catch {} } catch {}
}) });
} }
const root = doc.querySelector('.PdfjsAnnotationExtension') as HTMLElement | null const root = doc.querySelector(
'.PdfjsAnnotationExtension'
) as HTMLElement | null;
if (root) { if (root) {
root.classList.add('PdfjsAnnotationExtension_Comment_hidden') root.classList.add('PdfjsAnnotationExtension_Comment_hidden');
} }
viewerReady = true viewerReady = true;
} catch (e) { } catch (e) {
console.error('Failed to initialize annotation viewer for Add Stamps:', e) console.error(
} 'Failed to initialize annotation viewer for Add Stamps:',
e
);
} }
};
void initialize() void initialize();
} catch (e) { } catch (e) {
console.error('Error wiring Add Stamps viewer:', e) console.error('Error wiring Add Stamps viewer:', e);
} }
} }
async function onPdfSelected(file: File) { async function onPdfSelected(file: File) {
selectedFile = file const result = await loadPdfWithPasswordPrompt(file);
updateFileList() if (!result) return;
if (saveStampedBtn) saveStampedBtn.classList.remove('hidden') result.pdf.destroy();
await loadPdfInViewer(file) selectedFile = result.file;
updateFileList();
if (saveStampedBtn) saveStampedBtn.classList.remove('hidden');
await loadPdfInViewer(result.file);
} }
if (pdfInput) { if (pdfInput) {
pdfInput.addEventListener('change', async (e) => { pdfInput.addEventListener('change', async (e) => {
const target = e.target as HTMLInputElement const target = e.target as HTMLInputElement;
if (target.files && target.files.length > 0) { if (target.files && target.files.length > 0) {
const file = target.files[0] const file = target.files[0];
await onPdfSelected(file) await onPdfSelected(file);
} }
}) });
} }
// Add drag/drop support // Add drag/drop support
const dropZone = document.getElementById('drop-zone') const dropZone = document.getElementById('drop-zone');
if (dropZone) { if (dropZone) {
dropZone.addEventListener('dragover', (e) => { dropZone.addEventListener('dragover', (e) => {
e.preventDefault() e.preventDefault();
dropZone.classList.add('border-indigo-500') dropZone.classList.add('border-indigo-500');
}) });
dropZone.addEventListener('dragleave', () => { dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('border-indigo-500') dropZone.classList.remove('border-indigo-500');
}) });
dropZone.addEventListener('drop', async (e) => { dropZone.addEventListener('drop', async (e) => {
e.preventDefault() e.preventDefault();
dropZone.classList.remove('border-indigo-500') dropZone.classList.remove('border-indigo-500');
const file = e.dataTransfer?.files[0] const file = e.dataTransfer?.files[0];
if (file && file.type === 'application/pdf') { if (file && file.type === 'application/pdf') {
await onPdfSelected(file) await onPdfSelected(file);
} }
}) });
} }
if (saveStampedBtn) { if (saveStampedBtn) {
saveStampedBtn.addEventListener('click', () => { saveStampedBtn.addEventListener('click', () => {
if (!viewerIframe) { if (!viewerIframe) {
alert('Viewer not ready. Please upload a PDF and wait for it to finish loading.') alert(
return 'Viewer not ready. Please upload a PDF and wait for it to finish loading.'
);
return;
} }
try { try {
const win = viewerIframe.contentWindow as any const win = viewerIframe.contentWindow as any;
const extensionInstance = win?.pdfjsAnnotationExtensionInstance as any const extensionInstance = win?.pdfjsAnnotationExtensionInstance as any;
if (extensionInstance && typeof extensionInstance.exportPdf === 'function') { if (
const result = extensionInstance.exportPdf() extensionInstance &&
typeof extensionInstance.exportPdf === 'function'
) {
const result = extensionInstance.exportPdf();
if (result && typeof result.then === 'function') { if (result && typeof result.then === 'function') {
result.then(() => { result
.then(() => {
// Reset state after successful export // Reset state after successful export
setTimeout(() => resetState(), 500) setTimeout(() => resetState(), 500);
}).catch((err: unknown) => {
console.error('Error while exporting stamped PDF via annotation extension:', err)
}) })
.catch((err: unknown) => {
console.error(
'Error while exporting stamped PDF via annotation extension:',
err
);
});
} }
return return;
} }
alert('Could not access the stamped-PDF exporter. Please use the Export → PDF button in the viewer toolbar as a fallback.') alert(
'Could not access the stamped-PDF exporter. Please use the Export → PDF button in the viewer toolbar as a fallback.'
);
} catch (e) { } catch (e) {
console.error('Failed to trigger stamped PDF export:', e) console.error('Failed to trigger stamped PDF export:', e);
alert('Could not export the stamped PDF. Please use the Export → PDF button in the viewer toolbar as a fallback.') alert(
'Could not export the stamped PDF. Please use the Export → PDF button in the viewer toolbar as a fallback.'
);
} }
}) });
} }
if (backToToolsBtn) { if (backToToolsBtn) {
backToToolsBtn.addEventListener('click', () => { backToToolsBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL window.location.href = import.meta.env.BASE_URL;
}) });
} }
initializeGlobalShortcuts() initializeGlobalShortcuts();

View File

@@ -14,6 +14,7 @@ import {
} from '../utils/pdf-operations.js'; } from '../utils/pdf-operations.js';
import { AddWatermarkState, PageWatermarkConfig } from '@/types'; import { AddWatermarkState, PageWatermarkConfig } from '@/types';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@@ -115,16 +116,16 @@ async function handleFiles(files: FileList) {
showAlert('Invalid File', 'Please upload a valid PDF file.'); showAlert('Invalid File', 'Please upload a valid PDF file.');
return; return;
} }
showLoader('Loading PDF...');
try { try {
const arrayBuffer = await file.arrayBuffer(); const result = await loadPdfWithPasswordPrompt(file);
const pdfBytes = new Uint8Array(arrayBuffer); if (!result) return;
showLoader('Loading PDF...');
const pdfBytes = new Uint8Array(result.bytes);
pageState.pdfDoc = await PDFLibDocument.load(pdfBytes); pageState.pdfDoc = await PDFLibDocument.load(pdfBytes);
pageState.file = file; pageState.file = result.file;
pageState.pdfBytes = pdfBytes; pageState.pdfBytes = pdfBytes;
cachedPdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes.slice() }) cachedPdfjsDoc = result.pdf;
.promise;
totalPageCount = cachedPdfjsDoc.numPages; totalPageCount = cachedPdfjsDoc.numPages;
currentPageNum = 1; currentPageNum = 1;
pageWatermarks.clear(); pageWatermarks.clear();

View File

@@ -11,6 +11,7 @@ import { applyColorAdjustments } from '../utils/image-effects.js';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import type { AdjustColorsSettings } from '../types/adjust-colors-type.js'; import type { AdjustColorsSettings } from '../types/adjust-colors-type.js';
import { t } from '../i18n/i18n'; import { t } from '../i18n/i18n';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@@ -357,13 +358,13 @@ document.addEventListener('DOMContentLoaded', () => {
return; return;
} }
files = [validFiles[0]];
updateUI();
showLoader('Loading preview...');
try { try {
const buffer = await readFileAsArrayBuffer(validFiles[0]); const result = await loadPdfWithPasswordPrompt(validFiles[0]);
pdfjsDoc = await getPDFDocument({ data: buffer }).promise; if (!result) return;
showLoader('Loading preview...');
files = [result.file];
updateUI();
pdfjsDoc = result.pdf;
await renderPreview(); await renderPreview();
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View File

@@ -9,6 +9,7 @@ import {
showWasmRequiredDialog, showWasmRequiredDialog,
WasmProvider, WasmProvider,
} from '../utils/wasm-provider.js'; } from '../utils/wasm-provider.js';
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
const pageState: AlternateMergeState = { const pageState: AlternateMergeState = {
files: [], files: [],
@@ -69,20 +70,21 @@ async function updateUI() {
createIcons({ icons }); createIcons({ icons });
// Load PDFs and populate list // Load PDFs and populate list
hideLoader();
pageState.files = await batchDecryptIfNeeded(pageState.files);
showLoader('Loading PDF files...'); showLoader('Loading PDF files...');
fileList.innerHTML = ''; fileList.innerHTML = '';
try { try {
for (let i = 0; i < pageState.files.length; i++) { for (let i = 0; i < pageState.files.length; i++) {
const file = pageState.files[i]; const file = pageState.files[i];
const fileKey = makeUniqueFileKey(i, file.name);
const arrayBuffer = await file.arrayBuffer();
pageState.pdfBytes.set(fileKey, arrayBuffer);
const bytesForPdfJs = arrayBuffer.slice(0); const fileKey = makeUniqueFileKey(i, file.name);
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise; const bytes = await file.arrayBuffer();
pageState.pdfDocs.set(fileKey, pdfjsDoc); const pdf = await getPDFDocument({ data: bytes.slice(0) }).promise;
const pageCount = pdfjsDoc.numPages; pageState.pdfBytes.set(fileKey, bytes);
pageState.pdfDocs.set(fileKey, pdf);
const pageCount = pdf.numPages;
const li = document.createElement('li'); const li = document.createElement('li');
li.className = li.className =

View File

@@ -3,11 +3,15 @@ import { showAlert, showLoader, hideLoader } from '../ui.js';
import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js'; import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js';
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib'; import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
import { BackgroundColorState } from '@/types'; import { BackgroundColorState } from '@/types';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
const pageState: BackgroundColorState = { file: null, pdfDoc: null }; const pageState: BackgroundColorState = { file: null, pdfDoc: null };
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializePage); } if (document.readyState === 'loading') {
else { initializePage(); } document.addEventListener('DOMContentLoaded', initializePage);
} else {
initializePage();
}
function initializePage() { function initializePage() {
createIcons({ icons }); createIcons({ icons });
@@ -18,34 +22,57 @@ function initializePage() {
if (fileInput) { if (fileInput) {
fileInput.addEventListener('change', handleFileUpload); fileInput.addEventListener('change', handleFileUpload);
fileInput.addEventListener('click', () => { fileInput.value = ''; }); fileInput.addEventListener('click', () => {
fileInput.value = '';
});
} }
if (dropZone) { if (dropZone) {
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-indigo-500'); }); dropZone.addEventListener('dragover', (e) => {
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('border-indigo-500'); }); e.preventDefault();
dropZone.classList.add('border-indigo-500');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('border-indigo-500');
});
dropZone.addEventListener('drop', (e) => { dropZone.addEventListener('drop', (e) => {
e.preventDefault(); dropZone.classList.remove('border-indigo-500'); e.preventDefault();
dropZone.classList.remove('border-indigo-500');
if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files); if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files);
}); });
} }
if (backBtn) backBtn.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; }); if (backBtn)
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
if (processBtn) processBtn.addEventListener('click', changeBackgroundColor); if (processBtn) processBtn.addEventListener('click', changeBackgroundColor);
} }
function handleFileUpload(e: Event) { const input = e.target as HTMLInputElement; if (input.files?.length) handleFiles(input.files); } function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement;
if (input.files?.length) handleFiles(input.files);
}
async function handleFiles(files: FileList) { async function handleFiles(files: FileList) {
const file = files[0]; const file = files[0];
if (!file || file.type !== 'application/pdf') { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; } if (!file || file.type !== 'application/pdf') {
showLoader('Loading PDF...'); showAlert('Invalid File', 'Please upload a valid PDF file.');
return;
}
try { try {
const arrayBuffer = await file.arrayBuffer(); const result = await loadPdfWithPasswordPrompt(file);
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer); if (!result) return;
pageState.file = file; showLoader('Loading PDF...');
result.pdf.destroy();
pageState.pdfDoc = await PDFLibDocument.load(result.bytes);
pageState.file = result.file;
updateFileDisplay(); updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden'); document.getElementById('options-panel')?.classList.remove('hidden');
} catch (error) { console.error(error); showAlert('Error', 'Failed to load PDF file.'); } } catch (error) {
finally { hideLoader(); } console.error(error);
showAlert('Error', 'Failed to load PDF file.');
} finally {
hideLoader();
}
} }
function updateFileDisplay() { function updateFileDisplay() {
@@ -53,7 +80,8 @@ function updateFileDisplay() {
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return; if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
fileDisplayArea.innerHTML = ''; fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0'; infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div'); const nameSpan = document.createElement('div');
@@ -73,7 +101,8 @@ function updateFileDisplay() {
} }
function resetState() { function resetState() {
pageState.file = null; pageState.pdfDoc = null; pageState.file = null;
pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area'); const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = ''; if (fileDisplayArea) fileDisplayArea.innerHTML = '';
document.getElementById('options-panel')?.classList.add('hidden'); document.getElementById('options-panel')?.classList.add('hidden');
@@ -82,8 +111,13 @@ function resetState() {
} }
async function changeBackgroundColor() { async function changeBackgroundColor() {
if (!pageState.pdfDoc) { showAlert('Error', 'Please upload a PDF file first.'); return; } if (!pageState.pdfDoc) {
const colorHex = (document.getElementById('background-color') as HTMLInputElement).value; showAlert('Error', 'Please upload a PDF file first.');
return;
}
const colorHex = (
document.getElementById('background-color') as HTMLInputElement
).value;
const color = hexToRgb(colorHex); const color = hexToRgb(colorHex);
showLoader('Changing background color...'); showLoader('Changing background color...');
try { try {
@@ -92,13 +126,33 @@ async function changeBackgroundColor() {
const [originalPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]); const [originalPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
const { width, height } = originalPage.getSize(); const { width, height } = originalPage.getSize();
const newPage = newPdfDoc.addPage([width, height]); const newPage = newPdfDoc.addPage([width, height]);
newPage.drawRectangle({ x: 0, y: 0, width, height, color: rgb(color.r, color.g, color.b) }); newPage.drawRectangle({
x: 0,
y: 0,
width,
height,
color: rgb(color.r, color.g, color.b),
});
const embeddedPage = await newPdfDoc.embedPage(originalPage); const embeddedPage = await newPdfDoc.embedPage(originalPage);
newPage.drawPage(embeddedPage, { x: 0, y: 0, width, height }); newPage.drawPage(embeddedPage, { x: 0, y: 0, width, height });
} }
const newPdfBytes = await newPdfDoc.save(); const newPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'background-changed.pdf'); downloadFile(
showAlert('Success', 'Background color changed successfully!', 'success', () => { resetState(); }); new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
} catch (e) { console.error(e); showAlert('Error', 'Could not change the background color.'); } 'background-changed.pdf'
finally { hideLoader(); } );
showAlert(
'Success',
'Background color changed successfully!',
'success',
() => {
resetState();
}
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not change the background color.');
} finally {
hideLoader();
}
} }

View File

@@ -2,6 +2,7 @@ import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js'; import { showAlert, showLoader, hideLoader } from '../ui.js';
import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js'; import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js';
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib'; import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import JSZip from 'jszip'; import JSZip from 'jszip';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { FileEntry, Position, StylePreset } from '@/types'; import { FileEntry, Position, StylePreset } from '@/types';
@@ -178,9 +179,13 @@ async function handleFiles(fileList: FileList) {
try { try {
for (const file of Array.from(fileList)) { for (const file of Array.from(fileList)) {
if (file.type !== 'application/pdf') continue; if (file.type !== 'application/pdf') continue;
const arrayBuffer = await file.arrayBuffer(); hideLoader();
const pdfDoc = await PDFDocument.load(arrayBuffer); const result = await loadPdfWithPasswordPrompt(file);
files.push({ file, pageCount: pdfDoc.getPageCount() }); if (!result) continue;
showLoader('Loading PDFs...');
result.pdf.destroy();
const pdfDoc = await PDFDocument.load(result.bytes);
files.push({ file: result.file, pageCount: pdfDoc.getPageCount() });
} }
if (files.length === 0) { if (files.length === 0) {

View File

@@ -13,6 +13,7 @@ import {
escapeHtml, escapeHtml,
hexToRgb, hexToRgb,
} from '../utils/helpers.js'; } from '../utils/helpers.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import { import {
BookmarkNode, BookmarkNode,
BookmarkTree, BookmarkTree,
@@ -1223,7 +1224,14 @@ async function loadPDF(e?: Event): Promise<void> {
if (filenameDisplay) if (filenameDisplay)
filenameDisplay.textContent = truncateFilename(file.name); filenameDisplay.textContent = truncateFilename(file.name);
renderFileDisplay(file); renderFileDisplay(file);
const arrayBuffer = await file.arrayBuffer();
loaderModal?.classList.add('hidden');
const result = await loadPdfWithPasswordPrompt(file);
if (!result) {
loaderModal?.classList.add('hidden');
return;
}
loaderModal?.classList.remove('hidden');
currentPage = 1; currentPage = 1;
bookmarkTree = []; bookmarkTree = [];
@@ -1232,12 +1240,8 @@ async function loadPDF(e?: Event): Promise<void> {
selectedBookmarks.clear(); selectedBookmarks.clear();
collapsedNodes.clear(); collapsedNodes.clear();
pdfLibDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryption: true }); pdfLibDoc = await PDFDocument.load(result.bytes, { ignoreEncryption: true });
pdfJsDoc = result.pdf;
const loadingTask = getPDFDocument({
data: new Uint8Array(arrayBuffer),
});
pdfJsDoc = await loadingTask.promise;
if (gotoPageInput) gotoPageInput.max = String(pdfJsDoc.numPages); if (gotoPageInput) gotoPageInput.max = String(pdfJsDoc.numPages);

View File

@@ -1,11 +1,20 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes, hexToRgb, getPDFDocument } from '../utils/helpers.js'; import {
downloadFile,
formatBytes,
hexToRgb,
getPDFDocument,
} from '../utils/helpers.js';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib'; import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { CombineSinglePageState } from '@/types'; import { CombineSinglePageState } from '@/types';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
const pageState: CombineSinglePageState = { const pageState: CombineSinglePageState = {
file: null, file: null,
@@ -36,7 +45,8 @@ async function updateUI() {
if (pageState.file) { if (pageState.file) {
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden'; infoContainer.className = 'flex flex-col overflow-hidden';
@@ -63,11 +73,16 @@ async function updateUI() {
createIcons({ icons }); createIcons({ icons });
try { try {
const result = await loadPdfWithPasswordPrompt(pageState.file);
if (!result) {
resetState();
return;
}
showLoader('Loading PDF...'); showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer(); result.pdf.destroy();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, { pageState.file = result.file;
ignoreEncryption: true, pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
throwOnInvalidObject: false throwOnInvalidObject: false,
}); });
hideLoader(); hideLoader();
@@ -92,12 +107,26 @@ async function combineToSinglePage() {
return; return;
} }
const orientation = (document.getElementById('combine-orientation') as HTMLSelectElement).value; const orientation = (
const spacing = parseInt((document.getElementById('page-spacing') as HTMLInputElement).value) || 0; document.getElementById('combine-orientation') as HTMLSelectElement
const backgroundColorHex = (document.getElementById('background-color') as HTMLInputElement).value; ).value;
const addSeparator = (document.getElementById('add-separator') as HTMLInputElement).checked; const spacing =
const separatorThickness = parseFloat((document.getElementById('separator-thickness') as HTMLInputElement).value) || 0.5; parseInt(
const separatorColorHex = (document.getElementById('separator-color') as HTMLInputElement).value; (document.getElementById('page-spacing') as HTMLInputElement).value
) || 0;
const backgroundColorHex = (
document.getElementById('background-color') as HTMLInputElement
).value;
const addSeparator = (
document.getElementById('add-separator') as HTMLInputElement
).checked;
const separatorThickness =
parseFloat(
(document.getElementById('separator-thickness') as HTMLInputElement).value
) || 0.5;
const separatorColorHex = (
document.getElementById('separator-color') as HTMLInputElement
).value;
const backgroundColor = hexToRgb(backgroundColorHex); const backgroundColor = hexToRgb(backgroundColorHex);
const separatorColor = hexToRgb(separatorColorHex); const separatorColor = hexToRgb(separatorColorHex);
@@ -167,7 +196,7 @@ async function combineToSinglePage() {
await page.render({ await page.render({
canvasContext: context, canvasContext: context,
viewport, viewport,
canvas canvas,
}).promise; }).promise;
const pngDataUrl = canvas.toDataURL('image/png'); const pngDataUrl = canvas.toDataURL('image/png');
@@ -222,9 +251,14 @@ async function combineToSinglePage() {
`${originalName}_combined.pdf` `${originalName}_combined.pdf`
); );
showAlert('Success', 'Pages combined successfully!', 'success', function () { showAlert(
'Success',
'Pages combined successfully!',
'success',
function () {
resetState(); resetState();
}); }
);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showAlert('Error', 'An error occurred while combining pages.'); showAlert('Error', 'An error occurred while combining pages.');
@@ -236,7 +270,10 @@ async function combineToSinglePage() {
function handleFileSelect(files: FileList | null) { function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) { if (files && files.length > 0) {
const file = files[0]; const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file; pageState.file = file;
updateUI(); updateUI();
} }
@@ -288,7 +325,10 @@ document.addEventListener('DOMContentLoaded', function () {
const files = e.dataTransfer?.files; const files = e.dataTransfer?.files;
if (files && files.length > 0) { if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) { const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'); return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
}); });
if (pdfFiles.length > 0) { if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer(); const dataTransfer = new DataTransfer();

View File

@@ -1,5 +1,5 @@
import { showLoader, hideLoader, showAlert } from '../ui.ts'; import { showLoader, hideLoader, showAlert } from '../ui.ts';
import { getPDFDocument } from '../utils/helpers.ts'; import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { CompareState } from '@/types'; import { CompareState } from '@/types';
@@ -745,9 +745,11 @@ async function handleFileInput(
} }
try { try {
showLoader(`Loading ${file.name}...`); hideLoader();
const arrayBuffer = await file.arrayBuffer(); const result = await loadPdfWithPasswordPrompt(file);
pageState[docKey] = await getPDFDocument({ data: arrayBuffer }).promise; if (!result) return;
showLoader(`Loading ${result.file.name}...`);
pageState[docKey] = result.pdf;
caches.pageModelCache.clear(); caches.pageModelCache.clear();
caches.comparisonCache.clear(); caches.comparisonCache.clear();
caches.comparisonResultsCache.clear(); caches.comparisonResultsCache.clear();

View File

@@ -5,12 +5,14 @@ import {
formatBytes, formatBytes,
getPDFDocument, getPDFDocument,
} from '../utils/helpers.js'; } from '../utils/helpers.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { PDFDocument } from 'pdf-lib'; import { PDFDocument } from 'pdf-lib';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import type { PDFDocumentProxy } from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@@ -120,15 +122,27 @@ async function performCondenseCompression(
return { ...result, usedFallback: true }; return { ...result, usedFallback: true };
} }
throw new Error(`PDF compression failed: ${errorMessage}`); throw new Error(`PDF compression failed: ${errorMessage}`, {
cause: error,
});
} }
} }
async function performPhotonCompression( async function performPhotonCompression(
arrayBuffer: ArrayBuffer, arrayBuffer: ArrayBuffer,
level: string level: string,
file?: File
) { ) {
const pdfJsDoc = await getPDFDocument({ data: arrayBuffer }).promise; let pdfJsDoc: PDFDocumentProxy;
if (file) {
hideLoader();
const result = await loadPdfWithPasswordPrompt(file);
if (!result) return null;
showLoader('Running Photon compression...');
pdfJsDoc = result.pdf;
} else {
pdfJsDoc = await getPDFDocument({ data: arrayBuffer }).promise;
}
const newPdfDoc = await PDFDocument.create(); const newPdfDoc = await PDFDocument.create();
const settings = const settings =
PHOTON_PRESETS[level as keyof typeof PHOTON_PRESETS] || PHOTON_PRESETS[level as keyof typeof PHOTON_PRESETS] ||
@@ -429,8 +443,10 @@ document.addEventListener('DOMContentLoaded', () => {
)) as ArrayBuffer; )) as ArrayBuffer;
const resultBytes = await performPhotonCompression( const resultBytes = await performPhotonCompression(
arrayBuffer, arrayBuffer,
level level,
originalFile
); );
if (!resultBytes) return;
const buffer = resultBytes.buffer.slice( const buffer = resultBytes.buffer.slice(
resultBytes.byteOffset, resultBytes.byteOffset,
resultBytes.byteOffset + resultBytes.byteLength resultBytes.byteOffset + resultBytes.byteLength
@@ -494,7 +510,13 @@ document.addEventListener('DOMContentLoaded', () => {
const arrayBuffer = (await readFileAsArrayBuffer( const arrayBuffer = (await readFileAsArrayBuffer(
file file
)) as ArrayBuffer; )) as ArrayBuffer;
resultBytes = await performPhotonCompression(arrayBuffer, level); const photonResult = await performPhotonCompression(
arrayBuffer,
level,
file
);
if (!photonResult) return;
resultBytes = photonResult;
} }
totalCompressedSize += resultBytes.length; totalCompressedSize += resultBytes.length;

View File

@@ -1,11 +1,7 @@
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { import { downloadFile, formatBytes } from '../utils/helpers.js';
downloadFile, import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
readFileAsArrayBuffer,
formatBytes,
getPDFDocument,
} from '../utils/helpers.js';
import Cropper from 'cropperjs'; import Cropper from 'cropperjs';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
@@ -88,16 +84,19 @@ async function handleFile(file: File) {
return; return;
} }
showLoader('Loading PDF...');
cropperState.file = file; cropperState.file = file;
cropperState.pageCrops = {}; cropperState.pageCrops = {};
try { try {
const arrayBuffer = await readFileAsArrayBuffer(file); const result = await loadPdfWithPasswordPrompt(file);
cropperState.originalPdfBytes = arrayBuffer as ArrayBuffer; if (!result) {
cropperState.pdfDoc = await getPDFDocument({ cropperState.file = null;
data: (arrayBuffer as ArrayBuffer).slice(0), return;
}).promise; }
showLoader('Loading PDF...');
cropperState.file = result.file;
cropperState.originalPdfBytes = result.bytes;
cropperState.pdfDoc = result.pdf;
cropperState.currentPageNum = 1; cropperState.currentPageNum = 1;
updateFileDisplay(); updateFileDisplay();
@@ -308,7 +307,7 @@ async function performMetadataCrop(
): Promise<Uint8Array> { ): Promise<Uint8Array> {
const pdfToModify = await PDFLibDocument.load( const pdfToModify = await PDFLibDocument.load(
cropperState.originalPdfBytes!, cropperState.originalPdfBytes!,
{ ignoreEncryption: true, throwOnInvalidObject: false } { throwOnInvalidObject: false }
); );
for (const pageNum in cropData) { for (const pageNum in cropData) {
@@ -352,7 +351,7 @@ async function performFlatteningCrop(
const newPdfDoc = await PDFLibDocument.create(); const newPdfDoc = await PDFLibDocument.create();
const sourcePdfDocForCopying = await PDFLibDocument.load( const sourcePdfDocForCopying = await PDFLibDocument.load(
cropperState.originalPdfBytes!, cropperState.originalPdfBytes!,
{ ignoreEncryption: true, throwOnInvalidObject: false } { throwOnInvalidObject: false }
); );
const totalPages = cropperState.pdfDoc.numPages; const totalPages = cropperState.pdfDoc.numPages;

View File

@@ -1,13 +1,12 @@
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js'; import { showAlert, showLoader, hideLoader } from '../ui.js';
import { import {
readFileAsArrayBuffer,
formatBytes, formatBytes,
downloadFile, downloadFile,
getPDFDocument,
parsePageRanges, parsePageRanges,
} from '../utils/helpers.js'; } from '../utils/helpers.js';
import { PDFDocument } from 'pdf-lib'; import { PDFDocument } from 'pdf-lib';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import { deletePdfPages } from '../utils/pdf-operations.js'; import { deletePdfPages } from '../utils/pdf-operations.js';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { DeletePagesState } from '@/types'; import { DeletePagesState } from '@/types';
@@ -85,18 +84,20 @@ async function handleFile(file: File) {
return; return;
} }
showLoader('Loading PDF...');
deleteState.file = file; deleteState.file = file;
try { try {
const arrayBuffer = await readFileAsArrayBuffer(file); const result = await loadPdfWithPasswordPrompt(file);
deleteState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { if (!result) {
ignoreEncryption: true, deleteState.file = null;
return;
}
showLoader('Loading PDF...');
deleteState.file = result.file;
deleteState.pdfDoc = await PDFDocument.load(result.bytes, {
throwOnInvalidObject: false, throwOnInvalidObject: false,
}); });
deleteState.pdfJsDoc = await getPDFDocument({ deleteState.pdfJsDoc = result.pdf;
data: (arrayBuffer as ArrayBuffer).slice(0),
}).promise;
deleteState.totalPages = deleteState.pdfDoc.getPageCount(); deleteState.totalPages = deleteState.pdfDoc.getPageCount();
deleteState.pagesToDelete = new Set(); deleteState.pagesToDelete = new Set();

View File

@@ -1,6 +1,7 @@
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { downloadFile } from '../utils/helpers'; import { downloadFile } from '../utils/helpers';
@@ -151,6 +152,8 @@ async function processDeskew(): Promise<void> {
const threshold = parseFloat(thresholdSelect?.value || '0.5'); const threshold = parseFloat(thresholdSelect?.value || '0.5');
const dpi = parseInt(dpiSelect?.value || '150', 10); const dpi = parseInt(dpiSelect?.value || '150', 10);
selectedFiles = await batchDecryptIfNeeded(selectedFiles);
showLoader('Initializing PyMuPDF...'); showLoader('Initializing PyMuPDF...');
try { try {

View File

@@ -1,6 +1,11 @@
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js'; import { showAlert, showLoader, hideLoader } from '../ui.js';
import { readFileAsArrayBuffer, formatBytes, downloadFile, getPDFDocument } from '../utils/helpers.js'; import {
readFileAsArrayBuffer,
formatBytes,
downloadFile,
} from '../utils/helpers.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import { t } from '../i18n/i18n'; import { t } from '../i18n/i18n';
import { import {
signPdf, signPdf,
@@ -8,7 +13,11 @@ import {
parseCombinedPem, parseCombinedPem,
getCertificateInfo, getCertificateInfo,
} from './digital-sign-pdf.js'; } from './digital-sign-pdf.js';
import { SignatureInfo, VisibleSignatureOptions, DigitalSignState } from '@/types'; import {
SignatureInfo,
VisibleSignatureOptions,
DigitalSignState,
} from '@/types';
const state: DigitalSignState = { const state: DigitalSignState = {
pdfFile: null, pdfFile: null,
@@ -180,11 +189,17 @@ function initializePage(): void {
const file = input.files[0]; const file = input.files[0];
const validTypes = ['image/png', 'image/jpeg', 'image/webp']; const validTypes = ['image/png', 'image/jpeg', 'image/webp'];
if (!validTypes.includes(file.type)) { if (!validTypes.includes(file.type)) {
showAlert('Invalid Image', 'Please select a PNG, JPG, or WebP image.'); showAlert(
'Invalid Image',
'Please select a PNG, JPG, or WebP image.'
);
return; return;
} }
state.sigImageData = await readFileAsArrayBuffer(file) as ArrayBuffer; state.sigImageData = (await readFileAsArrayBuffer(file)) as ArrayBuffer;
state.sigImageType = file.type.replace('image/', '') as 'png' | 'jpeg' | 'webp'; state.sigImageType = file.type.replace('image/', '') as
| 'png'
| 'jpeg'
| 'webp';
if (sigImageThumb && sigImagePreview) { if (sigImageThumb && sigImagePreview) {
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
@@ -223,13 +238,18 @@ function handlePdfUpload(e: Event): void {
} }
async function handlePdfFile(file: File): Promise<void> { async function handlePdfFile(file: File): Promise<void> {
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) { if (
file.type !== 'application/pdf' &&
!file.name.toLowerCase().endsWith('.pdf')
) {
showAlert('Invalid File', 'Please select a PDF file.'); showAlert('Invalid File', 'Please select a PDF file.');
return; return;
} }
state.pdfFile = file; state.pdfFile = file;
state.pdfBytes = new Uint8Array(await readFileAsArrayBuffer(file) as ArrayBuffer); state.pdfBytes = new Uint8Array(
(await readFileAsArrayBuffer(file)) as ArrayBuffer
);
updatePdfDisplay(); updatePdfDisplay();
showCertificateSection(); showCertificateSection();
@@ -243,7 +263,8 @@ async function updatePdfDisplay(): Promise<void> {
fileDisplayArea.innerHTML = ''; fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0'; infoContainer.className = 'flex flex-col flex-1 min-w-0';
@@ -273,14 +294,21 @@ async function updatePdfDisplay(): Promise<void> {
fileDisplayArea.appendChild(fileDiv); fileDisplayArea.appendChild(fileDiv);
createIcons({ icons }); createIcons({ icons });
try { if (state.pdfFile) {
if (state.pdfBytes) { const result = await loadPdfWithPasswordPrompt(state.pdfFile);
const pdfDoc = await getPDFDocument({ data: state.pdfBytes.slice() }).promise; if (!result) {
metaSpan.textContent = `${formatBytes(state.pdfFile.size)}${pdfDoc.numPages} pages`; state.pdfFile = null;
state.pdfBytes = null;
fileDisplayArea.innerHTML = '';
hideCertificateSection();
updateProcessButton();
return;
} }
} catch (error) { state.pdfFile = result.file;
console.error('Error loading PDF:', error); state.pdfBytes = new Uint8Array(result.bytes);
metaSpan.textContent = `${formatBytes(state.pdfFile.size)}`; nameSpan.textContent = result.file.name;
metaSpan.textContent = `${formatBytes(result.file.size)}${result.pdf.numPages} pages`;
result.pdf.destroy();
} }
} }
@@ -315,7 +343,9 @@ function hideCertificateSection(): void {
certInfo.classList.add('hidden'); certInfo.classList.add('hidden');
} }
const certPasswordSection = getElement<HTMLDivElement>('cert-password-section'); const certPasswordSection = getElement<HTMLDivElement>(
'cert-password-section'
);
if (certPasswordSection) { if (certPasswordSection) {
certPasswordSection.classList.add('hidden'); certPasswordSection.classList.add('hidden');
} }
@@ -330,12 +360,15 @@ function handleCertUpload(e: Event): void {
async function handleCertFile(file: File): Promise<void> { async function handleCertFile(file: File): Promise<void> {
const validExtensions = ['.pfx', '.p12', '.pem']; const validExtensions = ['.pfx', '.p12', '.pem'];
const hasValidExtension = validExtensions.some(ext => const hasValidExtension = validExtensions.some((ext) =>
file.name.toLowerCase().endsWith(ext) file.name.toLowerCase().endsWith(ext)
); );
if (!hasValidExtension) { if (!hasValidExtension) {
showAlert('Invalid Certificate', 'Please select a .pfx, .p12, or .pem certificate file.'); showAlert(
'Invalid Certificate',
'Please select a .pfx, .p12, or .pem certificate file.'
);
return; return;
} }
@@ -361,7 +394,8 @@ async function handleCertFile(file: File): Promise<void> {
const certStatus = getElement<HTMLDivElement>('cert-status'); const certStatus = getElement<HTMLDivElement>('cert-status');
if (certStatus) { if (certStatus) {
certStatus.innerHTML = 'Certificate loaded <i data-lucide="check" class="inline w-4 h-4"></i>'; certStatus.innerHTML =
'Certificate loaded <i data-lucide="check" class="inline w-4 h-4"></i>';
createIcons({ icons }); createIcons({ icons });
certStatus.className = 'text-xs text-green-400'; certStatus.className = 'text-xs text-green-400';
} }
@@ -390,7 +424,8 @@ function updateCertDisplay(): void {
certDisplayArea.innerHTML = ''; certDisplayArea.innerHTML = '';
const certDiv = document.createElement('div'); const certDiv = document.createElement('div');
certDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; certDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0'; infoContainer.className = 'flex flex-col flex-1 min-w-0';
@@ -425,7 +460,9 @@ function updateCertDisplay(): void {
} }
function showPasswordSection(): void { function showPasswordSection(): void {
const certPasswordSection = getElement<HTMLDivElement>('cert-password-section'); const certPasswordSection = getElement<HTMLDivElement>(
'cert-password-section'
);
if (certPasswordSection) { if (certPasswordSection) {
certPasswordSection.classList.remove('hidden'); certPasswordSection.classList.remove('hidden');
} }
@@ -445,7 +482,9 @@ function updatePasswordLabel(labelText: string): void {
} }
function hidePasswordSection(): void { function hidePasswordSection(): void {
const certPasswordSection = getElement<HTMLDivElement>('cert-password-section'); const certPasswordSection = getElement<HTMLDivElement>(
'cert-password-section'
);
if (certPasswordSection) { if (certPasswordSection) {
certPasswordSection.classList.add('hidden'); certPasswordSection.classList.add('hidden');
} }
@@ -456,7 +495,9 @@ function showSignatureOptions(): void {
if (signatureOptions) { if (signatureOptions) {
signatureOptions.classList.remove('hidden'); signatureOptions.classList.remove('hidden');
} }
const visibleSigSection = getElement<HTMLDivElement>('visible-signature-section'); const visibleSigSection = getElement<HTMLDivElement>(
'visible-signature-section'
);
if (visibleSigSection) { if (visibleSigSection) {
visibleSigSection.classList.remove('hidden'); visibleSigSection.classList.remove('hidden');
} }
@@ -467,7 +508,9 @@ function hideSignatureOptions(): void {
if (signatureOptions) { if (signatureOptions) {
signatureOptions.classList.add('hidden'); signatureOptions.classList.add('hidden');
} }
const visibleSigSection = getElement<HTMLDivElement>('visible-signature-section'); const visibleSigSection = getElement<HTMLDivElement>(
'visible-signature-section'
);
if (visibleSigSection) { if (visibleSigSection) {
visibleSigSection.classList.add('hidden'); visibleSigSection.classList.add('hidden');
} }
@@ -495,7 +538,9 @@ async function handlePasswordInput(): Promise<void> {
const pemContent = await state.certFile.text(); const pemContent = await state.certFile.text();
state.certData = parseCombinedPem(pemContent, password); state.certData = parseCombinedPem(pemContent, password);
} else { } else {
const certBytes = await readFileAsArrayBuffer(state.certFile) as ArrayBuffer; const certBytes = (await readFileAsArrayBuffer(
state.certFile
)) as ArrayBuffer;
state.certData = parsePfxFile(certBytes, password); state.certData = parsePfxFile(certBytes, password);
} }
@@ -505,7 +550,8 @@ async function handlePasswordInput(): Promise<void> {
const certStatus = getElement<HTMLDivElement>('cert-status'); const certStatus = getElement<HTMLDivElement>('cert-status');
if (certStatus) { if (certStatus) {
certStatus.innerHTML = 'Certificate unlocked <i data-lucide="check-circle" class="inline w-4 h-4"></i>'; certStatus.innerHTML =
'Certificate unlocked <i data-lucide="check-circle" class="inline w-4 h-4"></i>';
createIcons({ icons }); createIcons({ icons });
certStatus.className = 'text-xs text-green-400'; certStatus.className = 'text-xs text-green-400';
} }
@@ -516,7 +562,10 @@ async function handlePasswordInput(): Promise<void> {
const certStatus = getElement<HTMLDivElement>('cert-status'); const certStatus = getElement<HTMLDivElement>('cert-status');
if (certStatus) { if (certStatus) {
const errorMessage = error instanceof Error ? error.message : 'Invalid password or certificate'; const errorMessage =
error instanceof Error
? error.message
: 'Invalid password or certificate';
certStatus.textContent = errorMessage.includes('password') certStatus.textContent = errorMessage.includes('password')
? 'Incorrect password' ? 'Incorrect password'
: 'Failed to parse certificate'; : 'Failed to parse certificate';
@@ -566,7 +615,10 @@ function updateProcessButton(): void {
async function processSignature(): Promise<void> { async function processSignature(): Promise<void> {
if (!state.pdfBytes || !state.certData) { if (!state.pdfBytes || !state.certData) {
showAlert('Missing Data', 'Please upload both a PDF and a valid certificate.'); showAlert(
'Missing Data',
'Please upload both a PDF and a valid certificate.'
);
return; return;
} }
@@ -583,20 +635,34 @@ async function processSignature(): Promise<void> {
const enableVisibleSig = getElement<HTMLInputElement>('enable-visible-sig'); const enableVisibleSig = getElement<HTMLInputElement>('enable-visible-sig');
if (enableVisibleSig?.checked) { if (enableVisibleSig?.checked) {
const sigX = parseInt(getElement<HTMLInputElement>('sig-x')?.value ?? '25', 10); const sigX = parseInt(
const sigY = parseInt(getElement<HTMLInputElement>('sig-y')?.value ?? '700', 10); getElement<HTMLInputElement>('sig-x')?.value ?? '25',
const sigWidth = parseInt(getElement<HTMLInputElement>('sig-width')?.value ?? '150', 10); 10
const sigHeight = parseInt(getElement<HTMLInputElement>('sig-height')?.value ?? '70', 10); );
const sigY = parseInt(
getElement<HTMLInputElement>('sig-y')?.value ?? '700',
10
);
const sigWidth = parseInt(
getElement<HTMLInputElement>('sig-width')?.value ?? '150',
10
);
const sigHeight = parseInt(
getElement<HTMLInputElement>('sig-height')?.value ?? '70',
10
);
const sigPageSelect = getElement<HTMLSelectElement>('sig-page'); const sigPageSelect = getElement<HTMLSelectElement>('sig-page');
let sigPage: number | string = 0; let sigPage: number | string = 0;
let numPages = 1; let numPages = 1;
try { if (state.pdfFile) {
const pdfDoc = await getPDFDocument({ data: state.pdfBytes.slice() }).promise; const pageCountResult = await loadPdfWithPasswordPrompt(state.pdfFile);
numPages = pdfDoc.numPages; if (!pageCountResult) return;
} catch (error) { state.pdfFile = pageCountResult.file;
console.error('Error getting PDF page count:', error); state.pdfBytes = new Uint8Array(pageCountResult.bytes);
numPages = pageCountResult.pdf.numPages;
pageCountResult.pdf.destroy();
} }
if (sigPageSelect) { if (sigPageSelect) {
@@ -609,16 +675,26 @@ async function processSignature(): Promise<void> {
sigPage = `0-${numPages - 1}`; sigPage = `0-${numPages - 1}`;
} }
} else if (sigPageSelect.value === 'custom') { } else if (sigPageSelect.value === 'custom') {
sigPage = parseInt(getElement<HTMLInputElement>('sig-custom-page')?.value ?? '1', 10) - 1; sigPage =
parseInt(
getElement<HTMLInputElement>('sig-custom-page')?.value ?? '1',
10
) - 1;
} else { } else {
sigPage = parseInt(sigPageSelect.value, 10); sigPage = parseInt(sigPageSelect.value, 10);
} }
} }
const enableSigText = getElement<HTMLInputElement>('enable-sig-text'); const enableSigText = getElement<HTMLInputElement>('enable-sig-text');
let sigText = enableSigText?.checked ? getElement<HTMLInputElement>('sig-text')?.value : undefined; let sigText = enableSigText?.checked
const sigTextColor = getElement<HTMLInputElement>('sig-text-color')?.value ?? '#000000'; ? getElement<HTMLInputElement>('sig-text')?.value
const sigTextSize = parseInt(getElement<HTMLInputElement>('sig-text-size')?.value ?? '12', 10); : undefined;
const sigTextColor =
getElement<HTMLInputElement>('sig-text-color')?.value ?? '#000000';
const sigTextSize = parseInt(
getElement<HTMLInputElement>('sig-text-size')?.value ?? '12',
10
);
if (!state.sigImageData && !sigText && state.certData) { if (!state.sigImageData && !sigText && state.certData) {
const certInfo = getCertificateInfo(state.certData.certificate); const certInfo = getCertificateInfo(state.certData.certificate);
@@ -631,7 +707,9 @@ async function processSignature(): Promise<void> {
const lineCount = (sigText.match(/\n/g) || []).length + 1; const lineCount = (sigText.match(/\n/g) || []).length + 1;
const lineHeightFactor = 1.4; const lineHeightFactor = 1.4;
const padding = 16; const padding = 16;
const calculatedHeight = Math.ceil(lineCount * sigTextSize * lineHeightFactor + padding); const calculatedHeight = Math.ceil(
lineCount * sigTextSize * lineHeightFactor + padding
);
finalHeight = Math.max(calculatedHeight, sigHeight); finalHeight = Math.max(calculatedHeight, sigHeight);
} }
@@ -658,21 +736,35 @@ async function processSignature(): Promise<void> {
visibleSignature, visibleSignature,
}); });
const blob = new Blob([signedPdfBytes.slice().buffer], { type: 'application/pdf' }); const blob = new Blob([signedPdfBytes.slice().buffer], {
type: 'application/pdf',
});
const originalName = state.pdfFile?.name ?? 'document.pdf'; const originalName = state.pdfFile?.name ?? 'document.pdf';
const signedName = originalName.replace(/\.pdf$/i, '_signed.pdf'); const signedName = originalName.replace(/\.pdf$/i, '_signed.pdf');
downloadFile(blob, signedName); downloadFile(blob, signedName);
hideLoader(); hideLoader();
showAlert('Success', 'PDF signed successfully! The signature can be verified in any PDF reader.', 'success', () => { resetState(); }); showAlert(
'Success',
'PDF signed successfully! The signature can be verified in any PDF reader.',
'success',
() => {
resetState();
}
);
} catch (error) { } catch (error) {
hideLoader(); hideLoader();
console.error('Signing error:', error); console.error('Signing error:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; const errorMessage =
error instanceof Error ? error.message : 'Unknown error occurred';
// Check if this is a CORS/network error from certificate chain fetching // Check if this is a CORS/network error from certificate chain fetching
if (errorMessage.includes('Failed to fetch') || errorMessage.includes('CORS') || errorMessage.includes('NetworkError')) { if (
errorMessage.includes('Failed to fetch') ||
errorMessage.includes('CORS') ||
errorMessage.includes('NetworkError')
) {
showAlert( showAlert(
'Signing Failed', 'Signing Failed',
'Failed to fetch certificate chain. This may be due to network issues or the certificate proxy being unavailable. Please check your internet connection and try again. If the issue persists, contact support.' 'Failed to fetch certificate chain. This may be due to network issues or the certificate proxy being unavailable. Please check your internet connection and try again. If the issue persists, contact support.'

View File

@@ -1,8 +1,13 @@
import { DividePagesState } from '@/types'; import { DividePagesState } from '@/types';
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes, parsePageRanges } from '../utils/helpers.js'; import {
downloadFile,
formatBytes,
parsePageRanges,
} from '../utils/helpers.js';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
const pageState: DividePagesState = { const pageState: DividePagesState = {
file: null, file: null,
@@ -24,10 +29,14 @@ function resetState() {
const fileInput = document.getElementById('file-input') as HTMLInputElement; const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = ''; if (fileInput) fileInput.value = '';
const splitTypeSelect = document.getElementById('split-type') as HTMLSelectElement; const splitTypeSelect = document.getElementById(
'split-type'
) as HTMLSelectElement;
if (splitTypeSelect) splitTypeSelect.value = 'vertical'; if (splitTypeSelect) splitTypeSelect.value = 'vertical';
const pageRangeInput = document.getElementById('page-range') as HTMLInputElement; const pageRangeInput = document.getElementById(
'page-range'
) as HTMLInputElement;
if (pageRangeInput) pageRangeInput.value = ''; if (pageRangeInput) pageRangeInput.value = '';
} }
@@ -41,7 +50,8 @@ async function updateUI() {
if (pageState.file) { if (pageState.file) {
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden'; infoContainer.className = 'flex flex-col overflow-hidden';
@@ -68,12 +78,16 @@ async function updateUI() {
createIcons({ icons }); createIcons({ icons });
try { try {
const result = await loadPdfWithPasswordPrompt(pageState.file);
if (!result) {
resetState();
return;
}
result.pdf.destroy();
pageState.file = result.file;
showLoader('Loading PDF...'); showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, { pageState.pdfDoc = await PDFLibDocument.load(result.bytes);
ignoreEncryption: true,
throwOnInvalidObject: false
});
pageState.totalPages = pageState.pdfDoc.getPageCount(); pageState.totalPages = pageState.pdfDoc.getPageCount();
hideLoader(); hideLoader();
@@ -97,21 +111,30 @@ async function dividePages() {
return; return;
} }
const pageRangeInput = document.getElementById('page-range') as HTMLInputElement; const pageRangeInput = document.getElementById(
'page-range'
) as HTMLInputElement;
const pageRangeValue = pageRangeInput?.value.trim().toLowerCase() || ''; const pageRangeValue = pageRangeInput?.value.trim().toLowerCase() || '';
const splitTypeSelect = document.getElementById('split-type') as HTMLSelectElement; const splitTypeSelect = document.getElementById(
'split-type'
) as HTMLSelectElement;
const splitType = splitTypeSelect.value; const splitType = splitTypeSelect.value;
let pagesToDivide: Set<number>; let pagesToDivide: Set<number>;
if (pageRangeValue === '' || pageRangeValue === 'all') { if (pageRangeValue === '' || pageRangeValue === 'all') {
pagesToDivide = new Set(Array.from({ length: pageState.totalPages }, (_, i) => i + 1)); pagesToDivide = new Set(
Array.from({ length: pageState.totalPages }, (_, i) => i + 1)
);
} else { } else {
const parsedIndices = parsePageRanges(pageRangeValue, pageState.totalPages); const parsedIndices = parsePageRanges(pageRangeValue, pageState.totalPages);
pagesToDivide = new Set(parsedIndices.map(i => i + 1)); pagesToDivide = new Set(parsedIndices.map((i) => i + 1));
if (pagesToDivide.size === 0) { if (pagesToDivide.size === 0) {
showAlert('Invalid Range', 'Please enter a valid page range (e.g., 1-5, 8, 11-13).'); showAlert(
'Invalid Range',
'Please enter a valid page range (e.g., 1-5, 8, 11-13).'
);
return; return;
} }
} }
@@ -160,9 +183,14 @@ async function dividePages() {
`${originalName}_divided.pdf` `${originalName}_divided.pdf`
); );
showAlert('Success', 'Pages have been divided successfully!', 'success', function () { showAlert(
'Success',
'Pages have been divided successfully!',
'success',
function () {
resetState(); resetState();
}); }
);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showAlert('Error', 'An error occurred while dividing the PDF.'); showAlert('Error', 'An error occurred while dividing the PDF.');
@@ -174,7 +202,10 @@ async function dividePages() {
function handleFileSelect(files: FileList | null) { function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) { if (files && files.length > 0) {
const file = files[0]; const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file; pageState.file = file;
updateUI(); updateUI();
} }
@@ -214,7 +245,10 @@ document.addEventListener('DOMContentLoaded', function () {
const files = e.dataTransfer?.files; const files = e.dataTransfer?.files;
if (files && files.length > 0) { if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) { const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'); return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
}); });
if (pdfFiles.length > 0) { if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer(); const dataTransfer = new DataTransfer();

View File

@@ -1,13 +1,20 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, getPDFDocument } from '../utils/helpers.js'; import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js'; import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import {
renderPagesProgressively,
cleanupLazyRendering,
} from '../utils/render-utils.js';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
const duplicateOrganizeState = { const duplicateOrganizeState = {
sortableInstances: {}, sortableInstances: {},
@@ -91,7 +98,15 @@ export async function renderDuplicateOrganizeThumbnails() {
showLoader('Rendering page previews...'); showLoader('Rendering page previews...');
const pdfData = await state.pdfDoc.save(); const pdfData = await state.pdfDoc.save();
const pdfjsDoc = await getPDFDocument({ data: pdfData }).promise; hideLoader();
const loadResult = await loadPdfWithPasswordPrompt(
state.files[0],
state.files,
0
);
if (!loadResult) return;
showLoader('Rendering page previews...');
const pdfjsDoc = loadResult.pdf;
grid.textContent = ''; grid.textContent = '';
@@ -148,11 +163,7 @@ export async function renderDuplicateOrganizeThumbnails() {
try { try {
// Render pages progressively with lazy loading // Render pages progressively with lazy loading
await renderPagesProgressively( await renderPagesProgressively(pdfjsDoc, grid, createWrapper, {
pdfjsDoc,
grid,
createWrapper,
{
batchSize: 8, batchSize: 8,
useLazyLoading: true, useLazyLoading: true,
lazyLoadMargin: '400px', lazyLoadMargin: '400px',
@@ -161,9 +172,8 @@ export async function renderDuplicateOrganizeThumbnails() {
}, },
onBatchComplete: () => { onBatchComplete: () => {
createIcons({ icons }); createIcons({ icons });
} },
} });
);
initializePageGridSortable(); initializePageGridSortable();
} catch (error) { } catch (error) {
@@ -181,8 +191,10 @@ export async function processAndSave() {
const finalPageElements = grid.querySelectorAll('.page-thumbnail'); const finalPageElements = grid.querySelectorAll('.page-thumbnail');
const finalIndices = Array.from(finalPageElements) const finalIndices = Array.from(finalPageElements)
.map((el) => parseInt((el as HTMLElement).dataset.originalPageIndex || '', 10)) .map((el) =>
.filter(index => !isNaN(index) && index >= 0); parseInt((el as HTMLElement).dataset.originalPageIndex || '', 10)
)
.filter((index) => !isNaN(index) && index >= 0);
console.log('Saving PDF with indices:', finalIndices); console.log('Saving PDF with indices:', finalIndices);
console.log('Original PDF Page Count:', state.pdfDoc?.getPageCount()); console.log('Original PDF Page Count:', state.pdfDoc?.getPageCount());
@@ -195,10 +207,13 @@ export async function processAndSave() {
const newPdfDoc = await PDFLibDocument.create(); const newPdfDoc = await PDFLibDocument.create();
const totalPages = state.pdfDoc.getPageCount(); const totalPages = state.pdfDoc.getPageCount();
const invalidIndices = finalIndices.filter(i => i >= totalPages); const invalidIndices = finalIndices.filter((i) => i >= totalPages);
if (invalidIndices.length > 0) { if (invalidIndices.length > 0) {
console.error('Found invalid indices:', invalidIndices); console.error('Found invalid indices:', invalidIndices);
showAlert('Error', 'Some pages could not be processed. Please try again.'); showAlert(
'Error',
'Some pages could not be processed. Please try again.'
);
return; return;
} }
@@ -212,7 +227,10 @@ export async function processAndSave() {
); );
} catch (e) { } catch (e) {
console.error('Save error:', e); console.error('Save error:', e);
showAlert('Error', 'Failed to save the new PDF. Check console for details.'); showAlert(
'Error',
'Failed to save the new PDF. Check console for details.'
);
} finally { } finally {
hideLoader(); hideLoader();
} }

View File

@@ -7,6 +7,7 @@ import {
showWasmRequiredDialog, showWasmRequiredDialog,
WasmProvider, WasmProvider,
} from '../utils/wasm-provider.js'; } from '../utils/wasm-provider.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
const worker = new Worker( const worker = new Worker(
import.meta.env.BASE_URL + 'workers/edit-attachments.worker.js' import.meta.env.BASE_URL + 'workers/edit-attachments.worker.js'
@@ -327,14 +328,17 @@ async function updateUI() {
} }
} }
function handleFileSelect(files: FileList | null) { async function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) { if (files && files.length > 0) {
const file = files[0]; const file = files[0];
if ( if (
file.type === 'application/pdf' || file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.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(); updateUI();
} }
} }

View File

@@ -3,6 +3,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js'; import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument, PDFName, PDFString } from 'pdf-lib'; import { PDFDocument as PDFLibDocument, PDFName, PDFString } from 'pdf-lib';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
const pageState: EditMetadataState = { const pageState: EditMetadataState = {
file: null, file: null,
@@ -23,14 +24,25 @@ function resetState() {
if (fileInput) fileInput.value = ''; if (fileInput) fileInput.value = '';
// Clear form fields // Clear form fields
const fields = ['meta-title', 'meta-author', 'meta-subject', 'meta-keywords', 'meta-creator', 'meta-producer', 'meta-creation-date', 'meta-mod-date']; const fields = [
'meta-title',
'meta-author',
'meta-subject',
'meta-keywords',
'meta-creator',
'meta-producer',
'meta-creation-date',
'meta-mod-date',
];
fields.forEach(function (fieldId) { fields.forEach(function (fieldId) {
const field = document.getElementById(fieldId) as HTMLInputElement; const field = document.getElementById(fieldId) as HTMLInputElement;
if (field) field.value = ''; if (field) field.value = '';
}); });
// Clear custom fields // Clear custom fields
const customFieldsContainer = document.getElementById('custom-fields-container'); const customFieldsContainer = document.getElementById(
'custom-fields-container'
);
if (customFieldsContainer) customFieldsContainer.innerHTML = ''; if (customFieldsContainer) customFieldsContainer.innerHTML = '';
} }
@@ -59,13 +71,15 @@ function addCustomFieldRow(key: string = '', value: string = '') {
keyInput.type = 'text'; keyInput.type = 'text';
keyInput.placeholder = 'Key (e.g., Department)'; keyInput.placeholder = 'Key (e.g., Department)';
keyInput.value = key; keyInput.value = key;
keyInput.className = 'custom-meta-key w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5'; keyInput.className =
'custom-meta-key w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5';
const valueInput = document.createElement('input'); const valueInput = document.createElement('input');
valueInput.type = 'text'; valueInput.type = 'text';
valueInput.placeholder = 'Value (e.g., Marketing)'; valueInput.placeholder = 'Value (e.g., Marketing)';
valueInput.value = value; valueInput.value = value;
valueInput.className = 'custom-meta-value w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5'; valueInput.className =
'custom-meta-value w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5';
const removeBtn = document.createElement('button'); const removeBtn = document.createElement('button');
removeBtn.type = 'button'; removeBtn.type = 'button';
@@ -84,13 +98,27 @@ function populateMetadataFields() {
if (!pageState.pdfDoc) return; if (!pageState.pdfDoc) return;
const titleInput = document.getElementById('meta-title') as HTMLInputElement; const titleInput = document.getElementById('meta-title') as HTMLInputElement;
const authorInput = document.getElementById('meta-author') as HTMLInputElement; const authorInput = document.getElementById(
const subjectInput = document.getElementById('meta-subject') as HTMLInputElement; 'meta-author'
const keywordsInput = document.getElementById('meta-keywords') as HTMLInputElement; ) as HTMLInputElement;
const creatorInput = document.getElementById('meta-creator') as HTMLInputElement; const subjectInput = document.getElementById(
const producerInput = document.getElementById('meta-producer') as HTMLInputElement; 'meta-subject'
const creationDateInput = document.getElementById('meta-creation-date') as HTMLInputElement; ) as HTMLInputElement;
const modDateInput = document.getElementById('meta-mod-date') as HTMLInputElement; const keywordsInput = document.getElementById(
'meta-keywords'
) as HTMLInputElement;
const creatorInput = document.getElementById(
'meta-creator'
) as HTMLInputElement;
const producerInput = document.getElementById(
'meta-producer'
) as HTMLInputElement;
const creationDateInput = document.getElementById(
'meta-creation-date'
) as HTMLInputElement;
const modDateInput = document.getElementById(
'meta-mod-date'
) as HTMLInputElement;
if (titleInput) titleInput.value = pageState.pdfDoc.getTitle() || ''; if (titleInput) titleInput.value = pageState.pdfDoc.getTitle() || '';
if (authorInput) authorInput.value = pageState.pdfDoc.getAuthor() || ''; if (authorInput) authorInput.value = pageState.pdfDoc.getAuthor() || '';
@@ -98,24 +126,38 @@ function populateMetadataFields() {
if (keywordsInput) keywordsInput.value = pageState.pdfDoc.getKeywords() || ''; if (keywordsInput) keywordsInput.value = pageState.pdfDoc.getKeywords() || '';
if (creatorInput) creatorInput.value = pageState.pdfDoc.getCreator() || ''; if (creatorInput) creatorInput.value = pageState.pdfDoc.getCreator() || '';
if (producerInput) producerInput.value = pageState.pdfDoc.getProducer() || ''; if (producerInput) producerInput.value = pageState.pdfDoc.getProducer() || '';
if (creationDateInput) creationDateInput.value = formatDateForInput(pageState.pdfDoc.getCreationDate()); if (creationDateInput)
if (modDateInput) modDateInput.value = formatDateForInput(pageState.pdfDoc.getModificationDate()); creationDateInput.value = formatDateForInput(
pageState.pdfDoc.getCreationDate()
);
if (modDateInput)
modDateInput.value = formatDateForInput(
pageState.pdfDoc.getModificationDate()
);
// Load custom fields // Load custom fields
const customFieldsContainer = document.getElementById('custom-fields-container'); const customFieldsContainer = document.getElementById(
'custom-fields-container'
);
if (customFieldsContainer) customFieldsContainer.innerHTML = ''; if (customFieldsContainer) customFieldsContainer.innerHTML = '';
try { try {
// @ts-expect-error getInfoDict is private but accessible at runtime // @ts-expect-error getInfoDict is private but accessible at runtime
const infoDict = pageState.pdfDoc.getInfoDict(); const infoDict = pageState.pdfDoc.getInfoDict();
const standardKeys = new Set([ const standardKeys = new Set([
'Title', 'Author', 'Subject', 'Keywords', 'Creator', 'Title',
'Producer', 'CreationDate', 'ModDate' 'Author',
'Subject',
'Keywords',
'Creator',
'Producer',
'CreationDate',
'ModDate',
]); ]);
const allKeys = infoDict const allKeys = infoDict.keys().map(function (key: {
.keys() asString: () => string;
.map(function (key: { asString: () => string }) { }) {
return key.asString().substring(1); return key.asString().substring(1);
}); });
@@ -150,7 +192,8 @@ async function updateUI() {
if (pageState.file) { if (pageState.file) {
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden'; infoContainer.className = 'flex flex-col overflow-hidden';
@@ -177,11 +220,16 @@ async function updateUI() {
createIcons({ icons }); createIcons({ icons });
try { try {
const result = await loadPdfWithPasswordPrompt(pageState.file);
if (!result) {
resetState();
return;
}
showLoader('Loading PDF...'); showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer(); result.pdf.destroy();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, { pageState.file = result.file;
ignoreEncryption: true, pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
throwOnInvalidObject: false throwOnInvalidObject: false,
}); });
hideLoader(); hideLoader();
@@ -211,14 +259,30 @@ async function saveMetadata() {
showLoader('Updating metadata...'); showLoader('Updating metadata...');
try { try {
const titleInput = document.getElementById('meta-title') as HTMLInputElement; const titleInput = document.getElementById(
const authorInput = document.getElementById('meta-author') as HTMLInputElement; 'meta-title'
const subjectInput = document.getElementById('meta-subject') as HTMLInputElement; ) as HTMLInputElement;
const keywordsInput = document.getElementById('meta-keywords') as HTMLInputElement; const authorInput = document.getElementById(
const creatorInput = document.getElementById('meta-creator') as HTMLInputElement; 'meta-author'
const producerInput = document.getElementById('meta-producer') as HTMLInputElement; ) as HTMLInputElement;
const creationDateInput = document.getElementById('meta-creation-date') as HTMLInputElement; const subjectInput = document.getElementById(
const modDateInput = document.getElementById('meta-mod-date') as HTMLInputElement; 'meta-subject'
) as HTMLInputElement;
const keywordsInput = document.getElementById(
'meta-keywords'
) as HTMLInputElement;
const creatorInput = document.getElementById(
'meta-creator'
) as HTMLInputElement;
const producerInput = document.getElementById(
'meta-producer'
) as HTMLInputElement;
const creationDateInput = document.getElementById(
'meta-creation-date'
) as HTMLInputElement;
const modDateInput = document.getElementById(
'meta-mod-date'
) as HTMLInputElement;
pageState.pdfDoc.setTitle(titleInput.value); pageState.pdfDoc.setTitle(titleInput.value);
pageState.pdfDoc.setAuthor(authorInput.value); pageState.pdfDoc.setAuthor(authorInput.value);
@@ -230,7 +294,9 @@ async function saveMetadata() {
pageState.pdfDoc.setKeywords( pageState.pdfDoc.setKeywords(
keywords keywords
.split(',') .split(',')
.map(function (k) { return k.trim(); }) .map(function (k) {
return k.trim();
})
.filter(Boolean) .filter(Boolean)
); );
@@ -250,14 +316,20 @@ async function saveMetadata() {
// @ts-expect-error getInfoDict is private but accessible at runtime // @ts-expect-error getInfoDict is private but accessible at runtime
const infoDict = pageState.pdfDoc.getInfoDict(); const infoDict = pageState.pdfDoc.getInfoDict();
const standardKeys = new Set([ const standardKeys = new Set([
'Title', 'Author', 'Subject', 'Keywords', 'Creator', 'Title',
'Producer', 'CreationDate', 'ModDate' 'Author',
'Subject',
'Keywords',
'Creator',
'Producer',
'CreationDate',
'ModDate',
]); ]);
// Remove existing custom keys // Remove existing custom keys
const allKeys = infoDict const allKeys = infoDict.keys().map(function (key: {
.keys() asString: () => string;
.map(function (key: { asString: () => string }) { }) {
return key.asString().substring(1); return key.asString().substring(1);
}); });
@@ -287,12 +359,20 @@ async function saveMetadata() {
`${originalName}_metadata-edited.pdf` `${originalName}_metadata-edited.pdf`
); );
showAlert('Success', 'Metadata updated successfully!', 'success', function () { showAlert(
'Success',
'Metadata updated successfully!',
'success',
function () {
resetState(); resetState();
}); }
);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showAlert('Error', 'Could not update metadata. Please check that date formats are correct.'); showAlert(
'Error',
'Could not update metadata. Please check that date formats are correct.'
);
} finally { } finally {
hideLoader(); hideLoader();
} }
@@ -301,7 +381,10 @@ async function saveMetadata() {
function handleFileSelect(files: FileList | null) { function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) { if (files && files.length > 0) {
const file = files[0]; const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file; pageState.file = file;
updateUI(); updateUI();
} }
@@ -348,7 +431,10 @@ document.addEventListener('DOMContentLoaded', function () {
const files = e.dataTransfer?.files; const files = e.dataTransfer?.files;
if (files && files.length > 0) { if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) { const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'); return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
}); });
if (pdfFiles.length > 0) { if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer(); const dataTransfer = new DataTransfer();

View File

@@ -3,6 +3,7 @@ import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js'; import { showAlert, showLoader, hideLoader } from '../ui.js';
import { formatBytes, downloadFile } from '../utils/helpers.js'; import { formatBytes, downloadFile } from '../utils/helpers.js';
import { makeUniqueFileKey } from '../utils/deduplicate-filename.js'; import { makeUniqueFileKey } from '../utils/deduplicate-filename.js';
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
const embedPdfWasmUrl = new URL( const embedPdfWasmUrl = new URL(
'embedpdf-snippet/dist/pdfium.wasm', 'embedpdf-snippet/dist/pdfium.wasm',
@@ -112,8 +113,17 @@ async function handleFiles(files: FileList) {
if (!pdfWrapper || !pdfContainer || !fileDisplayArea) return; if (!pdfWrapper || !pdfContainer || !fileDisplayArea) return;
hideLoader();
const decryptedFiles = await batchDecryptIfNeeded(pdfFiles);
showLoader('Loading PDF Editor...');
if (decryptedFiles.length === 0) {
hideLoader();
return;
}
if (!isViewerInitialized) { if (!isViewerInitialized) {
const firstFile = pdfFiles[0]; const firstFile = decryptedFiles[0];
const firstBuffer = await firstFile.arrayBuffer(); const firstBuffer = await firstFile.arrayBuffer();
pdfContainer.textContent = ''; pdfContainer.textContent = '';
@@ -163,7 +173,7 @@ async function handleFiles(files: FileList) {
} }
}); });
addFileEntries(fileDisplayArea, pdfFiles); addFileEntries(fileDisplayArea, decryptedFiles);
docManagerPlugin.openDocumentBuffer({ docManagerPlugin.openDocumentBuffer({
buffer: firstBuffer, buffer: firstBuffer,
@@ -171,11 +181,11 @@ async function handleFiles(files: FileList) {
autoActivate: true, autoActivate: true,
}); });
for (let i = 1; i < pdfFiles.length; i++) { for (let i = 1; i < decryptedFiles.length; i++) {
const buffer = await pdfFiles[i].arrayBuffer(); const buffer = await decryptedFiles[i].arrayBuffer();
docManagerPlugin.openDocumentBuffer({ docManagerPlugin.openDocumentBuffer({
buffer, buffer,
name: makeUniqueFileKey(i, pdfFiles[i].name), name: makeUniqueFileKey(i, decryptedFiles[i].name),
autoActivate: false, autoActivate: false,
}); });
} }
@@ -214,13 +224,13 @@ async function handleFiles(files: FileList) {
}); });
} }
} else { } else {
addFileEntries(fileDisplayArea, pdfFiles); addFileEntries(fileDisplayArea, decryptedFiles);
for (let i = 0; i < pdfFiles.length; i++) { for (let i = 0; i < decryptedFiles.length; i++) {
const buffer = await pdfFiles[i].arrayBuffer(); const buffer = await decryptedFiles[i].arrayBuffer();
docManagerPlugin.openDocumentBuffer({ docManagerPlugin.openDocumentBuffer({
buffer, buffer,
name: makeUniqueFileKey(i, pdfFiles[i].name), name: makeUniqueFileKey(i, decryptedFiles[i].name),
autoActivate: true, autoActivate: true,
}); });
} }

View File

@@ -1,4 +1,4 @@
import { showAlert } from '../ui.js'; import { showAlert, showLoader } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js'; import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import JSZip from 'jszip'; import JSZip from 'jszip';
@@ -7,6 +7,7 @@ import {
showWasmRequiredDialog, showWasmRequiredDialog,
WasmProvider, WasmProvider,
} from '../utils/wasm-provider.js'; } from '../utils/wasm-provider.js';
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
const worker = new Worker( const worker = new Worker(
import.meta.env.BASE_URL + 'workers/extract-attachments.worker.js' import.meta.env.BASE_URL + 'workers/extract-attachments.worker.js'
@@ -198,6 +199,9 @@ async function extractAttachments() {
showStatus('Reading files...', 'info'); showStatus('Reading files...', 'info');
try { try {
pageState.files = await batchDecryptIfNeeded(pageState.files);
showLoader('Reading files...');
const fileBuffers: ArrayBuffer[] = []; const fileBuffers: ArrayBuffer[] = [];
const fileNames: string[] = []; const fileNames: string[] = [];
@@ -207,6 +211,14 @@ async function extractAttachments() {
fileNames.push(file.name); fileNames.push(file.name);
} }
if (fileBuffers.length === 0) {
if (processBtn) {
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
processBtn.removeAttribute('disabled');
}
return;
}
showStatus( showStatus(
`Extracting attachments from ${pageState.files.length} file(s)...`, `Extracting attachments from ${pageState.files.length} file(s)...`,
'info' 'info'

View File

@@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide';
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
interface ExtractedImage { interface ExtractedImage {
data: Uint8Array; data: Uint8Array;
@@ -158,7 +159,9 @@ document.addEventListener('DOMContentLoaded', () => {
return; return;
} }
const decryptedFiles = await batchDecryptIfNeeded(state.files);
showLoader('Loading PDF processor...'); showLoader('Loading PDF processor...');
state.files = decryptedFiles;
const pymupdf = await loadPyMuPDF(); const pymupdf = await loadPyMuPDF();
extractedImages = []; extractedImages = [];

View File

@@ -1,7 +1,12 @@
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js'; import { showAlert, showLoader, hideLoader } from '../ui.js';
import { readFileAsArrayBuffer, formatBytes, downloadFile, parsePageRanges } from '../utils/helpers.js'; import {
formatBytes,
downloadFile,
parsePageRanges,
} from '../utils/helpers.js';
import { PDFDocument } from 'pdf-lib'; import { PDFDocument } from 'pdf-lib';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import JSZip from 'jszip'; import JSZip from 'jszip';
interface ExtractState { interface ExtractState {
@@ -75,18 +80,26 @@ function handleFileUpload(e: Event) {
} }
async function handleFile(file: File) { async function handleFile(file: File) {
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) { if (
file.type !== 'application/pdf' &&
!file.name.toLowerCase().endsWith('.pdf')
) {
showAlert('Invalid File', 'Please select a PDF file.'); showAlert('Invalid File', 'Please select a PDF file.');
return; return;
} }
showLoader('Loading PDF...');
extractState.file = file; extractState.file = file;
try { try {
const arrayBuffer = await readFileAsArrayBuffer(file); const result = await loadPdfWithPasswordPrompt(file);
extractState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { if (!result) {
ignoreEncryption: true, extractState.file = null;
return;
}
showLoader('Loading PDF...');
extractState.file = result.file;
result.pdf.destroy();
extractState.pdfDoc = await PDFDocument.load(result.bytes, {
throwOnInvalidObject: false, throwOnInvalidObject: false,
}); });
extractState.totalPages = extractState.pdfDoc.getPageCount(); extractState.totalPages = extractState.pdfDoc.getPageCount();
@@ -107,7 +120,8 @@ function updateFileDisplay() {
fileDisplayArea.innerHTML = ''; fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0'; infoContainer.className = 'flex flex-col flex-1 min-w-0';
@@ -144,16 +158,19 @@ function showOptions() {
} }
} }
async function extractPages() { async function extractPages() {
const pagesInput = document.getElementById('pages-to-extract') as HTMLInputElement; const pagesInput = document.getElementById(
'pages-to-extract'
) as HTMLInputElement;
if (!pagesInput || !pagesInput.value.trim()) { if (!pagesInput || !pagesInput.value.trim()) {
showAlert('No Pages', 'Please enter page numbers to extract.'); showAlert('No Pages', 'Please enter page numbers to extract.');
return; return;
} }
const pagesToExtract = parsePageRanges(pagesInput.value, extractState.totalPages).map(i => i + 1); const pagesToExtract = parsePageRanges(
pagesInput.value,
extractState.totalPages
).map((i) => i + 1);
if (pagesToExtract.length === 0) { if (pagesToExtract.length === 0) {
showAlert('Invalid Pages', 'No valid page numbers found.'); showAlert('Invalid Pages', 'No valid page numbers found.');
return; return;
@@ -167,7 +184,9 @@ async function extractPages() {
for (const pageNum of pagesToExtract) { for (const pageNum of pagesToExtract) {
const newPdf = await PDFDocument.create(); const newPdf = await PDFDocument.create();
const [copiedPage] = await newPdf.copyPages(extractState.pdfDoc, [pageNum - 1]); const [copiedPage] = await newPdf.copyPages(extractState.pdfDoc, [
pageNum - 1,
]);
newPdf.addPage(copiedPage); newPdf.addPage(copiedPage);
const pdfBytes = await newPdf.save(); const pdfBytes = await newPdf.save();
zip.file(`${baseName}_page_${pageNum}.pdf`, pdfBytes); zip.file(`${baseName}_page_${pageNum}.pdf`, pdfBytes);
@@ -177,9 +196,14 @@ async function extractPages() {
downloadFile(zipBlob, `${baseName}_extracted_pages.zip`); downloadFile(zipBlob, `${baseName}_extracted_pages.zip`);
hideLoader(); hideLoader();
showAlert('Success', `Extracted ${pagesToExtract.length} page(s) successfully!`, 'success', () => { showAlert(
'Success',
`Extracted ${pagesToExtract.length} page(s) successfully!`,
'success',
() => {
resetState(); resetState();
}); }
);
} catch (error) { } catch (error) {
console.error('Error extracting pages:', error); console.error('Error extracting pages:', error);
hideLoader(); hideLoader();
@@ -202,7 +226,9 @@ function resetState() {
fileDisplayArea.innerHTML = ''; fileDisplayArea.innerHTML = '';
} }
const pagesInput = document.getElementById('pages-to-extract') as HTMLInputElement; const pagesInput = document.getElementById(
'pages-to-extract'
) as HTMLInputElement;
if (pagesInput) { if (pagesInput) {
pagesInput.value = ''; pagesInput.value = '';
} }

View File

@@ -5,6 +5,7 @@ import JSZip from 'jszip';
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
let file: File | null = null; let file: File | null = null;
const updateUI = () => { const updateUI = () => {
@@ -93,6 +94,13 @@ async function extract() {
try { try {
showLoader('Loading Engine...'); showLoader('Loading Engine...');
const pymupdf = await loadPyMuPDF(); const pymupdf = await loadPyMuPDF();
hideLoader();
const pwResult = await loadPdfWithPasswordPrompt(file);
if (!pwResult) return;
pwResult.pdf.destroy();
file = pwResult.file;
showLoader('Extracting tables...'); showLoader('Extracting tables...');
const doc = await pymupdf.open(file); const doc = await pymupdf.open(file);

View File

@@ -1,6 +1,7 @@
import { showAlert } from '../ui.js'; import { showAlert } from '../ui.js';
import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js'; import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js';
import { fixPageSize as fixPageSizeCore } from '../utils/pdf-operations'; import { fixPageSize as fixPageSizeCore } from '../utils/pdf-operations';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import { FixPageSizeState } from '@/types'; import { FixPageSizeState } from '@/types';
@@ -64,14 +65,17 @@ async function updateUI() {
} }
} }
function handleFileSelect(files: FileList | null) { async function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) { if (files && files.length > 0) {
const file = files[0]; const file = files[0];
if ( if (
file.type === 'application/pdf' || file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.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(); updateUI();
} }
} }

View File

@@ -1,9 +1,6 @@
import { showAlert } from '../ui.js'; import { showAlert } from '../ui.js';
import { import { downloadFile, formatBytes } from '../utils/helpers.js';
downloadFile, import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
formatBytes,
readFileAsArrayBuffer,
} from '../utils/helpers.js';
import { PDFDocument } from 'pdf-lib'; import { PDFDocument } from 'pdf-lib';
import { flattenAnnotations } from '../utils/flatten-annotations.js'; import { flattenAnnotations } from '../utils/flatten-annotations.js';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
@@ -109,23 +106,22 @@ async function flattenPdf() {
const loaderModal = document.getElementById('loader-modal'); const loaderModal = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text'); const loaderText = document.getElementById('loader-text');
pageState.files = await batchDecryptIfNeeded(pageState.files);
try { try {
if (pageState.files.length === 1) { if (pageState.files.length === 1) {
if (loaderModal) loaderModal.classList.remove('hidden'); if (loaderModal) loaderModal.classList.remove('hidden');
if (loaderText) loaderText.textContent = 'Flattening PDF...'; if (loaderText) loaderText.textContent = 'Flattening PDF...';
const file = pageState.files[0]; const file = pageState.files[0];
const arrayBuffer = await readFileAsArrayBuffer(file); const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { const pdfDoc = await PDFDocument.load(arrayBuffer);
ignoreEncryption: true,
});
try { try {
flattenFormsInDoc(pdfDoc); flattenFormsInDoc(pdfDoc);
} catch (e: any) { } catch (e: unknown) {
if (e.message.includes('getForm')) { const msg = e instanceof Error ? e.message : String(e);
// Ignore if no form found if (!msg.includes('getForm')) {
} else {
throw e; throw e;
} }
} }
@@ -157,17 +153,14 @@ async function flattenPdf() {
loaderText.textContent = `Flattening ${i + 1}/${pageState.files.length}: ${file.name}...`; loaderText.textContent = `Flattening ${i + 1}/${pageState.files.length}: ${file.name}...`;
try { try {
const arrayBuffer = await readFileAsArrayBuffer(file); const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { const pdfDoc = await PDFDocument.load(arrayBuffer);
ignoreEncryption: true,
});
try { try {
flattenFormsInDoc(pdfDoc); flattenFormsInDoc(pdfDoc);
} catch (e: any) { } catch (e: unknown) {
if (e.message.includes('getForm')) { const msg = e instanceof Error ? e.message : String(e);
// Ignore if no form found if (!msg.includes('getForm')) {
} else {
throw e; throw e;
} }
} }
@@ -207,10 +200,12 @@ async function flattenPdf() {
} }
if (loaderModal) loaderModal.classList.add('hidden'); if (loaderModal) loaderModal.classList.add('hidden');
} }
} catch (e: any) { } catch (e: unknown) {
console.error(e); console.error(e);
if (loaderModal) loaderModal.classList.add('hidden'); if (loaderModal) loaderModal.classList.add('hidden');
showAlert('Error', e.message || 'An unexpected error occurred.'); const errorMessage =
e instanceof Error ? e.message : 'An unexpected error occurred.';
showAlert('Error', errorMessage);
} }
} }

View File

@@ -3,6 +3,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
import { convertFileToOutlines } from '../utils/ghostscript-loader.js'; import { convertFileToOutlines } from '../utils/ghostscript-loader.js';
import { isGhostscriptAvailable } from '../utils/ghostscript-dynamic-loader.js'; import { isGhostscriptAvailable } from '../utils/ghostscript-dynamic-loader.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import JSZip from 'jszip'; import JSZip from 'jszip';
import { deduplicateFileName } from '../utils/deduplicate-filename.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js';
@@ -107,6 +108,8 @@ async function processFiles() {
return; return;
} }
pageState.files = await batchDecryptIfNeeded(pageState.files);
const loaderModal = document.getElementById('loader-modal'); const loaderModal = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text'); const loaderText = document.getElementById('loader-text');

View File

@@ -29,6 +29,7 @@ type PdfViewerWindow = Window & {
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'; import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
import { downloadFile, hexToRgb, getPDFDocument } from '../utils/helpers.js'; import { downloadFile, hexToRgb, getPDFDocument } from '../utils/helpers.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import type { PDFDocumentProxy } from 'pdfjs-dist'; import type { PDFDocumentProxy } from 'pdfjs-dist';
@@ -3135,7 +3136,10 @@ function extractExistingFields(pdfDoc: PDFDocument): void {
async function handlePdfUpload(file: File) { async function handlePdfUpload(file: File) {
try { try {
const arrayBuffer = await file.arrayBuffer(); const result = await loadPdfWithPasswordPrompt(file);
if (!result) return;
const arrayBuffer = result.bytes;
uploadedPdfjsDoc = result.pdf;
uploadedPdfDoc = await PDFDocument.load(arrayBuffer); uploadedPdfDoc = await PDFDocument.load(arrayBuffer);
// Check for existing fields and update counter // Check for existing fields and update counter
@@ -3173,8 +3177,6 @@ async function handlePdfUpload(file: File) {
console.log('No form fields found or error reading fields:', e); console.log('No form fields found or error reading fields:', e);
} }
uploadedPdfjsDoc = await getPDFDocument({ data: arrayBuffer }).promise;
const pageCount = uploadedPdfDoc.getPageCount(); const pageCount = uploadedPdfDoc.getPageCount();
pages = []; pages = [];

View File

@@ -1,6 +1,7 @@
// Self-contained Form Filler logic for standalone page // Self-contained Form Filler logic for standalone page
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { getPDFDocument } from '../utils/helpers.js'; import { getPDFDocument } from '../utils/helpers.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
let viewerIframe: HTMLIFrameElement | null = null; let viewerIframe: HTMLIFrameElement | null = null;
let viewerReady = false; let viewerReady = false;
@@ -19,7 +20,12 @@ function hideLoader() {
if (loader) loader.classList.add('hidden'); if (loader) loader.classList.add('hidden');
} }
function showAlert(title: string, message: string, type: string = 'error', callback?: () => void) { function showAlert(
title: string,
message: string,
type: string = 'error',
callback?: () => void
) {
const modal = document.getElementById('alert-modal'); const modal = document.getElementById('alert-modal');
const alertTitle = document.getElementById('alert-title'); const alertTitle = document.getElementById('alert-title');
const alertMessage = document.getElementById('alert-message'); const alertMessage = document.getElementById('alert-message');
@@ -43,7 +49,8 @@ function updateFileDisplay() {
const displayArea = document.getElementById('file-display-area'); const displayArea = document.getElementById('file-display-area');
if (!displayArea || !currentFile) return; if (!displayArea || !currentFile) return;
const fileSize = currentFile.size < 1024 * 1024 const fileSize =
currentFile.size < 1024 * 1024
? `${(currentFile.size / 1024).toFixed(1)} KB` ? `${(currentFile.size / 1024).toFixed(1)} KB`
: `${(currentFile.size / 1024 / 1024).toFixed(2)} MB`; : `${(currentFile.size / 1024 / 1024).toFixed(2)} MB`;
@@ -63,7 +70,9 @@ function updateFileDisplay() {
createIcons({ icons }); createIcons({ icons });
document.getElementById('remove-file')?.addEventListener('click', () => resetState()); document
.getElementById('remove-file')
?.addEventListener('click', () => resetState());
} }
function resetState() { function resetState() {
@@ -99,9 +108,18 @@ async function handleFileUpload(file: File) {
return; return;
} }
currentFile = file; try {
const result = await loadPdfWithPasswordPrompt(file);
if (!result) return;
result.pdf.destroy();
currentFile = result.file;
updateFileDisplay(); updateFileDisplay();
await setupFormViewer(); await setupFormViewer();
} catch (error) {
console.error(error);
showAlert('Error', 'Failed to load PDF file.');
hideLoader();
}
} }
async function adjustViewerHeight(file: File) { async function adjustViewerHeight(file: File) {
@@ -180,7 +198,10 @@ async function setupFormViewer() {
async function processAndDownloadForm() { async function processAndDownloadForm() {
if (!viewerIframe || !viewerReady) { if (!viewerIframe || !viewerReady) {
showAlert('Viewer not ready', 'Please wait for the form to finish loading.'); showAlert(
'Viewer not ready',
'Please wait for the form to finish loading.'
);
return; return;
} }
@@ -188,35 +209,51 @@ async function processAndDownloadForm() {
const viewerWindow = viewerIframe.contentWindow; const viewerWindow = viewerIframe.contentWindow;
if (!viewerWindow) { if (!viewerWindow) {
console.error('Cannot access iframe window'); console.error('Cannot access iframe window');
showAlert('Download', 'Please use the Download button in the PDF viewer toolbar above.'); showAlert(
'Download',
'Please use the Download button in the PDF viewer toolbar above.'
);
return; return;
} }
const viewerDoc = viewerWindow.document; const viewerDoc = viewerWindow.document;
if (!viewerDoc) { if (!viewerDoc) {
console.error('Cannot access iframe document'); console.error('Cannot access iframe document');
showAlert('Download', 'Please use the Download button in the PDF viewer toolbar above.'); showAlert(
'Download',
'Please use the Download button in the PDF viewer toolbar above.'
);
return; return;
} }
const downloadBtn = viewerDoc.getElementById('downloadButton') as HTMLButtonElement | null; const downloadBtn = viewerDoc.getElementById(
'downloadButton'
) as HTMLButtonElement | null;
if (downloadBtn) { if (downloadBtn) {
console.log('Clicking download button...'); console.log('Clicking download button...');
downloadBtn.click(); downloadBtn.click();
} else { } else {
console.error('Download button not found in viewer'); console.error('Download button not found in viewer');
const secondaryDownload = viewerDoc.getElementById('secondaryDownload') as HTMLButtonElement | null; const secondaryDownload = viewerDoc.getElementById(
'secondaryDownload'
) as HTMLButtonElement | null;
if (secondaryDownload) { if (secondaryDownload) {
console.log('Clicking secondary download button...'); console.log('Clicking secondary download button...');
secondaryDownload.click(); secondaryDownload.click();
} else { } else {
showAlert('Download', 'Please use the Download button in the PDF viewer toolbar above.'); showAlert(
'Download',
'Please use the Download button in the PDF viewer toolbar above.'
);
} }
} }
} catch (e) { } catch (e) {
console.error('Failed to trigger download:', 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.'); showAlert(
'Download',
'Cannot access viewer controls. Please use the Download button in the PDF viewer toolbar above.'
);
} }
} }

View File

@@ -1,14 +1,22 @@
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js'; 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 { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import { HeaderFooterState } from '@/types'; import { HeaderFooterState } from '@/types';
const pageState: HeaderFooterState = { file: null, pdfDoc: null }; const pageState: HeaderFooterState = { file: null, pdfDoc: null };
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage); document.addEventListener('DOMContentLoaded', initializePage);
} else { initializePage(); } } else {
initializePage();
}
function initializePage() { function initializePage() {
createIcons({ icons }); createIcons({ icons });
@@ -19,36 +27,62 @@ function initializePage() {
if (fileInput) { if (fileInput) {
fileInput.addEventListener('change', handleFileUpload); fileInput.addEventListener('change', handleFileUpload);
fileInput.addEventListener('click', () => { fileInput.value = ''; }); fileInput.addEventListener('click', () => {
fileInput.value = '';
});
} }
if (dropZone) { if (dropZone) {
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-indigo-500'); }); dropZone.addEventListener('dragover', (e) => {
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('border-indigo-500'); }); e.preventDefault();
dropZone.classList.add('border-indigo-500');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('border-indigo-500');
});
dropZone.addEventListener('drop', (e) => { dropZone.addEventListener('drop', (e) => {
e.preventDefault(); dropZone.classList.remove('border-indigo-500'); e.preventDefault();
dropZone.classList.remove('border-indigo-500');
if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files); if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files);
}); });
} }
if (backBtn) backBtn.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; }); if (backBtn)
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
if (processBtn) processBtn.addEventListener('click', addHeaderFooter); if (processBtn) processBtn.addEventListener('click', addHeaderFooter);
} }
function handleFileUpload(e: Event) { const input = e.target as HTMLInputElement; if (input.files?.length) handleFiles(input.files); } function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement;
if (input.files?.length) handleFiles(input.files);
}
async function handleFiles(files: FileList) { async function handleFiles(files: FileList) {
const file = files[0]; const file = files[0];
if (!file || file.type !== 'application/pdf') { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; } if (!file || file.type !== 'application/pdf') {
showLoader('Loading PDF...'); showAlert('Invalid File', 'Please upload a valid PDF file.');
return;
}
try { try {
const arrayBuffer = await file.arrayBuffer(); const result = await loadPdfWithPasswordPrompt(file);
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer); if (!result) return;
pageState.file = file; showLoader('Loading PDF...');
pageState.pdfDoc = await PDFLibDocument.load(result.bytes);
pageState.file = result.file;
result.pdf.destroy();
updateFileDisplay(); updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden'); document.getElementById('options-panel')?.classList.remove('hidden');
const totalPagesSpan = document.getElementById('total-pages'); const totalPagesSpan = document.getElementById('total-pages');
if (totalPagesSpan) totalPagesSpan.textContent = String(pageState.pdfDoc.getPageCount()); if (totalPagesSpan)
} catch (error) { console.error(error); showAlert('Error', 'Failed to load PDF file.'); } totalPagesSpan.textContent = String(pageState.pdfDoc.getPageCount());
finally { hideLoader(); } } catch (error) {
console.error(error);
showAlert('Error', 'Failed to load PDF file.');
} finally {
hideLoader();
}
} }
function updateFileDisplay() { function updateFileDisplay() {
@@ -56,7 +90,8 @@ function updateFileDisplay() {
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return; if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
fileDisplayArea.innerHTML = ''; fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0'; infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div'); const nameSpan = document.createElement('div');
@@ -76,7 +111,8 @@ function updateFileDisplay() {
} }
function resetState() { function resetState() {
pageState.file = null; pageState.pdfDoc = null; pageState.file = null;
pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area'); const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = ''; if (fileDisplayArea) fileDisplayArea.innerHTML = '';
document.getElementById('options-panel')?.classList.add('hidden'); document.getElementById('options-panel')?.classList.add('hidden');
@@ -85,48 +121,140 @@ function resetState() {
} }
async function addHeaderFooter() { async function addHeaderFooter() {
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('Adding header & footer...'); showLoader('Adding header & footer...');
try { try {
const helveticaFont = await pageState.pdfDoc.embedFont(StandardFonts.Helvetica); const helveticaFont = await pageState.pdfDoc.embedFont(
StandardFonts.Helvetica
);
const allPages = pageState.pdfDoc.getPages(); const allPages = pageState.pdfDoc.getPages();
const totalPages = allPages.length; const totalPages = allPages.length;
const margin = 40; const margin = 40;
const fontSize = parseInt((document.getElementById('font-size') as HTMLInputElement)?.value || '10') || 10; const fontSize =
const colorHex = (document.getElementById('font-color') as HTMLInputElement)?.value || '#000000'; 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 fontColor = hexToRgb(colorHex);
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement)?.value || ''; const pageRangeInput =
(document.getElementById('page-range') as HTMLInputElement)?.value || '';
const texts = { const texts = {
headerLeft: (document.getElementById('header-left') as HTMLInputElement)?.value || '', headerLeft:
headerCenter: (document.getElementById('header-center') as HTMLInputElement)?.value || '', (document.getElementById('header-left') as HTMLInputElement)?.value ||
headerRight: (document.getElementById('header-right') as HTMLInputElement)?.value || '', '',
footerLeft: (document.getElementById('footer-left') as HTMLInputElement)?.value || '', headerCenter:
footerCenter: (document.getElementById('footer-center') as HTMLInputElement)?.value || '', (document.getElementById('header-center') as HTMLInputElement)?.value ||
footerRight: (document.getElementById('footer-right') 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); const indicesToProcess = parsePageRanges(pageRangeInput, totalPages);
if (indicesToProcess.length === 0) throw new Error("Invalid page range specified."); if (indicesToProcess.length === 0)
const drawOptions = { font: helveticaFont, size: fontSize, color: rgb(fontColor.r, fontColor.g, fontColor.b) }; 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) { for (const pageIndex of indicesToProcess) {
const page = allPages[pageIndex]; const page = allPages[pageIndex];
const { width, height } = page.getSize(); const { width, height } = page.getSize();
const pageNumber = pageIndex + 1; const pageNumber = pageIndex + 1;
const processText = (text: string) => text.replace(/{page}/g, String(pageNumber)).replace(/{total}/g, String(totalPages)); const processText = (text: string) =>
text
.replace(/{page}/g, String(pageNumber))
.replace(/{total}/g, String(totalPages));
const processed = { const processed = {
headerLeft: processText(texts.headerLeft), headerCenter: processText(texts.headerCenter), headerRight: processText(texts.headerRight), headerLeft: processText(texts.headerLeft),
footerLeft: processText(texts.footerLeft), footerCenter: processText(texts.footerCenter), footerRight: processText(texts.footerRight), 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.headerLeft)
if (processed.headerCenter) page.drawText(processed.headerCenter, { ...drawOptions, x: width / 2 - helveticaFont.widthOfTextAtSize(processed.headerCenter, fontSize) / 2, y: height - margin }); page.drawText(processed.headerLeft, {
if (processed.headerRight) page.drawText(processed.headerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processed.headerRight, fontSize), y: height - margin }); ...drawOptions,
if (processed.footerLeft) page.drawText(processed.footerLeft, { ...drawOptions, x: margin, y: margin }); x: margin,
if (processed.footerCenter) page.drawText(processed.footerCenter, { ...drawOptions, x: width / 2 - helveticaFont.widthOfTextAtSize(processed.footerCenter, fontSize) / 2, y: margin }); y: height - margin,
if (processed.footerRight) page.drawText(processed.footerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processed.footerRight, fontSize), y: 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(); const newPdfBytes = await pageState.pdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'header-footer-added.pdf'); downloadFile(
showAlert('Success', 'Header & Footer added successfully!', 'success', () => { resetState(); }); new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
} catch (e: any) { console.error(e); showAlert('Error', e.message || 'Could not add header or footer.'); } 'header-footer-added.pdf'
finally { hideLoader(); } );
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();
}
} }

View File

@@ -5,6 +5,7 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { applyInvertColors } from '../utils/image-effects.js'; import { applyInvertColors } from '../utils/image-effects.js';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { InvertColorsState } from '@/types'; import { InvertColorsState } from '@/types';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', '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.'); showAlert('Invalid File', 'Please upload a valid PDF file.');
return; return;
} }
showLoader('Loading PDF...');
try { try {
const arrayBuffer = await file.arrayBuffer(); const result = await loadPdfWithPasswordPrompt(file);
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer); if (!result) return;
pageState.file = file; showLoader('Loading PDF...');
result.pdf.destroy();
pageState.pdfDoc = await PDFLibDocument.load(result.bytes);
pageState.file = result.file;
updateFileDisplay(); updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden'); document.getElementById('options-panel')?.classList.remove('hidden');
} catch (error) { } catch (error) {

View File

@@ -1,10 +1,7 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { import { downloadFile } from '../utils/helpers.js';
downloadFile,
readFileAsArrayBuffer,
getPDFDocument,
} from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
import { import {
renderPagesProgressively, renderPagesProgressively,
cleanupLazyRendering, cleanupLazyRendering,
@@ -453,15 +450,23 @@ export async function refreshMergeUI() {
mergeState.pdfDocs = {}; mergeState.pdfDocs = {};
mergeState.pdfBytes = {}; mergeState.pdfBytes = {};
hideLoader();
state.files = await batchDecryptIfNeeded(state.files);
showLoader('Loading PDF documents...');
for (let i = 0; i < state.files.length; i++) { for (let i = 0; i < state.files.length; i++) {
const file = state.files[i]; const file = state.files[i];
const fileKey = `${i}_${file.name}`; const fileKey = `${i}_${file.name}`;
const pdfBytes = await readFileAsArrayBuffer(file);
mergeState.pdfBytes[fileKey] = pdfBytes as ArrayBuffer;
const bytesForPdfJs = (pdfBytes as ArrayBuffer).slice(0); const bytes = await file.arrayBuffer();
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise; const pdf = await pdfjsLib.getDocument({ data: bytes.slice(0) }).promise;
mergeState.pdfDocs[fileKey] = pdfjsDoc; mergeState.pdfBytes[fileKey] = bytes;
mergeState.pdfDocs[fileKey] = pdf;
}
if (state.files.length === 0) {
hideLoader();
return;
} }
} catch (error) { } catch (error) {
console.error('Error loading PDFs:', error); console.error('Error loading PDFs:', error);

View File

@@ -2,6 +2,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js'; import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib'; import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
interface NUpState { interface NUpState {
file: File | null; file: File | null;
@@ -37,7 +38,8 @@ async function updateUI() {
if (pageState.file) { if (pageState.file) {
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden'; infoContainer.className = 'flex flex-col overflow-hidden';
@@ -64,11 +66,16 @@ async function updateUI() {
createIcons({ icons }); createIcons({ icons });
try { try {
const result = await loadPdfWithPasswordPrompt(pageState.file);
if (!result) {
resetState();
return;
}
showLoader('Loading PDF...'); showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer(); result.pdf.destroy();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, { pageState.file = result.file;
ignoreEncryption: true, pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
throwOnInvalidObject: false throwOnInvalidObject: false,
}); });
hideLoader(); hideLoader();
@@ -93,12 +100,23 @@ async function nUpTool() {
return; return;
} }
const n = parseInt((document.getElementById('pages-per-sheet') as HTMLSelectElement).value); const n = parseInt(
const pageSizeKey = (document.getElementById('output-page-size') as HTMLSelectElement).value as keyof typeof PageSizes; (document.getElementById('pages-per-sheet') as HTMLSelectElement).value
let orientation = (document.getElementById('output-orientation') as HTMLSelectElement).value; );
const useMargins = (document.getElementById('add-margins') as HTMLInputElement).checked; const pageSizeKey = (
const addBorder = (document.getElementById('add-border') as HTMLInputElement).checked; document.getElementById('output-page-size') as HTMLSelectElement
const borderColor = hexToRgb((document.getElementById('border-color') as HTMLInputElement).value); ).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...'); showLoader('Creating N-Up PDF...');
@@ -107,7 +125,12 @@ async function nUpTool() {
const newDoc = await PDFLibDocument.create(); const newDoc = await PDFLibDocument.create();
const sourcePages = sourceDoc.getPages(); const sourcePages = sourceDoc.getPages();
const gridDims: Record<number, [number, number]> = { 2: [2, 1], 4: [2, 2], 9: [3, 3], 16: [4, 4] }; const gridDims: Record<number, [number, number]> = {
2: [2, 1],
4: [2, 2],
9: [3, 3],
16: [4, 4],
};
const dims = gridDims[n]; const dims = gridDims[n];
let [pageWidth, pageHeight] = PageSizes[pageSizeKey]; let [pageWidth, pageHeight] = PageSizes[pageSizeKey];
@@ -115,7 +138,8 @@ async function nUpTool() {
if (orientation === 'auto') { if (orientation === 'auto') {
const firstPage = sourcePages[0]; const firstPage = sourcePages[0];
const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight(); const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight();
orientation = isSourceLandscape && dims[0] > dims[1] ? 'landscape' : 'portrait'; orientation =
isSourceLandscape && dims[0] > dims[1] ? 'landscape' : 'portrait';
} }
if (orientation === 'landscape' && pageWidth < pageHeight) { if (orientation === 'landscape' && pageWidth < pageHeight) {
@@ -150,7 +174,8 @@ async function nUpTool() {
const row = Math.floor(j / dims[0]); const row = Math.floor(j / dims[0]);
const col = j % dims[0]; const col = j % dims[0];
const cellX = margin + col * (cellWidth + gutter); const cellX = margin + col * (cellWidth + gutter);
const cellY = pageHeight - margin - (row + 1) * cellHeight - row * gutter; const cellY =
pageHeight - margin - (row + 1) * cellHeight - row * gutter;
const x = cellX + (cellWidth - scaledWidth) / 2; const x = cellX + (cellWidth - scaledWidth) / 2;
const y = cellY + (cellHeight - scaledHeight) / 2; const y = cellY + (cellHeight - scaledHeight) / 2;
@@ -183,9 +208,14 @@ async function nUpTool() {
`${originalName}_${n}-up.pdf` `${originalName}_${n}-up.pdf`
); );
showAlert('Success', 'N-Up PDF created successfully!', 'success', function () { showAlert(
'Success',
'N-Up PDF created successfully!',
'success',
function () {
resetState(); resetState();
}); }
);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showAlert('Error', 'An error occurred while creating the N-Up PDF.'); showAlert('Error', 'An error occurred while creating the N-Up PDF.');
@@ -197,7 +227,10 @@ async function nUpTool() {
function handleFileSelect(files: FileList | null) { function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) { if (files && files.length > 0) {
const file = files[0]; const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file; pageState.file = file;
updateUI(); updateUI();
} }
@@ -220,7 +253,10 @@ document.addEventListener('DOMContentLoaded', function () {
if (addBorderCheckbox && borderColorWrapper) { if (addBorderCheckbox && borderColorWrapper) {
addBorderCheckbox.addEventListener('change', function () { addBorderCheckbox.addEventListener('change', function () {
borderColorWrapper.classList.toggle('hidden', !(addBorderCheckbox as HTMLInputElement).checked); borderColorWrapper.classList.toggle(
'hidden',
!(addBorderCheckbox as HTMLInputElement).checked
);
}); });
} }
@@ -245,7 +281,10 @@ document.addEventListener('DOMContentLoaded', function () {
const files = e.dataTransfer?.files; const files = e.dataTransfer?.files;
if (files && files.length > 0) { if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) { const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'); return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
}); });
if (pdfFiles.length > 0) { if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer(); const dataTransfer = new DataTransfer();

View File

@@ -1,6 +1,7 @@
import { tesseractLanguages } from '../config/tesseract-languages.js'; import { tesseractLanguages } from '../config/tesseract-languages.js';
import { showAlert } from '../ui.js'; import { showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js'; import { downloadFile, formatBytes } from '../utils/helpers.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import { OcrState } from '@/types'; import { OcrState } from '@/types';
import { performOcr } from '../utils/ocr.js'; 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) { if (files && files.length > 0) {
const file = files[0]; const file = files[0];
if ( if (
file.type === 'application/pdf' || file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.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(); updateUI();
} }
} }

View File

@@ -1,13 +1,9 @@
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js'; import { showAlert, showLoader, hideLoader } from '../ui.js';
import { import { formatBytes, downloadFile } from '../utils/helpers.js';
readFileAsArrayBuffer,
formatBytes,
downloadFile,
getPDFDocument,
} from '../utils/helpers.js';
import { initPagePreview } from '../utils/page-preview.js'; import { initPagePreview } from '../utils/page-preview.js';
import { PDFDocument } from 'pdf-lib'; import { PDFDocument } from 'pdf-lib';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
@@ -173,18 +169,18 @@ async function handleFile(file: File) {
return; return;
} }
showLoader('Loading PDF...');
organizeState.file = file; organizeState.file = file;
try { try {
const arrayBuffer = await readFileAsArrayBuffer(file); const result = await loadPdfWithPasswordPrompt(file);
organizeState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { if (!result) return;
ignoreEncryption: true, showLoader('Loading PDF...');
organizeState.pdfDoc = await PDFDocument.load(result.bytes, {
throwOnInvalidObject: false, throwOnInvalidObject: false,
}); });
organizeState.pdfJsDoc = await getPDFDocument({ organizeState.pdfJsDoc = result.pdf;
data: (arrayBuffer as ArrayBuffer).slice(0), organizeState.file = result.file;
}).promise;
organizeState.totalPages = organizeState.pdfDoc.getPageCount(); organizeState.totalPages = organizeState.pdfDoc.getPageCount();
updateFileDisplay(); updateFileDisplay();

View File

@@ -1,5 +1,10 @@
import { showAlert } from '../ui.js'; 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 { PDFDocument } from 'pdf-lib';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import { PageDimensionsState } from '@/types'; import { PageDimensionsState } from '@/types';
@@ -18,8 +23,8 @@ function calculateAspectRatio(width: number, height: number): string {
function calculateArea(width: number, height: number, unit: string): string { function calculateArea(width: number, height: number, unit: string): string {
const areaInPoints = width * height; const areaInPoints = width * height;
let convertedArea = 0; let convertedArea: number;
let unitSuffix = ''; let unitSuffix: string;
switch (unit) { switch (unit) {
case 'in': case 'in':
@@ -27,7 +32,7 @@ function calculateArea(width: number, height: number, unit: string): string {
unitSuffix = 'in²'; unitSuffix = 'in²';
break; break;
case 'mm': case 'mm':
convertedArea = areaInPoints / (72 * 72) * (25.4 * 25.4); convertedArea = (areaInPoints / (72 * 72)) * (25.4 * 25.4);
unitSuffix = 'mm²'; unitSuffix = 'mm²';
break; break;
case 'px': case 'px':
@@ -55,7 +60,7 @@ function getSummaryStats() {
count: (uniqueSizes.get(key)?.count || 0) + 1, count: (uniqueSizes.get(key)?.count || 0) + 1,
label: label, label: label,
width: pageData.width, width: pageData.width,
height: pageData.height height: pageData.height,
}); });
}); });
@@ -65,7 +70,7 @@ function getSummaryStats() {
totalPages, totalPages,
uniqueSizesCount: uniqueSizes.size, uniqueSizesCount: uniqueSizes.size,
uniqueSizes: Array.from(uniqueSizes.values()), uniqueSizes: Array.from(uniqueSizes.values()),
hasMixedSizes hasMixedSizes,
}; };
} }
@@ -103,9 +108,13 @@ function renderSummary() {
<h4 class="text-yellow-200 font-semibold mb-2">Mixed Page Sizes Detected</h4> <h4 class="text-yellow-200 font-semibold mb-2">Mixed Page Sizes Detected</h4>
<p class="text-sm text-gray-300 mb-3">This document contains pages with different dimensions:</p> <p class="text-sm text-gray-300 mb-3">This document contains pages with different dimensions:</p>
<ul class="space-y-1 text-sm text-gray-300"> <ul class="space-y-1 text-sm text-gray-300">
${stats.uniqueSizes.map((size: any) => ` ${stats.uniqueSizes
.map(
(size: any) => `
<li>• ${size.label}: ${size.count} page${size.count > 1 ? 's' : ''}</li> <li>• ${size.label}: ${size.count} page${size.count > 1 ? 's' : ''}</li>
`).join('')} `
)
.join('')}
</ul> </ul>
</div> </div>
</div> </div>
@@ -162,16 +171,35 @@ function renderTable(unit: string) {
rotationCell.className = 'px-4 py-3 text-gray-300'; rotationCell.className = 'px-4 py-3 text-gray-300';
rotationCell.textContent = `${pageData.rotation}°`; rotationCell.textContent = `${pageData.rotation}°`;
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell, aspectRatioCell, areaCell, rotationCell); row.append(
pageNumCell,
dimensionsCell,
sizeCell,
orientationCell,
aspectRatioCell,
areaCell,
rotationCell
);
tableBody.appendChild(row); tableBody.appendChild(row);
}); });
} }
function exportToCSV() { function exportToCSV() {
const unitsSelect = document.getElementById('units-select') as HTMLSelectElement; const unitsSelect = document.getElementById(
'units-select'
) as HTMLSelectElement;
const unit = unitsSelect?.value || 'pt'; const unit = unitsSelect?.value || 'pt';
const headers = ['Page #', `Width (${unit})`, `Height (${unit})`, 'Standard Size', 'Orientation', 'Aspect Ratio', `Area (${unit}²)`, 'Rotation']; const headers = [
'Page #',
`Width (${unit})`,
`Height (${unit})`,
'Standard Size',
'Orientation',
'Aspect Ratio',
`Area (${unit}²)`,
'Rotation',
];
const csvRows = [headers.join(',')]; const csvRows = [headers.join(',')];
analyzedPagesData.forEach((pageData: any) => { analyzedPagesData.forEach((pageData: any) => {
@@ -188,7 +216,7 @@ function exportToCSV() {
pageData.orientation, pageData.orientation,
aspectRatio, aspectRatio,
area, area,
`${pageData.rotation}°` `${pageData.rotation}°`,
]; ];
csvRows.push(row.join(',')); csvRows.push(row.join(','));
}); });
@@ -221,12 +249,14 @@ function analyzeAndDisplayDimensions() {
height, height,
orientation: width > height ? 'Landscape' : 'Portrait', orientation: width > height ? 'Landscape' : 'Portrait',
standardSize: getStandardPageName(width, height), standardSize: getStandardPageName(width, height),
rotation: rotation rotation: rotation,
}); });
}); });
const resultsContainer = document.getElementById('dimensions-results'); const resultsContainer = document.getElementById('dimensions-results');
const unitsSelect = document.getElementById('units-select') as HTMLSelectElement; const unitsSelect = document.getElementById(
'units-select'
) as HTMLSelectElement;
renderSummary(); renderSummary();
renderTable(unitsSelect.value); renderTable(unitsSelect.value);
@@ -269,7 +299,8 @@ async function updateUI() {
if (pageState.file) { if (pageState.file) {
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden'; infoContainer.className = 'flex flex-col overflow-hidden';
@@ -300,12 +331,17 @@ async function updateUI() {
async function handleFileSelect(files: FileList | null) { async function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) { if (files && files.length > 0) {
const file = files[0]; const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { if (
pageState.file = file; file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
try { try {
const arrayBuffer = await file.arrayBuffer(); const result = await loadPdfWithPasswordPrompt(file);
pageState.pdfDoc = await PDFDocument.load(arrayBuffer); if (!result) return;
result.pdf.destroy();
pageState.file = result.file;
pageState.pdfDoc = await PDFDocument.load(result.bytes);
updateUI(); updateUI();
analyzeAndDisplayDimensions(); analyzeAndDisplayDimensions();
} catch (e) { } catch (e) {

View File

@@ -2,6 +2,7 @@ import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js'; import { showAlert, showLoader, hideLoader } from '../ui.js';
import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js'; import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import { import {
addPageNumbers as addPageNumbersToPdf, addPageNumbers as addPageNumbersToPdf,
type PageNumberPosition, type PageNumberPosition,
@@ -83,11 +84,14 @@ async function handleFiles(files: FileList) {
return; return;
} }
showLoader('Loading PDF...');
try { try {
const arrayBuffer = await file.arrayBuffer(); const result = await loadPdfWithPasswordPrompt(file);
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer); if (!result) return;
pageState.file = file; showLoader('Loading PDF...');
pageState.pdfDoc = await PDFLibDocument.load(result.bytes);
pageState.file = result.file;
result.pdf.destroy();
updateFileDisplay(); updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden'); document.getElementById('options-panel')?.classList.remove('hidden');

View File

@@ -3,6 +3,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument, degrees, PageSizes } from 'pdf-lib'; import { PDFDocument as PDFLibDocument, degrees, PageSizes } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.mjs', 'pdfjs-dist/build/pdf.worker.mjs',
@@ -87,19 +88,20 @@ async function updateUI() {
createIcons({ icons }); createIcons({ icons });
try { try {
const result = await loadPdfWithPasswordPrompt(pageState.file);
if (!result) {
resetState();
return;
}
showLoader('Loading PDF...'); showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer(); pageState.file = result.file;
pageState.pdfBytes = new Uint8Array(arrayBuffer); pageState.pdfBytes = new Uint8Array(result.bytes);
pageState.pdfjsDoc = result.pdf;
pageState.pdfDoc = await PDFLibDocument.load(pageState.pdfBytes, { pageState.pdfDoc = await PDFLibDocument.load(pageState.pdfBytes, {
ignoreEncryption: true,
throwOnInvalidObject: false, throwOnInvalidObject: false,
}); });
pageState.pdfjsDoc = await pdfjsLib.getDocument({
data: pageState.pdfBytes.slice(),
}).promise;
hideLoader(); hideLoader();
const pageCount = pageState.pdfDoc.getPageCount(); const pageCount = pageState.pdfDoc.getPageCount();

View File

@@ -10,6 +10,7 @@ import { createIcons, icons } from 'lucide';
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
interface LayerData { interface LayerData {
number: number; 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) { if (files && files.length > 0) {
const file = files[0]; const file = files[0];
if ( if (
file.type === 'application/pdf' || file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf') file.name.toLowerCase().endsWith('.pdf')
) { ) {
currentFile = file; const result = await loadPdfWithPasswordPrompt(file);
if (!result) return;
result.pdf.destroy();
currentFile = result.file;
updateUI(); updateUI();
} else { } else {
showAlert('Invalid File', 'Please select a PDF file.'); showAlert('Invalid File', 'Please select a PDF file.');

View File

@@ -4,6 +4,7 @@ import * as pdfjsLib from 'pdfjs-dist';
import JSZip from 'jszip'; import JSZip from 'jszip';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { downloadFile, getPDFDocument } from '../utils/helpers'; import { downloadFile, getPDFDocument } from '../utils/helpers';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import { import {
renderPagesProgressively, renderPagesProgressively,
cleanupLazyRendering, cleanupLazyRendering,
@@ -428,6 +429,12 @@ async function loadPdfs(files: File[]) {
arrayBuffer = await file.arrayBuffer(); 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, { const pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true, ignoreEncryption: true,
throwOnInvalidObject: false, throwOnInvalidObject: false,
@@ -848,15 +855,17 @@ async function handleInsertPdf(e: Event) {
if (insertAfterIndex === undefined) return; if (insertAfterIndex === undefined) return;
try { try {
const arrayBuffer = await file.arrayBuffer(); const pwResult = await loadPdfWithPasswordPrompt(file);
const pdfDoc = await PDFLibDocument.load(arrayBuffer, { if (!pwResult) return;
pwResult.pdf.destroy();
const pdfDoc = await PDFLibDocument.load(pwResult.bytes, {
ignoreEncryption: true, ignoreEncryption: true,
throwOnInvalidObject: false, throwOnInvalidObject: false,
}); });
currentPdfDocs.push(pdfDoc); currentPdfDocs.push(pdfDoc);
const pdfIndex = currentPdfDocs.length - 1; const pdfIndex = currentPdfDocs.length - 1;
// Load PDF.js document for rendering
const pdfBytes = await pdfDoc.save(); const pdfBytes = await pdfDoc.save();
const pdfjsDoc = await getPDFDocument({ data: new Uint8Array(pdfBytes) }) const pdfjsDoc = await getPDFDocument({ data: new Uint8Array(pdfBytes) })
.promise; .promise;

View File

@@ -11,6 +11,7 @@ import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { PDFPageProxy } from 'pdfjs-dist'; import { PDFPageProxy } from 'pdfjs-dist';
import { t } from '../i18n/i18n'; import { t } from '../i18n/i18n';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@@ -98,10 +99,11 @@ async function convert() {
); );
return; return;
} }
showLoader(t('tools:pdfToBmp.loader.converting'));
try { try {
const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) const result = await loadPdfWithPasswordPrompt(files[0], files, 0);
.promise; if (!result) return;
showLoader(t('tools:pdfToBmp.loader.converting'));
const { pdf } = result;
if (pdf.numPages === 1) { if (pdf.numPages === 1) {
const page = await pdf.getPage(1); const page = await pdf.getPage(1);

View File

@@ -17,6 +17,7 @@ import {
generateComicBookInfoJson, generateComicBookInfoJson,
} from '../utils/comic-info.js'; } from '../utils/comic-info.js';
import type { CbzOptions, ComicMetadata } from '@/types'; import type { CbzOptions, ComicMetadata } from '@/types';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@@ -229,8 +230,11 @@ async function convert() {
try { try {
const options = getOptions(); const options = getOptions();
const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) hideLoader();
.promise; const result = await loadPdfWithPasswordPrompt(files[0], files, 0);
if (!result) return;
showLoader(t('tools:pdfToCbz.converting'));
const { pdf } = result;
if (pdf.numPages === 0) { if (pdf.numPages === 0) {
throw new Error('PDF has no pages'); throw new Error('PDF has no pages');

View File

@@ -5,6 +5,7 @@ import JSZip from 'jszip';
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
let file: File | null = null; let file: File | null = null;
const updateUI = () => { const updateUI = () => {
@@ -86,6 +87,13 @@ async function convert() {
try { try {
const pymupdf = await loadPyMuPDF(); const pymupdf = await loadPyMuPDF();
hideLoader();
const pwResult = await loadPdfWithPasswordPrompt(file);
if (!pwResult) return;
pwResult.pdf.destroy();
file = pwResult.file;
showLoader('Extracting tables...'); showLoader('Extracting tables...');
const doc = await pymupdf.open(file); const doc = await pymupdf.open(file);

View File

@@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide';
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@@ -105,6 +106,10 @@ document.addEventListener('DOMContentLoaded', () => {
showLoader('Loading PDF converter...'); showLoader('Loading PDF converter...');
const pymupdf = await loadPyMuPDF(); const pymupdf = await loadPyMuPDF();
hideLoader();
state.files = await batchDecryptIfNeeded(state.files);
showLoader('Converting...');
if (state.files.length === 1) { if (state.files.length === 1) {
const file = state.files[0]; const file = state.files[0];
showLoader(`Converting ${file.name}...`); showLoader(`Converting ${file.name}...`);
@@ -122,7 +127,6 @@ document.addEventListener('DOMContentLoaded', () => {
() => resetState() () => resetState()
); );
} else { } else {
showLoader('Converting multiple PDFs...');
const JSZip = (await import('jszip')).default; const JSZip = (await import('jszip')).default;
const zip = new JSZip(); const zip = new JSZip();
const usedNames = new Set<string>(); const usedNames = new Set<string>();

View File

@@ -4,6 +4,7 @@ import { createIcons, icons } from 'lucide';
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
let file: File | null = null; let file: File | null = null;
@@ -66,6 +67,13 @@ async function convert() {
try { try {
const pymupdf = await loadPyMuPDF(); const pymupdf = await loadPyMuPDF();
hideLoader();
const pwResult = await loadPdfWithPasswordPrompt(file);
if (!pwResult) return;
pwResult.pdf.destroy();
file = pwResult.file;
showLoader('Extracting tables...'); showLoader('Extracting tables...');
const doc = await pymupdf.open(file); const doc = await pymupdf.open(file);

View File

@@ -10,6 +10,7 @@ import { PDFDocument } from 'pdf-lib';
import { applyGreyscale } from '../utils/image-effects.js'; import { applyGreyscale } from '../utils/image-effects.js';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { t } from '../i18n/i18n'; import { t } from '../i18n/i18n';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@@ -94,13 +95,11 @@ async function convert() {
showAlert('No File', 'Please upload a PDF file first.'); showAlert('No File', 'Please upload a PDF file first.');
return; return;
} }
showLoader('Converting to greyscale...');
try { try {
const pdfBytes = (await readFileAsArrayBuffer(files[0])) as ArrayBuffer; const result = await loadPdfWithPasswordPrompt(files[0], files, 0);
const pdfDoc = await PDFDocument.load(pdfBytes); if (!result) return;
const pages = pdfDoc.getPages(); showLoader('Converting to greyscale...');
const { pdf: pdfjsDoc } = result;
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
const newPdfDoc = await PDFDocument.create(); const newPdfDoc = await PDFDocument.create();
for (let i = 1; i <= pdfjsDoc.numPages; i++) { for (let i = 1; i <= pdfjsDoc.numPages; i++) {

View File

@@ -11,6 +11,7 @@ import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { PDFPageProxy } from 'pdfjs-dist'; import { PDFPageProxy } from 'pdfjs-dist';
import { t } from '../i18n/i18n'; import { t } from '../i18n/i18n';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@@ -103,10 +104,11 @@ async function convert() {
); );
return; return;
} }
showLoader(t('tools:pdfToJpg.loader.converting'));
try { try {
const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) const result = await loadPdfWithPasswordPrompt(files[0], files, 0);
.promise; if (!result) return;
showLoader(t('tools:pdfToJpg.loader.converting'));
const { pdf } = result;
const qualityInput = document.getElementById( const qualityInput = document.getElementById(
'jpg-quality' 'jpg-quality'

View File

@@ -11,6 +11,7 @@ import {
showWasmRequiredDialog, showWasmRequiredDialog,
WasmProvider, WasmProvider,
} from '../utils/wasm-provider.js'; } from '../utils/wasm-provider.js';
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
const worker = new Worker( const worker = new Worker(
import.meta.env.BASE_URL + 'workers/pdf-to-json.worker.js' import.meta.env.BASE_URL + 'workers/pdf-to-json.worker.js'
@@ -105,6 +106,10 @@ async function convertPDFsToJSON() {
try { try {
convertBtn.disabled = true; convertBtn.disabled = true;
showStatus('Checking for encrypted PDFs...', 'info');
selectedFiles = await batchDecryptIfNeeded(selectedFiles);
showStatus('Reading files (Main Thread)...', 'info'); showStatus('Reading files (Main Thread)...', 'info');
const fileBuffers = await Promise.all( const fileBuffers = await Promise.all(

View File

@@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide';
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@@ -110,6 +111,10 @@ document.addEventListener('DOMContentLoaded', () => {
const includeImages = includeImagesCheckbox?.checked ?? false; const includeImages = includeImagesCheckbox?.checked ?? false;
hideLoader();
state.files = await batchDecryptIfNeeded(state.files);
showLoader('Converting...');
if (state.files.length === 1) { if (state.files.length === 1) {
const file = state.files[0]; const file = state.files[0];
showLoader(`Converting ${file.name}...`); showLoader(`Converting ${file.name}...`);
@@ -128,7 +133,6 @@ document.addEventListener('DOMContentLoaded', () => {
() => resetState() () => resetState()
); );
} else { } else {
showLoader('Converting multiple PDFs...');
const JSZip = (await import('jszip')).default; const JSZip = (await import('jszip')).default;
const zip = new JSZip(); const zip = new JSZip();
const usedNames = new Set<string>(); const usedNames = new Set<string>();

View File

@@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide';
import { convertFileToPdfA, type PdfALevel } from '../utils/ghostscript-loader'; import { convertFileToPdfA, type PdfALevel } from '../utils/ghostscript-loader';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@@ -108,10 +109,11 @@ document.addEventListener('DOMContentLoaded', () => {
try { try {
if (state.files.length === 0) { if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.'); showAlert('No Files', 'Please select at least one PDF file.');
hideLoader();
return; return;
} }
state.files = await batchDecryptIfNeeded(state.files);
if (state.files.length === 1) { if (state.files.length === 1) {
const originalFile = state.files[0]; const originalFile = state.files[0];
const preFlattenCheckbox = document.getElementById( const preFlattenCheckbox = document.getElementById(
@@ -125,7 +127,6 @@ document.addEventListener('DOMContentLoaded', () => {
if (shouldPreFlatten) { if (shouldPreFlatten) {
if (!isPyMuPDFAvailable()) { if (!isPyMuPDFAvailable()) {
showWasmRequiredDialog('pymupdf'); showWasmRequiredDialog('pymupdf');
hideLoader();
return; return;
} }

View File

@@ -11,6 +11,7 @@ import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { PDFPageProxy } from 'pdfjs-dist'; import { PDFPageProxy } from 'pdfjs-dist';
import { t } from '../i18n/i18n'; import { t } from '../i18n/i18n';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@@ -101,10 +102,11 @@ async function convert() {
); );
return; return;
} }
showLoader(t('tools:pdfToPng.loader.converting'));
try { try {
const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) const result = await loadPdfWithPasswordPrompt(files[0], files, 0);
.promise; if (!result) return;
showLoader(t('tools:pdfToPng.loader.converting'));
const { pdf } = result;
const scaleInput = document.getElementById('png-scale') as HTMLInputElement; const scaleInput = document.getElementById('png-scale') as HTMLInputElement;
const scale = scaleInput ? parseFloat(scaleInput.value) : 2.0; const scale = scaleInput ? parseFloat(scaleInput.value) : 2.0;

View File

@@ -5,6 +5,7 @@ import JSZip from 'jszip';
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
let pymupdf: any = null; let pymupdf: any = null;
let files: File[] = []; let files: File[] = [];
@@ -87,6 +88,10 @@ async function convert() {
pymupdf = await loadPyMuPDF(); pymupdf = await loadPyMuPDF();
} }
hideLoader();
files = await batchDecryptIfNeeded(files);
showLoader('Converting to SVG...');
const isSingleFile = files.length === 1; const isSingleFile = files.length === 1;
if (isSingleFile) { if (isSingleFile) {

View File

@@ -4,6 +4,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js';
let files: File[] = []; let files: File[] = [];
@@ -176,6 +177,10 @@ async function extractText() {
try { try {
const mupdf = await ensurePyMuPDF(); const mupdf = await ensurePyMuPDF();
hideLoader();
files = await batchDecryptIfNeeded(files);
showLoader('Extracting text...');
if (files.length === 1) { if (files.length === 1) {
const file = files[0]; const file = files[0];
showLoader(`Extracting text from ${file.name}...`); showLoader(`Extracting text from ${file.name}...`);

View File

@@ -14,6 +14,7 @@ import { t } from '../i18n/i18n';
import type Vips from 'wasm-vips'; import type Vips from 'wasm-vips';
import wasmUrl from 'wasm-vips/vips.wasm?url'; import wasmUrl from 'wasm-vips/vips.wasm?url';
import type { TiffOptions } from '@/types'; import type { TiffOptions } from '@/types';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@@ -229,8 +230,11 @@ async function convert() {
try { try {
const options = getOptions(); const options = getOptions();
const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) hideLoader();
.promise; 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) { if (options.multiPage && pdf.numPages > 1) {
const pages: Vips.Image[] = []; const pages: Vips.Image[] = [];

View File

@@ -11,6 +11,7 @@ import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { PDFPageProxy } from 'pdfjs-dist'; import { PDFPageProxy } from 'pdfjs-dist';
import { t } from '../i18n/i18n'; import { t } from '../i18n/i18n';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@@ -103,10 +104,11 @@ async function convert() {
); );
return; return;
} }
showLoader(t('tools:pdfToWebp.loader.converting'));
try { try {
const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) const result = await loadPdfWithPasswordPrompt(files[0], files, 0);
.promise; if (!result) return;
showLoader(t('tools:pdfToWebp.loader.converting'));
const { pdf } = result;
const qualityInput = document.getElementById( const qualityInput = document.getElementById(
'webp-quality' 'webp-quality'

View File

@@ -1,11 +1,19 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; 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 { PDFDocument, PageSizes } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { PosterizeState } from '@/types'; 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 = { const pageState: PosterizeState = {
file: null, file: null,
@@ -31,7 +39,9 @@ function resetState() {
const fileInput = document.getElementById('file-input') as HTMLInputElement; const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = ''; if (fileInput) fileInput.value = '';
const processBtn = document.getElementById('process-btn') as HTMLButtonElement; const processBtn = document.getElementById(
'process-btn'
) as HTMLButtonElement;
if (processBtn) processBtn.disabled = true; if (processBtn) processBtn.disabled = true;
const totalPages = document.getElementById('total-pages'); const totalPages = document.getElementById('total-pages');
@@ -44,7 +54,9 @@ async function renderPosterizePreview(pageNum: number) {
pageState.currentPage = pageNum; pageState.currentPage = pageNum;
showLoader(`Rendering preview for page ${pageNum}...`); showLoader(`Rendering preview for page ${pageNum}...`);
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement; const canvas = document.getElementById(
'posterize-preview-canvas'
) as HTMLCanvasElement;
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
if (!context) { if (!context) {
@@ -62,7 +74,12 @@ async function renderPosterizePreview(pageNum: number) {
canvas.width = viewport.width; canvas.width = viewport.width;
canvas.height = viewport.height; canvas.height = viewport.height;
await page.render({ canvasContext: context, viewport, canvas }).promise; await page.render({ canvasContext: context, viewport, canvas }).promise;
pageState.pageSnapshots[pageNum] = context.getImageData(0, 0, canvas.width, canvas.height); pageState.pageSnapshots[pageNum] = context.getImageData(
0,
0,
canvas.width,
canvas.height
);
} }
updatePreviewNav(); updatePreviewNav();
@@ -71,21 +88,35 @@ async function renderPosterizePreview(pageNum: number) {
} }
function drawGridOverlay() { 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 canvas = document.getElementById(
'posterize-preview-canvas'
) as HTMLCanvasElement;
const context = canvas.getContext('2d'); 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 pageRangeInput = (
const pagesToProcess = parsePageRanges(pageRangeInput, pageState.pdfJsDoc.numPages); document.getElementById('page-range') as HTMLInputElement
).value;
const pagesToProcess = parsePageRanges(
pageRangeInput,
pageState.pdfJsDoc.numPages
);
if (pagesToProcess.includes(pageState.currentPage - 1)) { if (pagesToProcess.includes(pageState.currentPage - 1)) {
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1; const rows =
const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1; 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.strokeStyle = 'rgba(239, 68, 68, 0.9)';
context.lineWidth = 2; context.lineWidth = 2;
@@ -116,12 +147,18 @@ function updatePreviewNav() {
if (!pageState.pdfJsDoc) return; if (!pageState.pdfJsDoc) return;
const currentPageSpan = document.getElementById('current-preview-page'); const currentPageSpan = document.getElementById('current-preview-page');
const prevBtn = document.getElementById('prev-preview-page') as HTMLButtonElement; const prevBtn = document.getElementById(
const nextBtn = document.getElementById('next-preview-page') as HTMLButtonElement; 'prev-preview-page'
) as HTMLButtonElement;
const nextBtn = document.getElementById(
'next-preview-page'
) as HTMLButtonElement;
if (currentPageSpan) currentPageSpan.textContent = pageState.currentPage.toString(); if (currentPageSpan)
currentPageSpan.textContent = pageState.currentPage.toString();
if (prevBtn) prevBtn.disabled = pageState.currentPage <= 1; if (prevBtn) prevBtn.disabled = pageState.currentPage <= 1;
if (nextBtn) nextBtn.disabled = pageState.currentPage >= pageState.pdfJsDoc.numPages; if (nextBtn)
nextBtn.disabled = pageState.currentPage >= pageState.pdfJsDoc.numPages;
} }
async function posterize() { async function posterize() {
@@ -133,14 +170,35 @@ async function posterize() {
showLoader('Posterizing PDF...'); showLoader('Posterizing PDF...');
try { try {
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1; const rows =
const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1; parseInt(
const pageSizeKey = (document.getElementById('output-page-size') as HTMLSelectElement).value as keyof typeof PageSizes; (document.getElementById('posterize-rows') as HTMLInputElement).value
const orientation = (document.getElementById('output-orientation') as HTMLSelectElement).value; ) || 1;
const scalingMode = (document.querySelector('input[name="scaling-mode"]:checked') as HTMLInputElement).value; const cols =
const overlap = parseFloat((document.getElementById('overlap') as HTMLInputElement).value) || 0; parseInt(
const overlapUnits = (document.getElementById('overlap-units') as HTMLSelectElement).value; (document.getElementById('posterize-cols') as HTMLInputElement).value
const pageRangeInput = (document.getElementById('page-range') 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; let overlapInPoints = overlap;
if (overlapUnits === 'in') overlapInPoints = overlap * 72; if (overlapUnits === 'in') overlapInPoints = overlap * 72;
@@ -166,18 +224,26 @@ async function posterize() {
const viewport = page.getViewport({ scale: 2.0 }); const viewport = page.getViewport({ scale: 2.0 });
tempCanvas.width = viewport.width; tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height; tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport, canvas: tempCanvas }).promise; await page.render({
canvasContext: tempCtx,
viewport,
canvas: tempCanvas,
}).promise;
let [targetWidth, targetHeight] = PageSizes[pageSizeKey] || PageSizes.A4; let [targetWidth, targetHeight] = PageSizes[pageSizeKey] || PageSizes.A4;
let currentOrientation = orientation; let currentOrientation = orientation;
if (currentOrientation === 'auto') { if (currentOrientation === 'auto') {
currentOrientation = viewport.width > viewport.height ? 'landscape' : 'portrait'; currentOrientation =
viewport.width > viewport.height ? 'landscape' : 'portrait';
} }
if (currentOrientation === 'landscape' && targetWidth < targetHeight) { if (currentOrientation === 'landscape' && targetWidth < targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth]; [targetWidth, targetHeight] = [targetHeight, targetWidth];
} else if (currentOrientation === 'portrait' && targetWidth > targetHeight) { } else if (
currentOrientation === 'portrait' &&
targetWidth > targetHeight
) {
[targetWidth, targetHeight] = [targetHeight, targetWidth]; [targetWidth, targetHeight] = [targetHeight, targetWidth];
} }
@@ -188,8 +254,14 @@ async function posterize() {
for (let c = 0; c < cols; c++) { for (let c = 0; c < cols; c++) {
const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0); const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0);
const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0); const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0);
const sWidth = tileWidth + (c > 0 ? overlapInPoints : 0) + (c < cols - 1 ? overlapInPoints : 0); const sWidth =
const sHeight = tileHeight + (r > 0 ? overlapInPoints : 0) + (r < rows - 1 ? overlapInPoints : 0); 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'); const tileCanvas = document.createElement('canvas');
tileCanvas.width = sWidth; tileCanvas.width = sWidth;
@@ -197,14 +269,29 @@ async function posterize() {
const tileCtx = tileCanvas.getContext('2d'); const tileCtx = tileCanvas.getContext('2d');
if (tileCtx) { if (tileCtx) {
tileCtx.drawImage(tempCanvas, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight); tileCtx.drawImage(
tempCanvas,
sx,
sy,
sWidth,
sHeight,
0,
0,
sWidth,
sHeight
);
const tileImage = await newDoc.embedPng(tileCanvas.toDataURL('image/png')); const tileImage = await newDoc.embedPng(
tileCanvas.toDataURL('image/png')
);
const newPage = newDoc.addPage([targetWidth, targetHeight]); const newPage = newDoc.addPage([targetWidth, targetHeight]);
const scaleX = newPage.getWidth() / sWidth; const scaleX = newPage.getWidth() / sWidth;
const scaleY = newPage.getHeight() / sHeight; const scaleY = newPage.getHeight() / sHeight;
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY); const scale =
scalingMode === 'fit'
? Math.min(scaleX, scaleY)
: Math.max(scaleX, scaleY);
const scaledWidth = sWidth * scale; const scaledWidth = sWidth * scale;
const scaledHeight = sHeight * scale; const scaledHeight = sHeight * scale;
@@ -238,7 +325,9 @@ async function posterize() {
async function updateUI() { async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area'); const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options'); const toolOptions = document.getElementById('tool-options');
const processBtn = document.getElementById('process-btn') as HTMLButtonElement; const processBtn = document.getElementById(
'process-btn'
) as HTMLButtonElement;
if (!fileDisplayArea) return; if (!fileDisplayArea) return;
@@ -246,7 +335,8 @@ async function updateUI() {
if (pageState.file) { if (pageState.file) {
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden'; infoContainer.className = 'flex flex-col overflow-hidden';
@@ -282,10 +372,16 @@ async function updateUI() {
async function handleFileSelect(files: FileList | null) { async function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) { if (files && files.length > 0) {
const file = files[0]; const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { if (
pageState.file = file; file.type === 'application/pdf' ||
pageState.pdfBytes = new Uint8Array(await file.arrayBuffer()); file.name.toLowerCase().endsWith('.pdf')
pageState.pdfJsDoc = await getPDFDocument({ data: pageState.pdfBytes }).promise; ) {
const result = await loadPdfWithPasswordPrompt(file);
if (!result) return;
pageState.file = result.file;
pageState.pdfBytes = new Uint8Array(result.bytes);
pageState.pdfJsDoc = result.pdf;
pageState.pageSnapshots = {}; pageState.pageSnapshots = {};
pageState.currentPage = 1; pageState.currentPage = 1;
@@ -308,7 +404,9 @@ async function handleFileSelect(files: FileList | null) {
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement; const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone'); const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn') as HTMLButtonElement; const processBtn = document.getElementById(
'process-btn'
) as HTMLButtonElement;
const backBtn = document.getElementById('back-to-tools'); const backBtn = document.getElementById('back-to-tools');
const prevBtn = document.getElementById('prev-preview-page'); const prevBtn = document.getElementById('prev-preview-page');
const nextBtn = document.getElementById('next-preview-page'); const nextBtn = document.getElementById('next-preview-page');
@@ -343,7 +441,10 @@ document.addEventListener('DOMContentLoaded', function () {
const files = e.dataTransfer?.files; const files = e.dataTransfer?.files;
if (files && files.length > 0) { if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) { const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'); return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
}); });
if (pdfFiles.length > 0) { if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer(); const dataTransfer = new DataTransfer();
@@ -369,7 +470,10 @@ document.addEventListener('DOMContentLoaded', function () {
if (nextBtn) { if (nextBtn) {
nextBtn.addEventListener('click', function () { nextBtn.addEventListener('click', function () {
if (pageState.pdfJsDoc && pageState.currentPage < pageState.pdfJsDoc.numPages) { if (
pageState.pdfJsDoc &&
pageState.currentPage < pageState.pdfJsDoc.numPages
) {
renderPosterizePreview(pageState.currentPage + 1); renderPosterizePreview(pageState.currentPage + 1);
} }
}); });

View File

@@ -9,6 +9,7 @@ import {
import { state } from '../state.js'; import { state } from '../state.js';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { loadPyMuPDF } from '../utils/pymupdf-loader.js'; import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@@ -104,6 +105,10 @@ document.addEventListener('DOMContentLoaded', () => {
showLoader('Loading engine...'); showLoader('Loading engine...');
const pymupdf = await loadPyMuPDF(); const pymupdf = await loadPyMuPDF();
hideLoader();
state.files = await batchDecryptIfNeeded(state.files);
showLoader('Extracting...');
const total = state.files.length; const total = state.files.length;
let completed = 0; let completed = 0;
let failed = 0; let failed = 0;
@@ -128,13 +133,13 @@ document.addEventListener('DOMContentLoaded', () => {
() => resetState() () => resetState()
); );
} else { } else {
// Multiple files - create ZIP
const JSZip = (await import('jszip')).default; const JSZip = (await import('jszip')).default;
const zip = new JSZip(); const zip = new JSZip();
const usedNames = new Set<string>(); const usedNames = new Set<string>();
for (const file of state.files) { for (let fi = 0; fi < state.files.length; fi++) {
try { try {
const file = state.files[fi];
showLoader( showLoader(
`Extracting ${file.name} for AI (${completed + 1}/${total})...` `Extracting ${file.name} for AI (${completed + 1}/${total})...`
); );
@@ -147,7 +152,7 @@ document.addEventListener('DOMContentLoaded', () => {
completed++; completed++;
} catch (error) { } catch (error) {
console.error(`Failed to extract ${file.name}:`, error); console.error(`Failed to extract ${state.files[fi].name}:`, error);
failed++; failed++;
} }
} }

View File

@@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide';
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js'; import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js'; import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@@ -123,6 +124,10 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('rasterize-grayscale') as HTMLInputElement document.getElementById('rasterize-grayscale') as HTMLInputElement
).checked; ).checked;
hideLoader();
state.files = await batchDecryptIfNeeded(state.files);
showLoader('Rasterizing...');
const total = state.files.length; const total = state.files.length;
let completed = 0; let completed = 0;
let failed = 0; let failed = 0;
@@ -149,13 +154,13 @@ document.addEventListener('DOMContentLoaded', () => {
() => resetState() () => resetState()
); );
} else { } else {
// Multiple files - create ZIP
const JSZip = (await import('jszip')).default; const JSZip = (await import('jszip')).default;
const zip = new JSZip(); const zip = new JSZip();
const usedNames = new Set<string>(); const usedNames = new Set<string>();
for (const file of state.files) { for (let fi = 0; fi < state.files.length; fi++) {
try { try {
const file = state.files[fi];
showLoader( showLoader(
`Rasterizing ${file.name} (${completed + 1}/${total})...` `Rasterizing ${file.name} (${completed + 1}/${total})...`
); );
@@ -174,7 +179,10 @@ document.addEventListener('DOMContentLoaded', () => {
completed++; completed++;
} catch (error) { } catch (error) {
console.error(`Failed to rasterize ${file.name}:`, error); console.error(
`Failed to rasterize ${state.files[fi].name}:`,
error
);
failed++; failed++;
} }
} }

View File

@@ -1,5 +1,6 @@
import { PDFDocument, PDFName } from 'pdf-lib'; import { PDFDocument, PDFName } from 'pdf-lib';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
// State management // State management
const pageState: { pdfDoc: PDFDocument | null; file: File | null } = { const pageState: { pdfDoc: PDFDocument | null; file: File | null } = {
@@ -20,7 +21,12 @@ function hideLoader() {
if (loader) loader.classList.add('hidden'); if (loader) loader.classList.add('hidden');
} }
function showAlert(title: string, message: string, type: string = 'error', callback?: () => void) { function showAlert(
title: string,
message: string,
type: string = 'error',
callback?: () => void
) {
const modal = document.getElementById('alert-modal'); const modal = document.getElementById('alert-modal');
const alertTitle = document.getElementById('alert-title'); const alertTitle = document.getElementById('alert-title');
const alertMessage = document.getElementById('alert-message'); const alertMessage = document.getElementById('alert-message');
@@ -53,7 +59,8 @@ function updateFileDisplay() {
const displayArea = document.getElementById('file-display-area'); const displayArea = document.getElementById('file-display-area');
if (!displayArea || !pageState.file || !pageState.pdfDoc) return; if (!displayArea || !pageState.file || !pageState.pdfDoc) return;
const fileSize = pageState.file.size < 1024 * 1024 const fileSize =
pageState.file.size < 1024 * 1024
? `${(pageState.file.size / 1024).toFixed(1)} KB` ? `${(pageState.file.size / 1024).toFixed(1)} KB`
: `${(pageState.file.size / 1024 / 1024).toFixed(2)} MB`; : `${(pageState.file.size / 1024 / 1024).toFixed(2)} MB`;
const pageCount = pageState.pdfDoc.getPageCount(); const pageCount = pageState.pdfDoc.getPageCount();
@@ -74,7 +81,9 @@ function updateFileDisplay() {
createIcons({ icons }); createIcons({ icons });
document.getElementById('remove-file')?.addEventListener('click', () => resetState()); document
.getElementById('remove-file')
?.addEventListener('click', () => resetState());
} }
function resetState() { function resetState() {
@@ -94,11 +103,13 @@ async function handleFileUpload(file: File) {
return; return;
} }
showLoader('Loading PDF...');
try { try {
const arrayBuffer = await file.arrayBuffer(); const result = await loadPdfWithPasswordPrompt(file);
pageState.pdfDoc = await PDFDocument.load(arrayBuffer); if (!result) return;
pageState.file = file; showLoader('Loading PDF...');
result.pdf.destroy();
pageState.pdfDoc = await PDFDocument.load(result.bytes);
pageState.file = result.file;
updateFileDisplay(); updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden'); document.getElementById('options-panel')?.classList.remove('hidden');
} catch (error) { } catch (error) {
@@ -130,8 +141,13 @@ async function processRemoveAnnotations() {
} }
const newPdfBytes = await pageState.pdfDoc.save(); const newPdfBytes = await pageState.pdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'annotations-removed.pdf'); downloadFile(
showAlert('Success', 'Annotations removed successfully!', 'success', () => { resetState(); }); new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'annotations-removed.pdf'
);
showAlert('Success', 'Annotations removed successfully!', 'success', () => {
resetState();
});
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showAlert('Error', 'Could not remove annotations.'); showAlert('Error', 'Could not remove annotations.');

View File

@@ -2,6 +2,7 @@ import { PDFDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { initPagePreview } from '../utils/page-preview.js'; import { initPagePreview } from '../utils/page-preview.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@@ -116,11 +117,13 @@ async function handleFileUpload(file: File) {
showAlert('Error', 'Please upload a valid PDF file.'); showAlert('Error', 'Please upload a valid PDF file.');
return; return;
} }
showLoader('Loading PDF...');
try { try {
const buf = await file.arrayBuffer(); const result = await loadPdfWithPasswordPrompt(file);
pageState.pdfDoc = await PDFDocument.load(buf); if (!result) return;
pageState.file = file; showLoader('Loading PDF...');
result.pdf.destroy();
pageState.pdfDoc = await PDFDocument.load(result.bytes);
pageState.file = result.file;
pageState.detectedBlankPages = []; pageState.detectedBlankPages = [];
updateFileDisplay(); updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden'); document.getElementById('options-panel')?.classList.remove('hidden');

View File

@@ -2,6 +2,7 @@ import { showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js'; import { downloadFile, formatBytes } from '../utils/helpers.js';
import { PDFDocument, PDFName } from 'pdf-lib'; import { PDFDocument, PDFName } from 'pdf-lib';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
interface PageState { interface PageState {
file: File | null; file: File | null;
@@ -12,9 +13,10 @@ const pageState: PageState = {
}; };
function removeMetadataFromDoc(pdfDoc: PDFDocument) { function removeMetadataFromDoc(pdfDoc: PDFDocument) {
const infoDict = (pdfDoc as any).getInfoDict(); // @ts-expect-error getInfoDict is private but accessible at runtime
const infoDict = pdfDoc.getInfoDict();
const allKeys = infoDict.keys(); const allKeys = infoDict.keys();
allKeys.forEach((key: any) => { allKeys.forEach((key: { asString: () => string }) => {
infoDict.delete(key); infoDict.delete(key);
}); });
@@ -26,30 +28,35 @@ function removeMetadataFromDoc(pdfDoc: PDFDocument) {
pdfDoc.setProducer(''); pdfDoc.setProducer('');
try { try {
const catalogDict = (pdfDoc.catalog as any).dict; // @ts-expect-error catalog.dict is private but accessible at runtime
const catalogDict = pdfDoc.catalog.dict;
if (catalogDict.has(PDFName.of('Metadata'))) { if (catalogDict.has(PDFName.of('Metadata'))) {
catalogDict.delete(PDFName.of('Metadata')); catalogDict.delete(PDFName.of('Metadata'));
} }
} catch (e: any) { } catch (e: unknown) {
console.warn('Could not remove XMP metadata:', e.message); const msg = e instanceof Error ? e.message : String(e);
console.warn('Could not remove XMP metadata:', msg);
} }
try { try {
const context = pdfDoc.context; const context = pdfDoc.context;
if ((context as any).trailerInfo) { if (context.trailerInfo) {
delete (context as any).trailerInfo.ID; delete context.trailerInfo.ID;
} }
} catch (e: any) { } catch (e: unknown) {
console.warn('Could not remove document IDs:', e.message); const msg = e instanceof Error ? e.message : String(e);
console.warn('Could not remove document IDs:', msg);
} }
try { try {
const catalogDict = (pdfDoc.catalog as any).dict; // @ts-expect-error catalog.dict is private but accessible at runtime
const catalogDict = pdfDoc.catalog.dict;
if (catalogDict.has(PDFName.of('PieceInfo'))) { if (catalogDict.has(PDFName.of('PieceInfo'))) {
catalogDict.delete(PDFName.of('PieceInfo')); catalogDict.delete(PDFName.of('PieceInfo'));
} }
} catch (e: any) { } catch (e: unknown) {
console.warn('Could not remove PieceInfo:', e.message); const msg = e instanceof Error ? e.message : String(e);
console.warn('Could not remove PieceInfo:', msg);
} }
} }
@@ -76,7 +83,8 @@ async function updateUI() {
if (pageState.file) { if (pageState.file) {
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden'; infoContainer.className = 'flex flex-col overflow-hidden';
@@ -111,7 +119,10 @@ async function updateUI() {
function handleFileSelect(files: FileList | null) { function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) { if (files && files.length > 0) {
const file = files[0]; const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file; pageState.file = file;
updateUI(); updateUI();
} }
@@ -130,8 +141,16 @@ async function removeMetadata() {
if (loaderText) loaderText.textContent = 'Removing all metadata...'; if (loaderText) loaderText.textContent = 'Removing all metadata...';
try { try {
const arrayBuffer = await pageState.file.arrayBuffer(); if (loaderModal) loaderModal.classList.add('hidden');
const pdfDoc = await PDFDocument.load(arrayBuffer); 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);
removeMetadataFromDoc(pdfDoc); removeMetadataFromDoc(pdfDoc);
@@ -140,7 +159,9 @@ async function removeMetadata() {
new Blob([newPdfBytes as BlobPart], { type: 'application/pdf' }), new Blob([newPdfBytes as BlobPart], { type: 'application/pdf' }),
'metadata-removed.pdf' 'metadata-removed.pdf'
); );
showAlert('Success', 'Metadata removed successfully!', 'success', () => { resetState(); }); showAlert('Success', 'Metadata removed successfully!', 'success', () => {
resetState();
});
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showAlert('Error', 'An error occurred while trying to remove metadata.'); showAlert('Error', 'An error occurred while trying to remove metadata.');
@@ -182,7 +203,10 @@ document.addEventListener('DOMContentLoaded', function () {
const files = e.dataTransfer?.files; const files = e.dataTransfer?.files;
if (files && files.length > 0) { if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) { const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'); return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
}); });
if (pdfFiles.length > 0) { if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer(); const dataTransfer = new DataTransfer();

View File

@@ -7,6 +7,7 @@ import {
import { state } from '../state.js'; import { state } from '../state.js';
import JSZip from 'jszip'; import JSZip from 'jszip';
import { deduplicateFileName } from '../utils/deduplicate-filename.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js';
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
export async function repairPdfFile(file: File): Promise<Uint8Array | null> { export async function repairPdfFile(file: File): Promise<Uint8Array | null> {
const inputPath = '/input.pdf'; const inputPath = '/input.pdf';
@@ -67,7 +68,9 @@ export async function repairPdf() {
const failedRepairs: string[] = []; const failedRepairs: string[] = [];
try { try {
const decryptedFiles = await batchDecryptIfNeeded(state.files);
showLoader('Initializing repair engine...'); showLoader('Initializing repair engine...');
state.files = decryptedFiles;
for (let i = 0; i < state.files.length; i++) { for (let i = 0; i < state.files.length; i++) {
const file = state.files[i]; const file = state.files[i];
@@ -105,7 +108,9 @@ export async function repairPdf() {
if (successfulRepairs.length === 1) { if (successfulRepairs.length === 1) {
const file = successfulRepairs[0]; 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); downloadFile(blob, file.name);
} else { } else {
showLoader('Creating ZIP archive...'); showLoader('Creating ZIP archive...');
@@ -124,7 +129,7 @@ export async function repairPdf() {
if (failedRepairs.length === 0) { if (failedRepairs.length === 0) {
showAlert('Success', 'All files repaired successfully!'); showAlert('Success', 'All files repaired successfully!');
} }
} catch (error: any) { } catch (error: unknown) {
console.error('Critical error during repair:', error); console.error('Critical error during repair:', error);
hideLoader(); hideLoader();
showAlert( showAlert(

View File

@@ -4,6 +4,7 @@ import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import JSZip from 'jszip'; import JSZip from 'jszip';
import { deduplicateFileName } from '../utils/deduplicate-filename.js'; import { deduplicateFileName } from '../utils/deduplicate-filename.js';
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
interface ReverseState { interface ReverseState {
files: File[]; files: File[];
@@ -76,75 +77,61 @@ function updateUI() {
} }
} }
async function reverseSingleFile(file: File): Promise<Uint8Array> {
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() { async function reversePages() {
if (reverseState.files.length === 0) { if (reverseState.files.length === 0) {
showAlert('No Files', 'Please select one or more PDF files.'); showAlert('No Files', 'Please select one or more PDF files.');
return; return;
} }
showLoader('Reversing page order...');
try { 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 zip = new JSZip();
const usedNames = new Set<string>(); const usedNames = new Set<string>();
for (let j = 0; j < reverseState.files.length; j++) { for (let j = 0; j < validFiles.length; j++) {
const file = reverseState.files[j]; const file = validFiles[j];
showLoader( showLoader(`Reversing ${file.name} (${j + 1}/${validFiles.length})...`);
`Processing ${file.name} (${j + 1}/${reverseState.files.length})...`
);
const arrayBuffer = await file.arrayBuffer(); const newPdfBytes = await reverseSingleFile(file);
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 originalName = file.name.replace(/\.pdf$/i, ''); const originalName = file.name.replace(/\.pdf$/i, '');
const fileName = `${originalName}_reversed.pdf`; const fileName = `${originalName}_reversed.pdf`;
const zipEntryName = deduplicateFileName(fileName, usedNames); const zipEntryName = deduplicateFileName(fileName, usedNames);
zip.file(zipEntryName, newPdfBytes); zip.file(zipEntryName, newPdfBytes);
} }
if (reverseState.files.length === 1) { if (validFiles.length === 1) {
// Single file: download directly const file = validFiles[0];
const file = reverseState.files[0]; const newPdfBytes = await reverseSingleFile(file);
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 originalName = file.name.replace(/\.pdf$/i, ''); const originalName = file.name.replace(/\.pdf$/i, '');
downloadFile( downloadFile(
@@ -152,7 +139,6 @@ async function reversePages() {
`${originalName}_reversed.pdf` `${originalName}_reversed.pdf`
); );
} else { } else {
// Multiple files: download as ZIP
const zipBlob = await zip.generateAsync({ type: 'blob' }); const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'reversed_pdfs.zip'); downloadFile(zipBlob, 'reversed_pdfs.zip');
} }

View File

@@ -1,11 +1,18 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; 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 { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument, degrees } from 'pdf-lib'; 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'; 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 { interface RotateState {
file: File | null; file: File | null;
@@ -40,32 +47,44 @@ function resetState() {
const fileInput = document.getElementById('file-input') as HTMLInputElement; const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = ''; if (fileInput) fileInput.value = '';
const batchAngle = document.getElementById('batch-custom-angle') as HTMLInputElement; const batchAngle = document.getElementById(
'batch-custom-angle'
) as HTMLInputElement;
if (batchAngle) batchAngle.value = '0'; if (batchAngle) batchAngle.value = '0';
} }
function updateAllRotationDisplays() { function updateAllRotationDisplays() {
for (let i = 0; i < pageState.rotations.length; i++) { for (let i = 0; i < pageState.rotations.length; i++) {
const input = document.getElementById(`page-angle-${i}`) as HTMLInputElement; const input = document.getElementById(
`page-angle-${i}`
) as HTMLInputElement;
if (input) input.value = pageState.rotations[i].toString(); if (input) input.value = pageState.rotations[i].toString();
const container = document.querySelector(`[data-page-index="${i}"]`); const container = document.querySelector(`[data-page-index="${i}"]`);
if (container) { if (container) {
const wrapper = container.querySelector('.thumbnail-wrapper') as HTMLElement; const wrapper = container.querySelector(
if (wrapper) wrapper.style.transform = `rotate(${-pageState.rotations[i]}deg)`; '.thumbnail-wrapper'
) as HTMLElement;
if (wrapper)
wrapper.style.transform = `rotate(${-pageState.rotations[i]}deg)`;
} }
} }
} }
function createPageWrapper(canvas: HTMLCanvasElement, pageNumber: number): HTMLElement { function createPageWrapper(
canvas: HTMLCanvasElement,
pageNumber: number
): HTMLElement {
const pageIndex = pageNumber - 1; const pageIndex = pageNumber - 1;
const container = document.createElement('div'); const container = document.createElement('div');
container.className = 'page-thumbnail relative bg-gray-700 rounded-lg overflow-hidden'; container.className =
'page-thumbnail relative bg-gray-700 rounded-lg overflow-hidden';
container.dataset.pageIndex = pageIndex.toString(); container.dataset.pageIndex = pageIndex.toString();
container.dataset.pageNumber = pageNumber.toString(); container.dataset.pageNumber = pageNumber.toString();
const canvasWrapper = document.createElement('div'); const canvasWrapper = document.createElement('div');
canvasWrapper.className = 'thumbnail-wrapper flex items-center justify-center p-2 h-36'; canvasWrapper.className =
'thumbnail-wrapper flex items-center justify-center p-2 h-36';
canvasWrapper.style.transition = 'transform 0.3s ease'; canvasWrapper.style.transition = 'transform 0.3s ease';
// Apply initial rotation if it exists (negated for canvas display) // Apply initial rotation if it exists (negated for canvas display)
const initialRotation = pageState.rotations[pageIndex] || 0; const initialRotation = pageState.rotations[pageIndex] || 0;
@@ -75,7 +94,8 @@ function createPageWrapper(canvas: HTMLCanvasElement, pageNumber: number): HTMLE
canvasWrapper.appendChild(canvas); canvasWrapper.appendChild(canvas);
const pageLabel = document.createElement('div'); 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.className =
'absolute top-1 left-1 bg-black bg-opacity-60 text-white text-xs px-2 py-1 rounded';
pageLabel.textContent = `${pageNumber}`; pageLabel.textContent = `${pageNumber}`;
container.appendChild(canvasWrapper); container.appendChild(canvasWrapper);
@@ -86,11 +106,14 @@ function createPageWrapper(canvas: HTMLCanvasElement, pageNumber: number): HTMLE
controls.className = 'flex items-center justify-center gap-1 p-2 bg-gray-800'; controls.className = 'flex items-center justify-center gap-1 p-2 bg-gray-800';
const decrementBtn = document.createElement('button'); 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.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.textContent = '-';
decrementBtn.onclick = function (e) { decrementBtn.onclick = function (e) {
e.stopPropagation(); e.stopPropagation();
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement; const input = document.getElementById(
`page-angle-${pageIndex}`
) as HTMLInputElement;
const current = parseInt(input.value) || 0; const current = parseInt(input.value) || 0;
input.value = (current - 1).toString(); input.value = (current - 1).toString();
}; };
@@ -99,27 +122,36 @@ function createPageWrapper(canvas: HTMLCanvasElement, pageNumber: number): HTMLE
angleInput.type = 'number'; angleInput.type = 'number';
angleInput.id = `page-angle-${pageIndex}`; angleInput.id = `page-angle-${pageIndex}`;
angleInput.value = pageState.rotations[pageIndex]?.toString() || '0'; 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'; 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'); 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.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.textContent = '+';
incrementBtn.onclick = function (e) { incrementBtn.onclick = function (e) {
e.stopPropagation(); e.stopPropagation();
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement; const input = document.getElementById(
`page-angle-${pageIndex}`
) as HTMLInputElement;
const current = parseInt(input.value) || 0; const current = parseInt(input.value) || 0;
input.value = (current + 1).toString(); input.value = (current + 1).toString();
}; };
const applyBtn = document.createElement('button'); 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.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 = '<i data-lucide="rotate-cw" class="w-4 h-4"></i>'; applyBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-4 h-4"></i>';
applyBtn.onclick = function (e) { applyBtn.onclick = function (e) {
e.stopPropagation(); e.stopPropagation();
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement; const input = document.getElementById(
`page-angle-${pageIndex}`
) as HTMLInputElement;
const angle = parseInt(input.value) || 0; const angle = parseInt(input.value) || 0;
pageState.rotations[pageIndex] = angle; pageState.rotations[pageIndex] = angle;
const wrapper = container.querySelector('.thumbnail-wrapper') as HTMLElement; const wrapper = container.querySelector(
'.thumbnail-wrapper'
) as HTMLElement;
if (wrapper) wrapper.style.transform = `rotate(${-angle}deg)`; if (wrapper) wrapper.style.transform = `rotate(${-angle}deg)`;
}; };
@@ -151,7 +183,7 @@ async function renderThumbnails() {
eagerLoadBatches: 2, eagerLoadBatches: 2,
onBatchComplete: function () { onBatchComplete: function () {
createIcons({ icons }); createIcons({ icons });
} },
} }
); );
@@ -168,7 +200,8 @@ async function updateUI() {
if (pageState.file) { if (pageState.file) {
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden'; infoContainer.className = 'flex flex-col overflow-hidden';
@@ -195,15 +228,18 @@ async function updateUI() {
createIcons({ icons }); createIcons({ icons });
try { try {
const result = await loadPdfWithPasswordPrompt(pageState.file);
if (!result) {
resetState();
return;
}
showLoader('Loading PDF...'); showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer.slice(0), { pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
ignoreEncryption: true, throwOnInvalidObject: false,
throwOnInvalidObject: false
}); });
pageState.pdfJsDoc = await getPDFDocument({ data: arrayBuffer.slice(0) }).promise; pageState.pdfJsDoc = result.pdf;
const pageCount = pageState.pdfDoc.getPageCount(); const pageCount = pageState.pdfDoc.getPageCount();
pageState.rotations = new Array(pageCount).fill(0); pageState.rotations = new Array(pageCount).fill(0);
@@ -243,7 +279,9 @@ async function applyRotations() {
const currentRotation = originalPage.getRotation().angle; const currentRotation = originalPage.getRotation().angle;
const totalRotation = currentRotation + rotation; 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) { if (totalRotation % 90 === 0) {
const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]); const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
@@ -262,8 +300,14 @@ async function applyRotations() {
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 x =
const y = newHeight / 2 - (width / 2 * Math.sin(angleRad) + height / 2 * Math.cos(angleRad)); 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, { newPage.drawPage(embeddedPage, {
x, x,
@@ -283,9 +327,14 @@ async function applyRotations() {
`${originalName}_rotated.pdf` `${originalName}_rotated.pdf`
); );
showAlert('Success', 'Rotations applied successfully!', 'success', function () { showAlert(
'Success',
'Rotations applied successfully!',
'success',
function () {
resetState(); resetState();
}); }
);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showAlert('Error', 'Could not apply rotations.'); showAlert('Error', 'Could not apply rotations.');
@@ -297,7 +346,10 @@ async function applyRotations() {
function handleFileSelect(files: FileList | null) { function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) { if (files && files.length > 0) {
const file = files[0]; const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file; pageState.file = file;
updateUI(); updateUI();
} }
@@ -312,7 +364,9 @@ document.addEventListener('DOMContentLoaded', function () {
const batchDecrement = document.getElementById('batch-decrement'); const batchDecrement = document.getElementById('batch-decrement');
const batchIncrement = document.getElementById('batch-increment'); const batchIncrement = document.getElementById('batch-increment');
const batchApply = document.getElementById('batch-apply'); const batchApply = document.getElementById('batch-apply');
const batchAngleInput = document.getElementById('batch-custom-angle') as HTMLInputElement; const batchAngleInput = document.getElementById(
'batch-custom-angle'
) as HTMLInputElement;
if (backBtn) { if (backBtn) {
backBtn.addEventListener('click', function () { backBtn.addEventListener('click', function () {
@@ -365,7 +419,10 @@ document.addEventListener('DOMContentLoaded', function () {
const files = e.dataTransfer?.files; const files = e.dataTransfer?.files;
if (files && files.length > 0) { if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) { const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'); return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
}); });
if (pdfFiles.length > 0) { if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer(); const dataTransfer = new DataTransfer();

View File

@@ -1,5 +1,5 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; 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 { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { import {
@@ -7,6 +7,7 @@ import {
cleanupLazyRendering, cleanupLazyRendering,
} from '../utils/render-utils.js'; } from '../utils/render-utils.js';
import { rotatePdfPages } from '../utils/pdf-operations.js'; import { rotatePdfPages } from '../utils/pdf-operations.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
@@ -199,16 +200,18 @@ async function updateUI() {
createIcons({ icons }); createIcons({ icons });
try { try {
const result = await loadPdfWithPasswordPrompt(pageState.file);
if (!result) {
resetState();
return;
}
showLoader('Loading PDF...'); showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer.slice(0), { pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
ignoreEncryption: true,
throwOnInvalidObject: false, throwOnInvalidObject: false,
}); });
pageState.pdfJsDoc = await getPDFDocument({ data: arrayBuffer.slice(0) }) pageState.pdfJsDoc = result.pdf;
.promise;
const pageCount = pageState.pdfDoc.getPageCount(); const pageCount = pageState.pdfDoc.getPageCount();
pageState.rotations = new Array(pageCount).fill(0); pageState.rotations = new Array(pageCount).fill(0);

View File

@@ -3,6 +3,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import { SanitizePdfState } from '@/types'; import { SanitizePdfState } from '@/types';
import { sanitizePdf } from '../utils/sanitize.js'; import { sanitizePdf } from '../utils/sanitize.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
const pageState: SanitizePdfState = { const pageState: SanitizePdfState = {
file: null, file: null,
@@ -132,8 +133,17 @@ async function runSanitize() {
return; return;
} }
const arrayBuffer = await pageState.file.arrayBuffer(); if (loaderModal) loaderModal.classList.add('hidden');
const result = await sanitizePdf(new Uint8Array(arrayBuffer), options); 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( downloadFile(
new Blob([new Uint8Array(result.bytes)], { type: 'application/pdf' }), new Blob([new Uint8Array(result.bytes)], { type: 'application/pdf' }),
@@ -147,9 +157,10 @@ async function runSanitize() {
resetState(); resetState();
} }
); );
} catch (e: any) { } catch (e: unknown) {
console.error('Sanitization Error:', e); 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 { } finally {
if (loaderModal) loaderModal.classList.add('hidden'); if (loaderModal) loaderModal.classList.add('hidden');
} }

View File

@@ -11,6 +11,7 @@ import { applyScannerEffect } from '../utils/image-effects.js';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import type { ScanSettings } from '../types/scanner-effect-type.js'; import type { ScanSettings } from '../types/scanner-effect-type.js';
import { t } from '../i18n/i18n'; import { t } from '../i18n/i18n';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@@ -402,13 +403,13 @@ document.addEventListener('DOMContentLoaded', () => {
return; return;
} }
files = [validFiles[0]];
updateUI();
showLoader('Loading preview...');
try { try {
const buffer = await readFileAsArrayBuffer(validFiles[0]); const result = await loadPdfWithPasswordPrompt(validFiles[0]);
pdfjsDoc = await getPDFDocument({ data: buffer }).promise; if (!result) return;
showLoader('Loading preview...');
files = [result.file];
updateUI();
pdfjsDoc = result.pdf;
await renderPreview(); await renderPreview();
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View File

@@ -1,6 +1,11 @@
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js'; 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 { PDFDocument } from 'pdf-lib';
import { t } from '../i18n/i18n'; import { t } from '../i18n/i18n';
@@ -80,7 +85,10 @@ function handleFileUpload(e: Event) {
} }
function handleFile(file: File) { function handleFile(file: File) {
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) { if (
file.type !== 'application/pdf' &&
!file.name.toLowerCase().endsWith('.pdf')
) {
showAlert('Invalid File', 'Please select a PDF file.'); showAlert('Invalid File', 'Please select a PDF file.');
return; return;
} }
@@ -98,7 +106,8 @@ async function updateFileDisplay() {
fileDisplayArea.innerHTML = ''; fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0'; infoContainer.className = 'flex flex-col flex-1 min-w-0';
@@ -127,14 +136,18 @@ async function updateFileDisplay() {
fileDisplayArea.appendChild(fileDiv); fileDisplayArea.appendChild(fileDiv);
createIcons({ icons }); createIcons({ icons });
// Load page count const result = await loadPdfWithPasswordPrompt(signState.file);
try { if (!result) {
const arrayBuffer = await readFileAsArrayBuffer(signState.file); signState.file = null;
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise; signState.pdfDoc = null;
metaSpan.textContent = `${formatBytes(signState.file.size)}${pdfDoc.numPages} pages`; fileDisplayArea.innerHTML = '';
} catch (error) { document.getElementById('signature-editor')?.classList.add('hidden');
console.error('Error loading PDF:', error); 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() { async function setupSignTool() {
@@ -182,7 +195,10 @@ async function setupSignTool() {
localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs)); localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs));
} catch {} } catch {}
const viewerUrl = new URL(`${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html`, window.location.origin); const viewerUrl = new URL(
`${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html`,
window.location.origin
);
const query = new URLSearchParams({ file: signState.blobUrl }); const query = new URLSearchParams({ file: signState.blobUrl });
iframe.src = `${viewerUrl.toString()}?${query.toString()}`; iframe.src = `${viewerUrl.toString()}?${query.toString()}`;
@@ -200,18 +216,24 @@ async function setupSignTool() {
editorModeButtons?.classList.remove('hidden'); editorModeButtons?.classList.remove('hidden');
const editorSignature = doc.getElementById('editorSignature'); const editorSignature = doc.getElementById('editorSignature');
editorSignature?.removeAttribute('hidden'); editorSignature?.removeAttribute('hidden');
const editorSignatureButton = doc.getElementById('editorSignatureButton') as HTMLButtonElement | null; const editorSignatureButton = doc.getElementById(
'editorSignatureButton'
) as HTMLButtonElement | null;
if (editorSignatureButton) { if (editorSignatureButton) {
editorSignatureButton.disabled = false; editorSignatureButton.disabled = false;
} }
const editorStamp = doc.getElementById('editorStamp'); const editorStamp = doc.getElementById('editorStamp');
editorStamp?.removeAttribute('hidden'); editorStamp?.removeAttribute('hidden');
const editorStampButton = doc.getElementById('editorStampButton') as HTMLButtonElement | null; const editorStampButton = doc.getElementById(
'editorStampButton'
) as HTMLButtonElement | null;
if (editorStampButton) { if (editorStampButton) {
editorStampButton.disabled = false; editorStampButton.disabled = false;
} }
try { try {
const highlightBtn = doc.getElementById('editorHighlightButton') as HTMLButtonElement | null; const highlightBtn = doc.getElementById(
'editorHighlightButton'
) as HTMLButtonElement | null;
highlightBtn?.click(); highlightBtn?.click();
} catch {} } catch {}
}); });
@@ -220,7 +242,9 @@ async function setupSignTool() {
console.error('Could not initialize PDF.js viewer for signing:', e); console.error('Could not initialize PDF.js viewer for signing:', e);
} }
const saveBtn = document.getElementById('process-btn') as HTMLButtonElement | null; const saveBtn = document.getElementById(
'process-btn'
) as HTMLButtonElement | null;
if (saveBtn) { if (saveBtn) {
saveBtn.style.display = ''; saveBtn.style.display = '';
} }
@@ -241,20 +265,29 @@ async function applyAndSaveSignatures() {
} }
const app = viewerWindow.PDFViewerApplication; const app = viewerWindow.PDFViewerApplication;
const flattenCheckbox = document.getElementById('flatten-signature-toggle') as HTMLInputElement | null; const flattenCheckbox = document.getElementById(
'flatten-signature-toggle'
) as HTMLInputElement | null;
const shouldFlatten = flattenCheckbox?.checked; const shouldFlatten = flattenCheckbox?.checked;
if (shouldFlatten) { if (shouldFlatten) {
showLoader('Flattening and saving PDF...'); showLoader('Flattening and saving PDF...');
const rawPdfBytes = await app.pdfDocument.saveDocument(app.pdfDocument.annotationStorage); const rawPdfBytes = await app.pdfDocument.saveDocument(
app.pdfDocument.annotationStorage
);
const pdfBytes = new Uint8Array(rawPdfBytes); const pdfBytes = new Uint8Array(rawPdfBytes);
const pdfDoc = await PDFDocument.load(pdfBytes); const pdfDoc = await PDFDocument.load(pdfBytes);
pdfDoc.getForm().flatten(); pdfDoc.getForm().flatten();
const flattenedPdfBytes = await pdfDoc.save(); const flattenedPdfBytes = await pdfDoc.save();
const blob = new Blob([flattenedPdfBytes as BlobPart], { type: 'application/pdf' }); const blob = new Blob([flattenedPdfBytes as BlobPart], {
downloadFile(blob, `signed_flattened_${signState.file?.name || 'document.pdf'}`); type: 'application/pdf',
});
downloadFile(
blob,
`signed_flattened_${signState.file?.name || 'document.pdf'}`
);
hideLoader(); hideLoader();
showAlert('Success', 'Signed PDF saved successfully!', 'success', () => { showAlert('Success', 'Signed PDF saved successfully!', 'success', () => {
@@ -262,14 +295,22 @@ async function applyAndSaveSignatures() {
}); });
} else { } else {
app.eventBus?.dispatch('download', { source: app }); app.eventBus?.dispatch('download', { source: app });
showAlert('Success', 'Signed PDF downloaded successfully!', 'success', () => { showAlert(
'Success',
'Signed PDF downloaded successfully!',
'success',
() => {
resetState(); resetState();
}); }
);
} }
} catch (error) { } catch (error) {
console.error('Failed to export the signed PDF:', error); console.error('Failed to export the signed PDF:', error);
hideLoader(); hideLoader();
showAlert('Export failed', 'Could not export the signed PDF. Please try again.'); showAlert(
'Export failed',
'Could not export the signed PDF. Please try again.'
);
} }
} }
@@ -294,12 +335,16 @@ function resetState() {
fileDisplayArea.innerHTML = ''; fileDisplayArea.innerHTML = '';
} }
const processBtn = document.getElementById('process-btn') as HTMLButtonElement | null; const processBtn = document.getElementById(
'process-btn'
) as HTMLButtonElement | null;
if (processBtn) { if (processBtn) {
processBtn.style.display = 'none'; processBtn.style.display = 'none';
} }
const flattenCheckbox = document.getElementById('flatten-signature-toggle') as HTMLInputElement | null; const flattenCheckbox = document.getElementById(
'flatten-signature-toggle'
) as HTMLInputElement | null;
if (flattenCheckbox) { if (flattenCheckbox) {
flattenCheckbox.checked = false; flattenCheckbox.checked = false;
} }

View File

@@ -2,12 +2,8 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { t } from '../i18n/i18n'; import { t } from '../i18n/i18n';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { import { downloadFile, getPDFDocument, formatBytes } from '../utils/helpers.js';
downloadFile, import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
getPDFDocument,
readFileAsArrayBuffer,
formatBytes,
} from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { import {
renderPagesProgressively, renderPagesProgressively,
@@ -94,12 +90,15 @@ document.addEventListener('DOMContentLoaded', () => {
// Load PDF Document // Load PDF Document
try { try {
if (!state.pdfDoc) { if (!state.pdfDoc) {
showLoader('Loading PDF...'); const result = await loadPdfWithPasswordPrompt(file);
const arrayBuffer = (await readFileAsArrayBuffer( if (!result) {
file state.files = [];
)) as ArrayBuffer; updateUI();
state.pdfDoc = await PDFLibDocument.load(arrayBuffer); return;
hideLoader(); }
result.pdf.destroy();
state.files[0] = result.file;
state.pdfDoc = await PDFLibDocument.load(result.bytes);
} }
// Update page count // Update page count
metaSpan.textContent = `${formatBytes(file.size)}${state.pdfDoc.getPageCount()} pages`; 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 pdfDoc is not loaded yet (e.g. page refresh), try to load it from the first file
if (state.files.length > 0) { if (state.files.length > 0) {
const file = state.files[0]; const file = state.files[0];
const arrayBuffer = (await readFileAsArrayBuffer( hideLoader();
file const result = await loadPdfWithPasswordPrompt(file);
)) as ArrayBuffer; if (!result) {
state.pdfDoc = await PDFLibDocument.load(arrayBuffer); 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 { } else {
throw new Error('No PDF document loaded'); throw new Error('No PDF document loaded');
} }

View File

@@ -5,6 +5,7 @@ import {
showWasmRequiredDialog, showWasmRequiredDialog,
WasmProvider, WasmProvider,
} from '../utils/wasm-provider.js'; } from '../utils/wasm-provider.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
const worker = new Worker( const worker = new Worker(
import.meta.env.BASE_URL + 'workers/table-of-contents.worker.js' import.meta.env.BASE_URL + 'workers/table-of-contents.worker.js'
@@ -95,15 +96,18 @@ function renderFileDisplay(file: File) {
fileDisplayArea.appendChild(fileDiv); fileDisplayArea.appendChild(fileDiv);
} }
function handleFileSelect(file: File) { async function handleFileSelect(file: File) {
if (file.type !== 'application/pdf') { if (file.type !== 'application/pdf') {
showStatus('Please select a PDF file.', 'error'); showStatus('Please select a PDF file.', 'error');
return; return;
} }
pdfFile = file; const result = await loadPdfWithPasswordPrompt(file);
if (!result) return;
result.pdf.destroy();
pdfFile = result.file;
generateBtn.disabled = false; generateBtn.disabled = false;
renderFileDisplay(file); renderFileDisplay(pdfFile);
} }
dropZone.addEventListener('dragover', (e) => { dropZone.addEventListener('dragover', (e) => {

View File

@@ -1,16 +1,29 @@
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js'; 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 { PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { TextColorState } from '@/types'; 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 }; const pageState: TextColorState = { file: null, pdfDoc: null };
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializePage); } if (document.readyState === 'loading') {
else { initializePage(); } document.addEventListener('DOMContentLoaded', initializePage);
} else {
initializePage();
}
function initializePage() { function initializePage() {
createIcons({ icons }); createIcons({ icons });
@@ -21,34 +34,57 @@ function initializePage() {
if (fileInput) { if (fileInput) {
fileInput.addEventListener('change', handleFileUpload); fileInput.addEventListener('change', handleFileUpload);
fileInput.addEventListener('click', () => { fileInput.value = ''; }); fileInput.addEventListener('click', () => {
fileInput.value = '';
});
} }
if (dropZone) { if (dropZone) {
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-indigo-500'); }); dropZone.addEventListener('dragover', (e) => {
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('border-indigo-500'); }); e.preventDefault();
dropZone.classList.add('border-indigo-500');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('border-indigo-500');
});
dropZone.addEventListener('drop', (e) => { dropZone.addEventListener('drop', (e) => {
e.preventDefault(); dropZone.classList.remove('border-indigo-500'); e.preventDefault();
dropZone.classList.remove('border-indigo-500');
if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files); if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files);
}); });
} }
if (backBtn) backBtn.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; }); if (backBtn)
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
if (processBtn) processBtn.addEventListener('click', changeTextColor); if (processBtn) processBtn.addEventListener('click', changeTextColor);
} }
function handleFileUpload(e: Event) { const input = e.target as HTMLInputElement; if (input.files?.length) handleFiles(input.files); } function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement;
if (input.files?.length) handleFiles(input.files);
}
async function handleFiles(files: FileList) { async function handleFiles(files: FileList) {
const file = files[0]; const file = files[0];
if (!file || file.type !== 'application/pdf') { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; } if (!file || file.type !== 'application/pdf') {
showLoader('Loading PDF...'); showAlert('Invalid File', 'Please upload a valid PDF file.');
return;
}
try { try {
const arrayBuffer = await file.arrayBuffer(); const result = await loadPdfWithPasswordPrompt(file);
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer); if (!result) return;
pageState.file = file; showLoader('Loading PDF...');
result.pdf.destroy();
pageState.pdfDoc = await PDFLibDocument.load(result.bytes);
pageState.file = result.file;
updateFileDisplay(); updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden'); document.getElementById('options-panel')?.classList.remove('hidden');
} catch (error) { console.error(error); showAlert('Error', 'Failed to load PDF file.'); } } catch (error) {
finally { hideLoader(); } console.error(error);
showAlert('Error', 'Failed to load PDF file.');
} finally {
hideLoader();
}
} }
function updateFileDisplay() { function updateFileDisplay() {
@@ -56,7 +92,8 @@ function updateFileDisplay() {
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return; if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
fileDisplayArea.innerHTML = ''; fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0'; infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div'); const nameSpan = document.createElement('div');
@@ -76,7 +113,8 @@ function updateFileDisplay() {
} }
function resetState() { function resetState() {
pageState.file = null; pageState.pdfDoc = null; pageState.file = null;
pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area'); const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = ''; if (fileDisplayArea) fileDisplayArea.innerHTML = '';
document.getElementById('options-panel')?.classList.add('hidden'); document.getElementById('options-panel')?.classList.add('hidden');
@@ -85,14 +123,21 @@ function resetState() {
} }
async function changeTextColor() { async function changeTextColor() {
if (!pageState.pdfDoc || !pageState.file) { showAlert('Error', 'Please upload a PDF file first.'); return; } if (!pageState.pdfDoc || !pageState.file) {
const colorHex = (document.getElementById('text-color-input') as HTMLInputElement).value; 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 { r, g, b } = hexToRgb(colorHex);
const darknessThreshold = 120; const darknessThreshold = 120;
showLoader('Changing text color...'); showLoader('Changing text color...');
try { try {
const newPdfDoc = await PDFLibDocument.create(); const newPdfDoc = await PDFLibDocument.create();
const pdf = await getPDFDocument(await readFileAsArrayBuffer(pageState.file)).promise; const pdf = await getPDFDocument(
await readFileAsArrayBuffer(pageState.file)
).promise;
for (let i = 1; i <= pdf.numPages; i++) { for (let i = 1; i <= pdf.numPages; i++) {
showLoader(`Processing page ${i} of ${pdf.numPages}...`); showLoader(`Processing page ${i} of ${pdf.numPages}...`);
@@ -107,7 +152,11 @@ async function changeTextColor() {
const imageData = context.getImageData(0, 0, canvas.width, canvas.height); const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; const data = imageData.data;
for (let j = 0; j < data.length; j += 4) { for (let j = 0; j < data.length; j += 4) {
if (data[j] < darknessThreshold && data[j + 1] < darknessThreshold && data[j + 2] < darknessThreshold) { if (
data[j] < darknessThreshold &&
data[j + 1] < darknessThreshold &&
data[j + 2] < darknessThreshold
) {
data[j] = r * 255; data[j] = r * 255;
data[j + 1] = g * 255; data[j + 1] = g * 255;
data[j + 2] = b * 255; data[j + 2] = b * 255;
@@ -118,18 +167,33 @@ async function changeTextColor() {
const pngImageBytes = await new Promise<Uint8Array>((resolve) => const pngImageBytes = await new Promise<Uint8Array>((resolve) =>
canvas.toBlob((blob) => { canvas.toBlob((blob) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer)); reader.onload = () =>
resolve(new Uint8Array(reader.result as ArrayBuffer));
reader.readAsArrayBuffer(blob!); reader.readAsArrayBuffer(blob!);
}, 'image/png') }, 'image/png')
); );
const pngImage = await newPdfDoc.embedPng(pngImageBytes); const pngImage = await newPdfDoc.embedPng(pngImageBytes);
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]); const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
newPage.drawImage(pngImage, { x: 0, y: 0, width: viewport.width, height: viewport.height }); newPage.drawImage(pngImage, {
x: 0,
y: 0,
width: viewport.width,
height: viewport.height,
});
} }
const newPdfBytes = await newPdfDoc.save(); const newPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'text-color-changed.pdf'); downloadFile(
showAlert('Success', 'Text color changed successfully!', 'success', () => { resetState(); }); new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
} catch (e) { console.error(e); showAlert('Error', 'Could not change text color.'); } 'text-color-changed.pdf'
finally { hideLoader(); } );
showAlert('Success', 'Text color changed successfully!', 'success', () => {
resetState();
});
} catch (e) {
console.error(e);
showAlert('Error', 'Could not change text color.');
} finally {
hideLoader();
}
} }

View File

@@ -1,7 +1,8 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; 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 { createIcons, icons } from 'lucide';
import { ViewMetadataState } from '@/types'; import { ViewMetadataState } from '@/types';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
const pageState: ViewMetadataState = { const pageState: ViewMetadataState = {
file: null, file: null,
@@ -25,14 +26,18 @@ function resetState() {
if (fileInput) fileInput.value = ''; if (fileInput) fileInput.value = '';
} }
function createSection(title: string): { wrapper: HTMLDivElement; ul: HTMLUListElement } { function createSection(title: string): {
wrapper: HTMLDivElement;
ul: HTMLUListElement;
} {
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.className = 'mb-6'; wrapper.className = 'mb-6';
const h3 = document.createElement('h3'); const h3 = document.createElement('h3');
h3.className = 'text-lg font-semibold text-white mb-2'; h3.className = 'text-lg font-semibold text-white mb-2';
h3.textContent = title; h3.textContent = title;
const ul = document.createElement('ul'); const ul = document.createElement('ul');
ul.className = 'space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700'; ul.className =
'space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700';
wrapper.append(h3, ul); wrapper.append(h3, ul);
return { wrapper, ul }; return { wrapper, ul };
} }
@@ -61,13 +66,19 @@ function parsePdfDate(pdfDate: string | unknown): string {
const hour = pdfDate.substring(10, 12); const hour = pdfDate.substring(10, 12);
const minute = pdfDate.substring(12, 14); const minute = pdfDate.substring(12, 14);
const second = pdfDate.substring(14, 16); const second = pdfDate.substring(14, 16);
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`).toLocaleString(); return new Date(
`${year}-${month}-${day}T${hour}:${minute}:${second}Z`
).toLocaleString();
} catch { } catch {
return pdfDate; return pdfDate;
} }
} }
function createXmpListItem(key: string, value: string, indent: number = 0): HTMLLIElement { function createXmpListItem(
key: string,
value: string,
indent: number = 0
): HTMLLIElement {
const li = document.createElement('li'); const li = document.createElement('li');
li.className = 'flex flex-col sm:flex-row'; li.className = 'flex flex-col sm:flex-row';
@@ -95,7 +106,11 @@ function createXmpHeaderItem(key: string, indent: number = 0): HTMLLIElement {
return li; return li;
} }
function appendXmpNodes(xmlNode: Element, ulElement: HTMLUListElement, indentLevel: number) { function appendXmpNodes(
xmlNode: Element,
ulElement: HTMLUListElement,
indentLevel: number
) {
const xmpDateKeys = ['xap:CreateDate', 'xap:ModifyDate', 'xap:MetadataDate']; const xmpDateKeys = ['xap:CreateDate', 'xap:ModifyDate', 'xap:MetadataDate'];
const childNodes = Array.from(xmlNode.children); const childNodes = Array.from(xmlNode.children);
@@ -116,8 +131,13 @@ function appendXmpNodes(xmlNode: Element, ulElement: HTMLUListElement, indentLev
key = '(alt container)'; key = '(alt container)';
} }
if (child.getAttribute('rdf:parseType') === 'Resource' && elementChildren.length === 0) { if (
ulElement.appendChild(createXmpListItem(key, '(Empty Resource)', indentLevel)); child.getAttribute('rdf:parseType') === 'Resource' &&
elementChildren.length === 0
) {
ulElement.appendChild(
createXmpListItem(key, '(Empty Resource)', indentLevel)
);
continue; continue;
} }
@@ -143,11 +163,12 @@ async function displayMetadata() {
metadataDisplay.innerHTML = ''; metadataDisplay.innerHTML = '';
pageState.metadata = {}; pageState.metadata = {};
showLoader('Analyzing full PDF metadata...');
try { try {
const pdfBytes = await pageState.file.arrayBuffer(); const result = await loadPdfWithPasswordPrompt(pageState.file);
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise; if (!result) return;
showLoader('Analyzing full PDF metadata...');
const { pdf: pdfjsDoc, file: currentFile } = result;
pageState.file = currentFile;
const [metadataResult, fieldObjects] = await Promise.all([ const [metadataResult, fieldObjects] = await Promise.all([
pdfjsDoc.getMetadata(), pdfjsDoc.getMetadata(),
@@ -166,7 +187,11 @@ async function displayMetadata() {
if (value === null || typeof value === 'undefined') { if (value === null || typeof value === 'undefined') {
displayValue = '- Not Set -'; displayValue = '- Not Set -';
} else if (typeof value === 'object' && value !== null && 'name' in value) { } else if (
typeof value === 'object' &&
value !== null &&
'name' in value
) {
displayValue = String((value as { name: string }).name); displayValue = String((value as { name: string }).name);
} else if (typeof value === 'object') { } else if (typeof value === 'object') {
try { try {
@@ -174,7 +199,10 @@ async function displayMetadata() {
} catch { } catch {
displayValue = '[object Object]'; displayValue = '[object Object]';
} }
} else if ((key === 'CreationDate' || key === 'ModDate') && typeof value === 'string') { } else if (
(key === 'CreationDate' || key === 'ModDate') &&
typeof value === 'string'
) {
displayValue = parsePdfDate(value); displayValue = parsePdfDate(value);
} else { } else {
displayValue = String(value); displayValue = String(value);
@@ -192,7 +220,9 @@ async function displayMetadata() {
const fieldsSection = createSection('Interactive Form Fields'); const fieldsSection = createSection('Interactive Form Fields');
if (fieldObjects && Object.keys(fieldObjects).length > 0) { if (fieldObjects && Object.keys(fieldObjects).length > 0) {
for (const fieldName in fieldObjects) { for (const fieldName in fieldObjects) {
const field = (fieldObjects as Record<string, Array<{ fieldValue?: unknown }>>)[fieldName][0]; const field = (
fieldObjects as Record<string, Array<{ fieldValue?: unknown }>>
)[fieldName][0];
const value = field.fieldValue || '- Not Set -'; const value = field.fieldValue || '- Not Set -';
fieldsSection.ul.appendChild(createListItem(fieldName, String(value))); fieldsSection.ul.appendChild(createListItem(fieldName, String(value)));
} }
@@ -233,10 +263,14 @@ async function displayMetadata() {
} }
metadataDisplay.appendChild(xmpSection.wrapper); metadataDisplay.appendChild(xmpSection.wrapper);
pdfjsDoc.destroy();
createIcons({ icons }); createIcons({ icons });
} catch (e) { } catch (e) {
console.error('Failed to view metadata or fields:', 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.'); showAlert(
'Error',
'Could not fully analyze the PDF. It may be corrupted or have an unusual structure.'
);
} finally { } finally {
hideLoader(); hideLoader();
} }
@@ -252,7 +286,8 @@ async function updateUI() {
if (pageState.file) { if (pageState.file) {
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden'; infoContainer.className = 'flex flex-col overflow-hidden';
@@ -288,9 +323,12 @@ async function updateUI() {
function copyMetadataAsJson() { function copyMetadataAsJson() {
const jsonString = JSON.stringify(pageState.metadata, null, 2); const jsonString = JSON.stringify(pageState.metadata, null, 2);
navigator.clipboard.writeText(jsonString).then(function () { navigator.clipboard
.writeText(jsonString)
.then(function () {
showAlert('Copied', 'Metadata copied to clipboard as JSON.'); showAlert('Copied', 'Metadata copied to clipboard as JSON.');
}).catch(function (err) { })
.catch(function (err) {
console.error('Failed to copy:', err); console.error('Failed to copy:', err);
showAlert('Error', 'Failed to copy metadata to clipboard.'); showAlert('Error', 'Failed to copy metadata to clipboard.');
}); });
@@ -299,7 +337,10 @@ function copyMetadataAsJson() {
function handleFileSelect(files: FileList | null) { function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) { if (files && files.length > 0) {
const file = files[0]; const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file; pageState.file = file;
updateUI(); updateUI();
} }
@@ -339,7 +380,10 @@ document.addEventListener('DOMContentLoaded', function () {
const files = e.dataTransfer?.files; const files = e.dataTransfer?.files;
if (files && files.length > 0) { if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) { const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'); return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
}); });
if (pdfFiles.length > 0) { if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer(); const dataTransfer = new DataTransfer();

View File

@@ -54,3 +54,4 @@ export * from './page-preview-type.ts';
export * from './add-page-labels-type.ts'; export * from './add-page-labels-type.ts';
export * from './pdf-to-tiff-type.ts'; export * from './pdf-to-tiff-type.ts';
export * from './pdf-to-cbz-type.ts'; export * from './pdf-to-cbz-type.ts';
export * from './password-prompt-type.ts';

View File

@@ -0,0 +1,7 @@
import type { PDFDocumentProxy } from 'pdfjs-dist';
export interface LoadedPdf {
pdf: PDFDocumentProxy;
bytes: ArrayBuffer;
file: File;
}

View File

@@ -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<unknown> | null = null;
function getEl<T extends HTMLElement>(id: string): T | null {
return document.getElementById(id) as T | null;
}
function esc(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function ensureSingleModal(): HTMLDivElement {
let modal = getEl<HTMLDivElement>('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 = `
<div class="bg-gray-800 rounded-xl border border-gray-700 shadow-2xl max-w-md w-full overflow-hidden">
<div class="p-6">
<div class="flex items-start gap-4 mb-4">
<div class="w-12 h-12 flex items-center justify-center rounded-full bg-indigo-500/10 flex-shrink-0">
<i data-lucide="lock" class="w-6 h-6 text-indigo-400"></i>
</div>
<div class="flex-1">
<h3 id="password-modal-title" class="text-xl font-bold text-white mb-1">Password Required</h3>
<p id="password-modal-subtitle" class="text-gray-400 text-sm truncate"></p>
</div>
</div>
<div class="mt-4">
<div class="relative">
<input type="password" id="password-modal-input"
class="w-full bg-gray-700 border border-gray-600 text-gray-200 rounded-lg px-4 py-2.5 pr-10 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
placeholder="Enter password" autocomplete="off" />
<button id="password-modal-toggle" type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-200">
<i data-lucide="eye" class="w-4 h-4"></i>
</button>
</div>
<p id="password-modal-error" class="text-xs text-red-400 mt-2 hidden"></p>
<p id="password-modal-progress" class="text-xs text-gray-400 mt-2 hidden"></p>
</div>
</div>
<div class="flex gap-3 p-4 border-t border-gray-700">
<button id="password-modal-cancel"
class="flex-1 px-4 py-2.5 bg-gray-700 hover:bg-gray-600 text-white rounded-lg font-medium transition-colors">
Skip
</button>
<button id="password-modal-submit"
class="flex-1 px-4 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg font-medium transition-colors">
Unlock
</button>
</div>
</div>`;
document.body.appendChild(modal);
return modal;
}
function ensureBatchModal(): HTMLDivElement {
let modal = getEl<HTMLDivElement>('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 = `
<div class="bg-gray-800 rounded-xl border border-gray-700 shadow-2xl max-w-lg w-full max-h-[90vh] flex flex-col overflow-hidden">
<div class="p-6 overflow-y-auto flex-1 min-h-0">
<div class="flex items-start gap-4 mb-4">
<div class="w-12 h-12 flex items-center justify-center rounded-full bg-indigo-500/10 flex-shrink-0">
<i data-lucide="lock" class="w-6 h-6 text-indigo-400"></i>
</div>
<div class="flex-1">
<h3 id="batch-modal-title" class="text-xl font-bold text-white mb-1"></h3>
<p class="text-gray-400 text-sm">Enter passwords for each encrypted file</p>
</div>
</div>
<div class="flex items-center gap-2 mt-3 mb-3">
<input type="checkbox" id="batch-modal-same-pw" checked
class="w-4 h-4 rounded bg-gray-700 border-gray-600 text-indigo-500 focus:ring-indigo-500 focus:ring-offset-0 cursor-pointer" />
<label for="batch-modal-same-pw" class="text-sm text-gray-300 cursor-pointer select-none">Use same password for all files</label>
</div>
<div id="batch-modal-shared" class="mb-3">
<div class="relative">
<input type="password" id="batch-modal-shared-input"
class="w-full bg-gray-700 border border-gray-600 text-gray-200 rounded-lg px-4 py-2.5 pr-10 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
placeholder="Password for all files" autocomplete="off" />
<button id="batch-modal-shared-toggle" type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-200">
<i data-lucide="eye" class="w-4 h-4"></i>
</button>
</div>
</div>
<div id="batch-modal-filelist" class="space-y-2 hidden"></div>
<p id="batch-modal-error" class="text-xs text-red-400 mt-2 hidden"></p>
<p id="batch-modal-progress" class="text-xs text-gray-400 mt-2 hidden"></p>
</div>
<div class="flex gap-3 p-4 border-t border-gray-700 flex-shrink-0">
<button id="batch-modal-cancel"
class="flex-1 px-4 py-2.5 bg-gray-700 hover:bg-gray-600 text-white rounded-lg font-medium transition-colors">
Skip All
</button>
<button id="batch-modal-submit"
class="flex-1 px-4 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg font-medium transition-colors">
Unlock All
</button>
</div>
</div>`;
document.body.appendChild(modal);
return modal;
}
function validatePasswordWithPdfjs(
pdfBytes: ArrayBuffer,
password: string
): Promise<boolean> {
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<boolean> {
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<File> {
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<File | null> {
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<HTMLInputElement>('password-modal-input');
const titleEl = getEl<HTMLHeadingElement>('password-modal-title');
const subtitleEl = getEl<HTMLParagraphElement>('password-modal-subtitle');
const errorEl = getEl<HTMLParagraphElement>('password-modal-error');
const progressEl = getEl<HTMLParagraphElement>('password-modal-progress');
const submitBtn = getEl<HTMLButtonElement>('password-modal-submit');
const cancelBtn = getEl<HTMLButtonElement>('password-modal-cancel');
const toggleBtn = getEl<HTMLButtonElement>('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<File | null>((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<Map<number, File>> {
if (activeModalPromise) {
await activeModalPromise;
}
const decryptedFiles = new Map<number, File>();
if (encryptedIndices.length === 0) return decryptedFiles;
if (encryptedIndices.length === 1) {
const idx = encryptedIndices[0];
const decrypted = await promptAndDecryptFile(files[idx]);
if (decrypted) decryptedFiles.set(idx, decrypted);
return decryptedFiles;
}
if (cachedPassword) {
let allValid = true;
for (const idx of encryptedIndices) {
const bytes = (await readFileAsArrayBuffer(files[idx])) as ArrayBuffer;
const valid = await validatePasswordWithPdfjs(bytes, cachedPassword);
if (!valid) {
allValid = false;
cachedPassword = null;
break;
}
}
if (allValid && cachedPassword) {
const tempMap = new Map<number, File>();
let allDecrypted = true;
for (const idx of encryptedIndices) {
try {
tempMap.set(
idx,
await decryptFileWithPassword(files[idx], cachedPassword)
);
} catch {
cachedPassword = null;
allDecrypted = false;
break;
}
}
if (allDecrypted && tempMap.size === encryptedIndices.length) {
for (const [k, v] of tempMap) decryptedFiles.set(k, v);
return decryptedFiles;
}
}
}
const fileNames = encryptedIndices.map((i) => files[i].name);
const modal = ensureBatchModal();
const titleEl = getEl<HTMLHeadingElement>('batch-modal-title');
const samePwCheckbox = getEl<HTMLInputElement>('batch-modal-same-pw');
const sharedSection = getEl<HTMLDivElement>('batch-modal-shared');
const sharedInput = getEl<HTMLInputElement>('batch-modal-shared-input');
const sharedToggle = getEl<HTMLButtonElement>('batch-modal-shared-toggle');
const filelistEl = getEl<HTMLDivElement>('batch-modal-filelist');
const errorEl = getEl<HTMLParagraphElement>('batch-modal-error');
const progressEl = getEl<HTMLParagraphElement>('batch-modal-progress');
const submitBtn = getEl<HTMLButtonElement>('batch-modal-submit');
const cancelBtn = getEl<HTMLButtonElement>('batch-modal-cancel');
if (
!submitBtn ||
!cancelBtn ||
!samePwCheckbox ||
!sharedInput ||
!filelistEl
) {
return decryptedFiles;
}
if (titleEl)
titleEl.textContent = `${fileNames.length} Files Need a Password`;
samePwCheckbox.checked = true;
sharedInput.value = '';
sharedInput.type = 'password';
if (sharedSection) sharedSection.classList.remove('hidden');
filelistEl.classList.add('hidden');
filelistEl.innerHTML = fileNames
.map(
(name, i) =>
`<div class="flex items-center gap-2 p-2 bg-gray-700/50 rounded-lg transition-all" data-file-idx="${i}">
<i data-lucide="file-lock" class="w-4 h-4 text-indigo-400 flex-shrink-0" data-icon-idx="${i}"></i>
<span class="text-sm text-gray-300 truncate flex-1" title="${esc(name)}">${esc(name)}</span>
<div class="flex items-center gap-1.5 flex-shrink-0">
<input type="password" data-pw-idx="${i}" placeholder="Password"
class="w-32 bg-gray-600 border border-gray-500 text-gray-200 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-indigo-500 focus:border-transparent" autocomplete="off" />
<button type="button" data-skip-idx="${i}" title="Skip this file"
class="p-1 rounded text-gray-400 hover:text-red-400 hover:bg-gray-600 transition-colors">
<i data-lucide="x" class="w-3.5 h-3.5"></i>
</button>
</div>
</div>`
)
.join('');
if (errorEl) {
errorEl.textContent = '';
errorEl.classList.add('hidden');
}
if (progressEl) {
progressEl.textContent = '';
progressEl.classList.add('hidden');
}
submitBtn.disabled = false;
submitBtn.textContent = 'Unlock All';
submitBtn.dataset.originalText = 'Unlock All';
cancelBtn.disabled = false;
modal.classList.remove('hidden');
modal.classList.add('flex');
createIcons({ icons });
setTimeout(() => sharedInput.focus(), 100);
const batchPromise = new Promise<Map<number, File>>((resolve) => {
let resolved = false;
let busy = false;
const skippedSet = new Set<number>();
const succeededSet = new Set<number>();
function getRemainingCount(): number {
let count = 0;
for (let i = 0; i < fileNames.length; i++) {
if (!skippedSet.has(i) && !succeededSet.has(i)) count++;
}
return count;
}
function updateButtons(autoClose = false) {
const hasSucceeded = succeededSet.size > 0;
cancelBtn.textContent = hasSucceeded ? 'Skip Remaining' : 'Skip All';
const remaining = getRemainingCount();
if (autoClose && remaining === 0) {
finish(decryptedFiles);
return;
}
if (hasSucceeded) {
submitBtn.textContent = `Unlock Remaining (${remaining})`;
submitBtn.dataset.originalText = submitBtn.textContent;
}
}
function toggleMode() {
const useSame = samePwCheckbox.checked;
if (sharedSection) sharedSection.classList.toggle('hidden', !useSame);
filelistEl.classList.toggle('hidden', useSame);
if (useSame) {
setTimeout(() => sharedInput.focus(), 50);
} else {
const firstInput = filelistEl.querySelector<HTMLInputElement>(
'input[data-pw-idx]:not(:disabled)'
);
if (firstInput) setTimeout(() => firstInput.focus(), 50);
}
}
function markRowSuccess(localIdx: number) {
succeededSet.add(localIdx);
const row = filelistEl.querySelector<HTMLDivElement>(
`[data-file-idx="${localIdx}"]`
);
if (!row) return;
row.classList.remove('border', 'border-red-500/50');
row.classList.add('opacity-50', 'border', 'border-green-500/50');
const pwInput = row.querySelector<HTMLInputElement>('input[data-pw-idx]');
if (pwInput) pwInput.disabled = true;
const skipBtn = row.querySelector<HTMLButtonElement>('[data-skip-idx]');
if (skipBtn) skipBtn.classList.add('hidden');
const iconEl = row.querySelector<HTMLElement>(
`[data-icon-idx="${localIdx}"]`
);
if (iconEl) {
iconEl.setAttribute('data-lucide', 'check-circle');
iconEl.classList.remove('text-indigo-400');
iconEl.classList.add('text-green-400');
}
createIcons({ icons });
}
function markRowFailed(localIdx: number) {
const row = filelistEl.querySelector<HTMLDivElement>(
`[data-file-idx="${localIdx}"]`
);
if (!row) return;
row.classList.remove('border-green-500/50');
row.classList.add('border', 'border-red-500/50');
const pwInput = row.querySelector<HTMLInputElement>('input[data-pw-idx]');
if (pwInput) {
pwInput.value = '';
pwInput.focus();
pwInput.classList.add('border-red-500');
setTimeout(() => pwInput.classList.remove('border-red-500'), 2000);
}
}
function onSkipFile(e: Event) {
if (busy) return;
const btn = (e.target as HTMLElement).closest<HTMLButtonElement>(
'[data-skip-idx]'
);
if (!btn || btn.dataset.skipIdx === undefined) return;
const idx = parseInt(btn.dataset.skipIdx, 10);
if (isNaN(idx) || succeededSet.has(idx)) return;
const row = filelistEl.querySelector<HTMLDivElement>(
`[data-file-idx="${idx}"]`
);
if (!row) return;
if (skippedSet.has(idx)) {
skippedSet.delete(idx);
row.classList.remove('opacity-40');
const pwInput =
row.querySelector<HTMLInputElement>('input[data-pw-idx]');
if (pwInput) pwInput.disabled = false;
btn.title = 'Skip this file';
} else {
skippedSet.add(idx);
row.classList.add('opacity-40');
row.classList.remove('border', 'border-red-500/50');
const pwInput =
row.querySelector<HTMLInputElement>('input[data-pw-idx]');
if (pwInput) pwInput.disabled = true;
btn.title = 'Include this file';
}
updateButtons(true);
}
function cleanup() {
modal.classList.add('hidden');
modal.classList.remove('flex');
submitBtn.removeEventListener('click', onSubmit);
cancelBtn.removeEventListener('click', onCancel);
samePwCheckbox.removeEventListener('change', toggleMode);
filelistEl.removeEventListener('click', onSkipFile);
if (sharedToggle)
sharedToggle.removeEventListener('click', onSharedToggle);
sharedInput.removeEventListener('keydown', onKeydown);
}
function finish(result: Map<number, File>) {
if (resolved) return;
resolved = true;
cleanup();
resolve(result);
}
async function onSubmit() {
if (busy) return;
const useSame = samePwCheckbox.checked;
const toProcess: { localIdx: number; password: string }[] = [];
for (let i = 0; i < fileNames.length; i++) {
if (skippedSet.has(i) || succeededSet.has(i)) continue;
let password: string;
if (useSame) {
password = sharedInput.value;
} else {
const pwInput = filelistEl.querySelector<HTMLInputElement>(
`input[data-pw-idx="${i}"]`
);
password = pwInput?.value || '';
}
if (!password) {
if (errorEl) {
const msg = useSame
? 'Please enter a password'
: `Please enter a password for ${fileNames[i]} or skip it`;
errorEl.textContent = msg;
errorEl.classList.remove('hidden');
}
return;
}
toProcess.push({ localIdx: i, password });
}
if (toProcess.length === 0) {
finish(decryptedFiles);
return;
}
if (errorEl) errorEl.classList.add('hidden');
busy = true;
submitBtn.disabled = true;
cancelBtn.disabled = true;
const failedNames: string[] = [];
for (let i = 0; i < toProcess.length; i++) {
const { localIdx, password } = toProcess[i];
const realIdx = encryptedIndices[localIdx];
if (progressEl) {
progressEl.textContent = `Validating ${i + 1} of ${toProcess.length}: ${files[realIdx].name}`;
progressEl.classList.remove('hidden');
}
const bytes = (await readFileAsArrayBuffer(
files[realIdx]
)) as ArrayBuffer;
const valid = await validatePasswordWithPdfjs(bytes, password);
if (!valid) {
failedNames.push(files[realIdx].name);
markRowFailed(localIdx);
continue;
}
if (progressEl) {
progressEl.textContent = `Decrypting ${i + 1} of ${toProcess.length}: ${files[realIdx].name}`;
}
try {
const decrypted = await decryptFileWithPassword(
files[realIdx],
password
);
decryptedFiles.set(realIdx, decrypted);
markRowSuccess(localIdx);
} catch {
failedNames.push(files[realIdx].name);
markRowFailed(localIdx);
}
}
if (progressEl) progressEl.classList.add('hidden');
busy = false;
submitBtn.disabled = false;
cancelBtn.disabled = false;
if (failedNames.length > 0) {
if (errorEl) {
errorEl.textContent = `Wrong password for: ${failedNames.join(', ')}`;
errorEl.classList.remove('hidden');
}
if (!samePwCheckbox.checked) {
const firstFailed = filelistEl.querySelector<HTMLInputElement>(
'input[data-pw-idx]:not(:disabled)'
);
if (firstFailed) firstFailed.focus();
} else {
sharedInput.value = '';
sharedInput.focus();
}
updateButtons();
submitBtn.textContent = 'Retry Failed';
return;
}
if (useSame && toProcess.length > 0) {
cachedPassword = toProcess[0].password;
}
updateButtons();
if (!resolved) finish(decryptedFiles);
}
function onCancel() {
if (busy) return;
finish(decryptedFiles);
}
function onKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault();
onSubmit();
} else if (e.key === 'Escape') {
e.preventDefault();
onCancel();
}
}
function onSharedToggle() {
const isPassword = sharedInput.type === 'password';
sharedInput.type = isPassword ? 'text' : 'password';
const icon = sharedToggle?.querySelector('i[data-lucide]');
if (icon)
icon.setAttribute('data-lucide', isPassword ? 'eye-off' : 'eye');
createIcons({ icons });
}
samePwCheckbox.addEventListener('change', toggleMode);
submitBtn.addEventListener('click', onSubmit);
cancelBtn.addEventListener('click', onCancel);
filelistEl.addEventListener('click', onSkipFile);
sharedInput.addEventListener('keydown', onKeydown);
if (sharedToggle) sharedToggle.addEventListener('click', onSharedToggle);
});
activeModalPromise = batchPromise;
batchPromise.finally(() => {
if (activeModalPromise === batchPromise) activeModalPromise = null;
});
return batchPromise;
}
export type { LoadedPdf };
export async function loadPdfWithPasswordPrompt(
file: File,
files?: File[],
index?: number
): Promise<LoadedPdf | null> {
let bytes = (await readFileAsArrayBuffer(file)) as ArrayBuffer;
let currentFile = file;
try {
const pdf = await getPDFDocument(bytes).promise;
return { pdf, bytes, file: currentFile };
} catch (err: unknown) {
if (
err &&
typeof err === 'object' &&
'name' in err &&
(err as { name: string }).name === 'PasswordException'
) {
const decryptedFile = await promptAndDecryptFile(currentFile);
if (!decryptedFile) return null;
currentFile = decryptedFile;
if (files && index !== undefined) {
files[index] = decryptedFile;
}
bytes = (await readFileAsArrayBuffer(decryptedFile)) as ArrayBuffer;
const pdf = await getPDFDocument(bytes).promise;
return { pdf, bytes, file: currentFile };
}
throw err;
}
}
export async function batchDecryptIfNeeded(files: File[]): Promise<File[]> {
const encryptedIndices: number[] = [];
for (let i = 0; i < files.length; i++) {
const encrypted = await isFileEncrypted(files[i]);
if (encrypted) encryptedIndices.push(i);
}
if (encryptedIndices.length === 0) return [...files];
const decryptedMap = await promptAndDecryptBatch(files, encryptedIndices);
const skippedSet = new Set(
encryptedIndices.filter((idx) => !decryptedMap.has(idx))
);
const result: File[] = [];
for (let i = 0; i < files.length; i++) {
if (skippedSet.has(i)) continue;
result.push(decryptedMap.get(i) ?? files[i]);
}
return result;
}
export async function handleEncryptedFiles(
files: File[],
encryptedIndices: number[]
): Promise<Map<number, File>> {
return promptAndDecryptBatch(files, encryptedIndices);
}

File diff suppressed because it is too large Load Diff