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
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>
<!-- COMPLIANCE SECTION START -->

View File

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

View File

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

View File

@@ -2,230 +2,274 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import { AddBlankPageState } from '@/types';
const pageState: AddBlankPageState = {
file: null,
pdfDoc: null,
file: null,
pdfDoc: null,
};
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
pageState.file = null;
pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const pagePositionInput = document.getElementById('page-position') as HTMLInputElement;
if (pagePositionInput) pagePositionInput.value = '0';
const pagePositionInput = document.getElementById(
'page-position'
) as HTMLInputElement;
if (pagePositionInput) pagePositionInput.value = '0';
const pageCountInput = document.getElementById('page-count') as HTMLInputElement;
if (pageCountInput) pageCountInput.value = '1';
const pageCountInput = document.getElementById(
'page-count'
) as HTMLInputElement;
if (pageCountInput) pageCountInput.value = '1';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const pagePositionHint = document.getElementById('page-position-hint');
const pagePositionInput = document.getElementById('page-position') as HTMLInputElement;
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const pagePositionHint = document.getElementById('page-position-hint');
const pagePositionInput = document.getElementById(
'page-position'
) as HTMLInputElement;
if (!fileDisplayArea) return;
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
// Load PDF document
try {
showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false
});
hideLoader();
// Load PDF document
try {
const result = await loadPdfWithPasswordPrompt(pageState.file);
if (!result) {
resetState();
return;
}
showLoader('Loading PDF...');
pageState.file = result.file;
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
throwOnInvalidObject: false,
});
result.pdf.destroy();
hideLoader();
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
if (pagePositionHint) {
pagePositionHint.textContent = `Enter 0 to insert at the beginning, or ${pageCount} to insert at the end.`;
}
if (pagePositionInput) {
pagePositionInput.max = pageCount.toString();
}
if (pagePositionHint) {
pagePositionHint.textContent = `Enter 0 to insert at the beginning, or ${pageCount} to insert at the end.`;
}
if (pagePositionInput) {
pagePositionInput.max = pageCount.toString();
}
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
async function addBlankPages() {
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF first.');
return;
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF first.');
return;
}
const pagePositionInput = document.getElementById(
'page-position'
) as HTMLInputElement;
const pageCountInput = document.getElementById(
'page-count'
) as HTMLInputElement;
const position = parseInt(pagePositionInput.value);
const insertCount = parseInt(pageCountInput.value);
const totalPages = pageState.pdfDoc.getPageCount();
if (isNaN(position) || position < 0 || position > totalPages) {
showAlert(
'Invalid Input',
`Please enter a number between 0 and ${totalPages}.`
);
return;
}
if (isNaN(insertCount) || insertCount < 1) {
showAlert(
'Invalid Input',
'Please enter a valid number of pages (1 or more).'
);
return;
}
showLoader(
`Adding ${insertCount} blank page${insertCount > 1 ? 's' : ''}...`
);
try {
const newPdf = await PDFLibDocument.create();
const { width, height } = pageState.pdfDoc.getPage(0).getSize();
const allIndices = Array.from({ length: totalPages }, function (_, i) {
return i;
});
const indicesBefore = allIndices.slice(0, position);
const indicesAfter = allIndices.slice(position);
if (indicesBefore.length > 0) {
const copied = await newPdf.copyPages(pageState.pdfDoc, indicesBefore);
copied.forEach(function (p) {
newPdf.addPage(p);
});
}
const pagePositionInput = document.getElementById('page-position') as HTMLInputElement;
const pageCountInput = document.getElementById('page-count') as HTMLInputElement;
const position = parseInt(pagePositionInput.value);
const insertCount = parseInt(pageCountInput.value);
const totalPages = pageState.pdfDoc.getPageCount();
if (isNaN(position) || position < 0 || position > totalPages) {
showAlert('Invalid Input', `Please enter a number between 0 and ${totalPages}.`);
return;
// Add the specified number of blank pages
for (let i = 0; i < insertCount; i++) {
newPdf.addPage([width, height]);
}
if (isNaN(insertCount) || insertCount < 1) {
showAlert('Invalid Input', 'Please enter a valid number of pages (1 or more).');
return;
if (indicesAfter.length > 0) {
const copied = await newPdf.copyPages(pageState.pdfDoc, indicesAfter);
copied.forEach(function (p) {
newPdf.addPage(p);
});
}
showLoader(`Adding ${insertCount} blank page${insertCount > 1 ? 's' : ''}...`);
const newPdfBytes = await newPdf.save();
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
try {
const newPdf = await PDFLibDocument.create();
const { width, height } = pageState.pdfDoc.getPage(0).getSize();
const allIndices = Array.from({ length: totalPages }, function (_, i) { return i; });
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`${originalName}_blank-pages-added.pdf`
);
const indicesBefore = allIndices.slice(0, position);
const indicesAfter = allIndices.slice(position);
if (indicesBefore.length > 0) {
const copied = await newPdf.copyPages(pageState.pdfDoc, indicesBefore);
copied.forEach(function (p) { newPdf.addPage(p); });
}
// Add the specified number of blank pages
for (let i = 0; i < insertCount; i++) {
newPdf.addPage([width, height]);
}
if (indicesAfter.length > 0) {
const copied = await newPdf.copyPages(pageState.pdfDoc, indicesAfter);
copied.forEach(function (p) { newPdf.addPage(p); });
}
const newPdfBytes = await newPdf.save();
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`${originalName}_blank-pages-added.pdf`
);
showAlert('Success', `Added ${insertCount} blank page${insertCount > 1 ? 's' : ''} successfully!`, 'success', function () {
resetState();
});
} catch (e) {
console.error(e);
showAlert('Error', `Could not add blank page${insertCount > 1 ? 's' : ''}.`);
} finally {
hideLoader();
}
showAlert(
'Success',
`Added ${insertCount} blank page${insertCount > 1 ? 's' : ''} successfully!`,
'success',
function () {
resetState();
}
);
} catch (e) {
console.error(e);
showAlert(
'Error',
`Could not add blank page${insertCount > 1 ? 's' : ''}.`
);
} finally {
hideLoader();
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
if (files && files.length > 0) {
const file = files[0];
if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
});
}
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', addBlankPages);
}
if (processBtn) {
processBtn.addEventListener('click', addBlankPages);
}
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,102 +3,156 @@ import { showAlert, showLoader, hideLoader } from '../ui.js';
import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js';
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
import { BackgroundColorState } from '@/types';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
const pageState: BackgroundColorState = { file: null, pdfDoc: null };
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializePage); }
else { initializePage(); }
function initializePage() {
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const backBtn = document.getElementById('back-to-tools');
const processBtn = document.getElementById('process-btn');
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
fileInput.addEventListener('click', () => { fileInput.value = ''; });
}
if (dropZone) {
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-indigo-500'); });
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('border-indigo-500'); });
dropZone.addEventListener('drop', (e) => {
e.preventDefault(); dropZone.classList.remove('border-indigo-500');
if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files);
});
}
if (backBtn) backBtn.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; });
if (processBtn) processBtn.addEventListener('click', changeBackgroundColor);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
} else {
initializePage();
}
function handleFileUpload(e: Event) { const input = e.target as HTMLInputElement; if (input.files?.length) handleFiles(input.files); }
function initializePage() {
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const backBtn = document.getElementById('back-to-tools');
const processBtn = document.getElementById('process-btn');
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-indigo-500');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('border-indigo-500');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-indigo-500');
if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files);
});
}
if (backBtn)
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
if (processBtn) processBtn.addEventListener('click', changeBackgroundColor);
}
function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement;
if (input.files?.length) handleFiles(input.files);
}
async function handleFiles(files: FileList) {
const file = files[0];
if (!file || file.type !== 'application/pdf') { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; }
const file = files[0];
if (!file || file.type !== 'application/pdf') {
showAlert('Invalid File', 'Please upload a valid PDF file.');
return;
}
try {
const result = await loadPdfWithPasswordPrompt(file);
if (!result) return;
showLoader('Loading PDF...');
try {
const arrayBuffer = await file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer);
pageState.file = file;
updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden');
} catch (error) { console.error(error); showAlert('Error', 'Failed to load PDF file.'); }
finally { hideLoader(); }
result.pdf.destroy();
pageState.pdfDoc = await PDFLibDocument.load(result.bytes);
pageState.file = result.file;
updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden');
} catch (error) {
console.error(error);
showAlert('Error', 'Failed to load PDF file.');
} finally {
hideLoader();
}
}
function updateFileDisplay() {
const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageState.pdfDoc.getPageCount()} pages`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = resetState;
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageState.pdfDoc.getPageCount()} pages`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = resetState;
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
}
function resetState() {
pageState.file = null; pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
document.getElementById('options-panel')?.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
pageState.file = null;
pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
document.getElementById('options-panel')?.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function changeBackgroundColor() {
if (!pageState.pdfDoc) { showAlert('Error', 'Please upload a PDF file first.'); return; }
const colorHex = (document.getElementById('background-color') as HTMLInputElement).value;
const color = hexToRgb(colorHex);
showLoader('Changing background color...');
try {
const newPdfDoc = await PDFLibDocument.create();
for (let i = 0; i < pageState.pdfDoc.getPageCount(); i++) {
const [originalPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
const { width, height } = originalPage.getSize();
const newPage = newPdfDoc.addPage([width, height]);
newPage.drawRectangle({ x: 0, y: 0, width, height, color: rgb(color.r, color.g, color.b) });
const embeddedPage = await newPdfDoc.embedPage(originalPage);
newPage.drawPage(embeddedPage, { x: 0, y: 0, width, height });
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'background-changed.pdf');
showAlert('Success', 'Background color changed successfully!', 'success', () => { resetState(); });
} catch (e) { console.error(e); showAlert('Error', 'Could not change the background color.'); }
finally { hideLoader(); }
if (!pageState.pdfDoc) {
showAlert('Error', 'Please upload a PDF file first.');
return;
}
const colorHex = (
document.getElementById('background-color') as HTMLInputElement
).value;
const color = hexToRgb(colorHex);
showLoader('Changing background color...');
try {
const newPdfDoc = await PDFLibDocument.create();
for (let i = 0; i < pageState.pdfDoc.getPageCount(); i++) {
const [originalPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
const { width, height } = originalPage.getSize();
const newPage = newPdfDoc.addPage([width, height]);
newPage.drawRectangle({
x: 0,
y: 0,
width,
height,
color: rgb(color.r, color.g, color.b),
});
const embeddedPage = await newPdfDoc.embedPage(originalPage);
newPage.drawPage(embeddedPage, { x: 0, y: 0, width, height });
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'background-changed.pdf'
);
showAlert(
'Success',
'Background color changed successfully!',
'success',
() => {
resetState();
}
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not change the background color.');
} finally {
hideLoader();
}
}

View File

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

View File

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

View File

@@ -1,309 +1,349 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes, hexToRgb, getPDFDocument } from '../utils/helpers.js';
import {
downloadFile,
formatBytes,
hexToRgb,
getPDFDocument,
} from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
import { CombineSinglePageState } from '@/types';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
const pageState: CombineSinglePageState = {
file: null,
pdfDoc: null,
file: null,
pdfDoc: null,
};
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
pageState.file = null;
pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
try {
showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false
});
hideLoader();
try {
const result = await loadPdfWithPasswordPrompt(pageState.file);
if (!result) {
resetState();
return;
}
showLoader('Loading PDF...');
result.pdf.destroy();
pageState.file = result.file;
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
throwOnInvalidObject: false,
});
hideLoader();
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
async function combineToSinglePage() {
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF first.');
return;
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF first.');
return;
}
const orientation = (
document.getElementById('combine-orientation') as HTMLSelectElement
).value;
const spacing =
parseInt(
(document.getElementById('page-spacing') as HTMLInputElement).value
) || 0;
const backgroundColorHex = (
document.getElementById('background-color') as HTMLInputElement
).value;
const addSeparator = (
document.getElementById('add-separator') as HTMLInputElement
).checked;
const separatorThickness =
parseFloat(
(document.getElementById('separator-thickness') as HTMLInputElement).value
) || 0.5;
const separatorColorHex = (
document.getElementById('separator-color') as HTMLInputElement
).value;
const backgroundColor = hexToRgb(backgroundColorHex);
const separatorColor = hexToRgb(separatorColorHex);
showLoader('Combining pages...');
try {
const sourceDoc = pageState.pdfDoc;
const newDoc = await PDFLibDocument.create();
const pdfBytes = await sourceDoc.save();
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
const sourcePages = sourceDoc.getPages();
let maxWidth = 0;
let maxHeight = 0;
let totalWidth = 0;
let totalHeight = 0;
sourcePages.forEach(function (page) {
const { width, height } = page.getSize();
if (width > maxWidth) maxWidth = width;
if (height > maxHeight) maxHeight = height;
totalWidth += width;
totalHeight += height;
});
let finalWidth: number, finalHeight: number;
if (orientation === 'horizontal') {
finalWidth = totalWidth + Math.max(0, sourcePages.length - 1) * spacing;
finalHeight = maxHeight;
} else {
finalWidth = maxWidth;
finalHeight = totalHeight + Math.max(0, sourcePages.length - 1) * spacing;
}
const orientation = (document.getElementById('combine-orientation') as HTMLSelectElement).value;
const spacing = parseInt((document.getElementById('page-spacing') as HTMLInputElement).value) || 0;
const backgroundColorHex = (document.getElementById('background-color') as HTMLInputElement).value;
const addSeparator = (document.getElementById('add-separator') as HTMLInputElement).checked;
const separatorThickness = parseFloat((document.getElementById('separator-thickness') as HTMLInputElement).value) || 0.5;
const separatorColorHex = (document.getElementById('separator-color') as HTMLInputElement).value;
const newPage = newDoc.addPage([finalWidth, finalHeight]);
const backgroundColor = hexToRgb(backgroundColorHex);
const separatorColor = hexToRgb(separatorColorHex);
if (backgroundColorHex.toUpperCase() !== '#FFFFFF') {
newPage.drawRectangle({
x: 0,
y: 0,
width: finalWidth,
height: finalHeight,
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
});
}
showLoader('Combining pages...');
let currentX = 0;
let currentY = finalHeight;
try {
const sourceDoc = pageState.pdfDoc;
const newDoc = await PDFLibDocument.create();
for (let i = 0; i < sourcePages.length; i++) {
showLoader(`Processing page ${i + 1} of ${sourcePages.length}...`);
const sourcePage = sourcePages[i];
const { width, height } = sourcePage.getSize();
const pdfBytes = await sourceDoc.save();
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
try {
const page = await pdfjsDoc.getPage(i + 1);
const scale = 2.0;
const viewport = page.getViewport({ scale });
const sourcePages = sourceDoc.getPages();
let maxWidth = 0;
let maxHeight = 0;
let totalWidth = 0;
let totalHeight = 0;
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d')!;
sourcePages.forEach(function (page) {
const { width, height } = page.getSize();
if (width > maxWidth) maxWidth = width;
if (height > maxHeight) maxHeight = height;
totalWidth += width;
totalHeight += height;
});
await page.render({
canvasContext: context,
viewport,
canvas,
}).promise;
const pngDataUrl = canvas.toDataURL('image/png');
const pngImage = await newDoc.embedPng(pngDataUrl);
let finalWidth: number, finalHeight: number;
if (orientation === 'horizontal') {
finalWidth = totalWidth + Math.max(0, sourcePages.length - 1) * spacing;
finalHeight = maxHeight;
const y = (finalHeight - height) / 2;
newPage.drawImage(pngImage, { x: currentX, y, width, height });
} else {
finalWidth = maxWidth;
finalHeight = totalHeight + Math.max(0, sourcePages.length - 1) * spacing;
currentY -= height;
const x = (finalWidth - width) / 2;
newPage.drawImage(pngImage, { x, y: currentY, width, height });
}
} catch (renderError) {
console.warn(`Failed to render page ${i + 1}:`, renderError);
}
const newPage = newDoc.addPage([finalWidth, finalHeight]);
if (backgroundColorHex.toUpperCase() !== '#FFFFFF') {
newPage.drawRectangle({
x: 0,
y: 0,
width: finalWidth,
height: finalHeight,
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
});
if (addSeparator && i < sourcePages.length - 1) {
if (orientation === 'horizontal') {
const lineX = currentX + width + spacing / 2;
newPage.drawLine({
start: { x: lineX, y: 0 },
end: { x: lineX, y: finalHeight },
thickness: separatorThickness,
color: rgb(separatorColor.r, separatorColor.g, separatorColor.b),
});
currentX += width + spacing;
} else {
const lineY = currentY - spacing / 2;
newPage.drawLine({
start: { x: 0, y: lineY },
end: { x: finalWidth, y: lineY },
thickness: separatorThickness,
color: rgb(separatorColor.r, separatorColor.g, separatorColor.b),
});
currentY -= spacing;
}
let currentX = 0;
let currentY = finalHeight;
for (let i = 0; i < sourcePages.length; i++) {
showLoader(`Processing page ${i + 1} of ${sourcePages.length}...`);
const sourcePage = sourcePages[i];
const { width, height } = sourcePage.getSize();
try {
const page = await pdfjsDoc.getPage(i + 1);
const scale = 2.0;
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d')!;
await page.render({
canvasContext: context,
viewport,
canvas
}).promise;
const pngDataUrl = canvas.toDataURL('image/png');
const pngImage = await newDoc.embedPng(pngDataUrl);
if (orientation === 'horizontal') {
const y = (finalHeight - height) / 2;
newPage.drawImage(pngImage, { x: currentX, y, width, height });
} else {
currentY -= height;
const x = (finalWidth - width) / 2;
newPage.drawImage(pngImage, { x, y: currentY, width, height });
}
} catch (renderError) {
console.warn(`Failed to render page ${i + 1}:`, renderError);
}
if (addSeparator && i < sourcePages.length - 1) {
if (orientation === 'horizontal') {
const lineX = currentX + width + spacing / 2;
newPage.drawLine({
start: { x: lineX, y: 0 },
end: { x: lineX, y: finalHeight },
thickness: separatorThickness,
color: rgb(separatorColor.r, separatorColor.g, separatorColor.b),
});
currentX += width + spacing;
} else {
const lineY = currentY - spacing / 2;
newPage.drawLine({
start: { x: 0, y: lineY },
end: { x: finalWidth, y: lineY },
thickness: separatorThickness,
color: rgb(separatorColor.r, separatorColor.g, separatorColor.b),
});
currentY -= spacing;
}
} else {
if (orientation === 'horizontal') {
currentX += width + spacing;
} else {
currentY -= spacing;
}
}
} else {
if (orientation === 'horizontal') {
currentX += width + spacing;
} else {
currentY -= spacing;
}
const newPdfBytes = await newDoc.save();
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`${originalName}_combined.pdf`
);
showAlert('Success', 'Pages combined successfully!', 'success', function () {
resetState();
});
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while combining pages.');
} finally {
hideLoader();
}
}
const newPdfBytes = await newDoc.save();
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`${originalName}_combined.pdf`
);
showAlert(
'Success',
'Pages combined successfully!',
'success',
function () {
resetState();
}
);
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while combining pages.');
} finally {
hideLoader();
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
if (files && files.length > 0) {
const file = files[0];
if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const addSeparatorCheckbox = document.getElementById('add-separator');
const separatorOptions = document.getElementById('separator-options');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const addSeparatorCheckbox = document.getElementById('add-separator');
const separatorOptions = document.getElementById('separator-options');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (addSeparatorCheckbox && separatorOptions) {
addSeparatorCheckbox.addEventListener('change', function () {
if ((addSeparatorCheckbox as HTMLInputElement).checked) {
separatorOptions.classList.remove('hidden');
} else {
separatorOptions.classList.add('hidden');
}
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
});
}
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
if (addSeparatorCheckbox && separatorOptions) {
addSeparatorCheckbox.addEventListener('change', function () {
if ((addSeparatorCheckbox as HTMLInputElement).checked) {
separatorOptions.classList.remove('hidden');
} else {
separatorOptions.classList.add('hidden');
}
});
}
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', combineToSinglePage);
}
if (processBtn) {
processBtn.addEventListener('click', combineToSinglePage);
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,235 +1,269 @@
import { DividePagesState } from '@/types';
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes, parsePageRanges } from '../utils/helpers.js';
import {
downloadFile,
formatBytes,
parsePageRanges,
} from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
const pageState: DividePagesState = {
file: null,
pdfDoc: null,
totalPages: 0,
file: null,
pdfDoc: null,
totalPages: 0,
};
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
pageState.totalPages = 0;
pageState.file = null;
pageState.pdfDoc = null;
pageState.totalPages = 0;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const splitTypeSelect = document.getElementById('split-type') as HTMLSelectElement;
if (splitTypeSelect) splitTypeSelect.value = 'vertical';
const splitTypeSelect = document.getElementById(
'split-type'
) as HTMLSelectElement;
if (splitTypeSelect) splitTypeSelect.value = 'vertical';
const pageRangeInput = document.getElementById('page-range') as HTMLInputElement;
if (pageRangeInput) pageRangeInput.value = '';
const pageRangeInput = document.getElementById(
'page-range'
) as HTMLInputElement;
if (pageRangeInput) pageRangeInput.value = '';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
try {
showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false
});
pageState.totalPages = pageState.pdfDoc.getPageCount();
hideLoader();
try {
const result = await loadPdfWithPasswordPrompt(pageState.file);
if (!result) {
resetState();
return;
}
result.pdf.destroy();
pageState.file = result.file;
showLoader('Loading PDF...');
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageState.totalPages} pages`;
pageState.pdfDoc = await PDFLibDocument.load(result.bytes);
pageState.totalPages = pageState.pdfDoc.getPageCount();
hideLoader();
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageState.totalPages} pages`;
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
async function dividePages() {
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF first.');
return;
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF first.');
return;
}
const pageRangeInput = document.getElementById(
'page-range'
) as HTMLInputElement;
const pageRangeValue = pageRangeInput?.value.trim().toLowerCase() || '';
const splitTypeSelect = document.getElementById(
'split-type'
) as HTMLSelectElement;
const splitType = splitTypeSelect.value;
let pagesToDivide: Set<number>;
if (pageRangeValue === '' || pageRangeValue === 'all') {
pagesToDivide = new Set(
Array.from({ length: pageState.totalPages }, (_, i) => i + 1)
);
} else {
const parsedIndices = parsePageRanges(pageRangeValue, pageState.totalPages);
pagesToDivide = new Set(parsedIndices.map((i) => i + 1));
if (pagesToDivide.size === 0) {
showAlert(
'Invalid Range',
'Please enter a valid page range (e.g., 1-5, 8, 11-13).'
);
return;
}
}
const pageRangeInput = document.getElementById('page-range') as HTMLInputElement;
const pageRangeValue = pageRangeInput?.value.trim().toLowerCase() || '';
const splitTypeSelect = document.getElementById('split-type') as HTMLSelectElement;
const splitType = splitTypeSelect.value;
showLoader('Splitting PDF pages...');
let pagesToDivide: Set<number>;
try {
const newPdfDoc = await PDFLibDocument.create();
const pages = pageState.pdfDoc.getPages();
if (pageRangeValue === '' || pageRangeValue === 'all') {
pagesToDivide = new Set(Array.from({ length: pageState.totalPages }, (_, i) => i + 1));
} else {
const parsedIndices = parsePageRanges(pageRangeValue, pageState.totalPages);
pagesToDivide = new Set(parsedIndices.map(i => i + 1));
for (let i = 0; i < pages.length; i++) {
const pageNum = i + 1;
const originalPage = pages[i];
const { width, height } = originalPage.getSize();
if (pagesToDivide.size === 0) {
showAlert('Invalid Range', 'Please enter a valid page range (e.g., 1-5, 8, 11-13).');
return;
}
}
showLoader(`Processing page ${pageNum} of ${pages.length}...`);
showLoader('Splitting PDF pages...');
if (pagesToDivide.has(pageNum)) {
const [page1] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
const [page2] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
try {
const newPdfDoc = await PDFLibDocument.create();
const pages = pageState.pdfDoc.getPages();
for (let i = 0; i < pages.length; i++) {
const pageNum = i + 1;
const originalPage = pages[i];
const { width, height } = originalPage.getSize();
showLoader(`Processing page ${pageNum} of ${pages.length}...`);
if (pagesToDivide.has(pageNum)) {
const [page1] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
const [page2] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
switch (splitType) {
case 'vertical':
page1.setCropBox(0, 0, width / 2, height);
page2.setCropBox(width / 2, 0, width / 2, height);
break;
case 'horizontal':
page1.setCropBox(0, height / 2, width, height / 2);
page2.setCropBox(0, 0, width, height / 2);
break;
}
newPdfDoc.addPage(page1);
newPdfDoc.addPage(page2);
} else {
const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
newPdfDoc.addPage(copiedPage);
}
switch (splitType) {
case 'vertical':
page1.setCropBox(0, 0, width / 2, height);
page2.setCropBox(width / 2, 0, width / 2, height);
break;
case 'horizontal':
page1.setCropBox(0, height / 2, width, height / 2);
page2.setCropBox(0, 0, width, height / 2);
break;
}
const newPdfBytes = await newPdfDoc.save();
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`${originalName}_divided.pdf`
);
showAlert('Success', 'Pages have been divided successfully!', 'success', function () {
resetState();
});
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while dividing the PDF.');
} finally {
hideLoader();
newPdfDoc.addPage(page1);
newPdfDoc.addPage(page2);
} else {
const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
newPdfDoc.addPage(copiedPage);
}
}
const newPdfBytes = await newPdfDoc.save();
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`${originalName}_divided.pdf`
);
showAlert(
'Success',
'Pages have been divided successfully!',
'success',
function () {
resetState();
}
);
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while dividing the PDF.');
} finally {
hideLoader();
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
if (files && files.length > 0) {
const file = files[0];
if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
});
}
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', dividePages);
}
if (processBtn) {
processBtn.addEventListener('click', dividePages);
}
});

View File

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

View File

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

View File

@@ -3,367 +3,453 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument, PDFName, PDFString } from 'pdf-lib';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
const pageState: EditMetadataState = {
file: null,
pdfDoc: null,
file: null,
pdfDoc: null,
};
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
pageState.file = null;
pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
// Clear form fields
const fields = ['meta-title', 'meta-author', 'meta-subject', 'meta-keywords', 'meta-creator', 'meta-producer', 'meta-creation-date', 'meta-mod-date'];
fields.forEach(function (fieldId) {
const field = document.getElementById(fieldId) as HTMLInputElement;
if (field) field.value = '';
});
// Clear form fields
const fields = [
'meta-title',
'meta-author',
'meta-subject',
'meta-keywords',
'meta-creator',
'meta-producer',
'meta-creation-date',
'meta-mod-date',
];
fields.forEach(function (fieldId) {
const field = document.getElementById(fieldId) as HTMLInputElement;
if (field) field.value = '';
});
// Clear custom fields
const customFieldsContainer = document.getElementById('custom-fields-container');
if (customFieldsContainer) customFieldsContainer.innerHTML = '';
// Clear custom fields
const customFieldsContainer = document.getElementById(
'custom-fields-container'
);
if (customFieldsContainer) customFieldsContainer.innerHTML = '';
}
function formatDateForInput(date: Date | undefined): string {
if (!date) return '';
try {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
} catch {
return '';
}
if (!date) return '';
try {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
} catch {
return '';
}
}
function addCustomFieldRow(key: string = '', value: string = '') {
const container = document.getElementById('custom-fields-container');
if (!container) return;
const container = document.getElementById('custom-fields-container');
if (!container) return;
const row = document.createElement('div');
row.className = 'flex flex-col gap-2';
const row = document.createElement('div');
row.className = 'flex flex-col gap-2';
const keyInput = document.createElement('input');
keyInput.type = 'text';
keyInput.placeholder = 'Key (e.g., Department)';
keyInput.value = key;
keyInput.className = 'custom-meta-key w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5';
const keyInput = document.createElement('input');
keyInput.type = 'text';
keyInput.placeholder = 'Key (e.g., Department)';
keyInput.value = key;
keyInput.className =
'custom-meta-key w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5';
const valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.placeholder = 'Value (e.g., Marketing)';
valueInput.value = value;
valueInput.className = 'custom-meta-value w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5';
const valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.placeholder = 'Value (e.g., Marketing)';
valueInput.value = value;
valueInput.className =
'custom-meta-value w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5';
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'text-red-400 hover:text-red-300 p-2 self-center';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-5 h-5"></i>';
removeBtn.onclick = function () {
row.remove();
};
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'text-red-400 hover:text-red-300 p-2 self-center';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-5 h-5"></i>';
removeBtn.onclick = function () {
row.remove();
};
row.append(keyInput, valueInput, removeBtn);
container.appendChild(row);
createIcons({ icons });
row.append(keyInput, valueInput, removeBtn);
container.appendChild(row);
createIcons({ icons });
}
function populateMetadataFields() {
if (!pageState.pdfDoc) return;
if (!pageState.pdfDoc) return;
const titleInput = document.getElementById('meta-title') as HTMLInputElement;
const authorInput = document.getElementById('meta-author') as HTMLInputElement;
const subjectInput = document.getElementById('meta-subject') as HTMLInputElement;
const keywordsInput = document.getElementById('meta-keywords') as HTMLInputElement;
const creatorInput = document.getElementById('meta-creator') as HTMLInputElement;
const producerInput = document.getElementById('meta-producer') as HTMLInputElement;
const creationDateInput = document.getElementById('meta-creation-date') as HTMLInputElement;
const modDateInput = document.getElementById('meta-mod-date') as HTMLInputElement;
const titleInput = document.getElementById('meta-title') as HTMLInputElement;
const authorInput = document.getElementById(
'meta-author'
) as HTMLInputElement;
const subjectInput = document.getElementById(
'meta-subject'
) as HTMLInputElement;
const keywordsInput = document.getElementById(
'meta-keywords'
) as HTMLInputElement;
const creatorInput = document.getElementById(
'meta-creator'
) as HTMLInputElement;
const producerInput = document.getElementById(
'meta-producer'
) as HTMLInputElement;
const creationDateInput = document.getElementById(
'meta-creation-date'
) as HTMLInputElement;
const modDateInput = document.getElementById(
'meta-mod-date'
) as HTMLInputElement;
if (titleInput) titleInput.value = pageState.pdfDoc.getTitle() || '';
if (authorInput) authorInput.value = pageState.pdfDoc.getAuthor() || '';
if (subjectInput) subjectInput.value = pageState.pdfDoc.getSubject() || '';
if (keywordsInput) keywordsInput.value = pageState.pdfDoc.getKeywords() || '';
if (creatorInput) creatorInput.value = pageState.pdfDoc.getCreator() || '';
if (producerInput) producerInput.value = pageState.pdfDoc.getProducer() || '';
if (creationDateInput) creationDateInput.value = formatDateForInput(pageState.pdfDoc.getCreationDate());
if (modDateInput) modDateInput.value = formatDateForInput(pageState.pdfDoc.getModificationDate());
if (titleInput) titleInput.value = pageState.pdfDoc.getTitle() || '';
if (authorInput) authorInput.value = pageState.pdfDoc.getAuthor() || '';
if (subjectInput) subjectInput.value = pageState.pdfDoc.getSubject() || '';
if (keywordsInput) keywordsInput.value = pageState.pdfDoc.getKeywords() || '';
if (creatorInput) creatorInput.value = pageState.pdfDoc.getCreator() || '';
if (producerInput) producerInput.value = pageState.pdfDoc.getProducer() || '';
if (creationDateInput)
creationDateInput.value = formatDateForInput(
pageState.pdfDoc.getCreationDate()
);
if (modDateInput)
modDateInput.value = formatDateForInput(
pageState.pdfDoc.getModificationDate()
);
// Load custom fields
const customFieldsContainer = document.getElementById('custom-fields-container');
if (customFieldsContainer) customFieldsContainer.innerHTML = '';
// Load custom fields
const customFieldsContainer = document.getElementById(
'custom-fields-container'
);
if (customFieldsContainer) customFieldsContainer.innerHTML = '';
try {
// @ts-expect-error getInfoDict is private but accessible at runtime
const infoDict = pageState.pdfDoc.getInfoDict();
const standardKeys = new Set([
'Title', 'Author', 'Subject', 'Keywords', 'Creator',
'Producer', 'CreationDate', 'ModDate'
]);
try {
// @ts-expect-error getInfoDict is private but accessible at runtime
const infoDict = pageState.pdfDoc.getInfoDict();
const standardKeys = new Set([
'Title',
'Author',
'Subject',
'Keywords',
'Creator',
'Producer',
'CreationDate',
'ModDate',
]);
const allKeys = infoDict
.keys()
.map(function (key: { asString: () => string }) {
return key.asString().substring(1);
});
const allKeys = infoDict.keys().map(function (key: {
asString: () => string;
}) {
return key.asString().substring(1);
});
allKeys.forEach(function (key: string) {
if (!standardKeys.has(key)) {
const rawValue = infoDict.lookup(key);
let displayValue = '';
allKeys.forEach(function (key: string) {
if (!standardKeys.has(key)) {
const rawValue = infoDict.lookup(key);
let displayValue = '';
if (rawValue && typeof rawValue.decodeText === 'function') {
displayValue = rawValue.decodeText();
} else if (rawValue && typeof rawValue.asString === 'function') {
displayValue = rawValue.asString();
} else if (rawValue) {
displayValue = String(rawValue);
}
if (rawValue && typeof rawValue.decodeText === 'function') {
displayValue = rawValue.decodeText();
} else if (rawValue && typeof rawValue.asString === 'function') {
displayValue = rawValue.asString();
} else if (rawValue) {
displayValue = String(rawValue);
}
addCustomFieldRow(key, displayValue);
}
});
} catch (e) {
console.warn('Could not read custom metadata fields:', e);
}
addCustomFieldRow(key, displayValue);
}
});
} catch (e) {
console.warn('Could not read custom metadata fields:', e);
}
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
try {
showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false
});
hideLoader();
try {
const result = await loadPdfWithPasswordPrompt(pageState.file);
if (!result) {
resetState();
return;
}
showLoader('Loading PDF...');
result.pdf.destroy();
pageState.file = result.file;
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
throwOnInvalidObject: false,
});
hideLoader();
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
populateMetadataFields();
populateMetadataFields();
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
async function saveMetadata() {
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF first.');
return;
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF first.');
return;
}
showLoader('Updating metadata...');
try {
const titleInput = document.getElementById(
'meta-title'
) as HTMLInputElement;
const authorInput = document.getElementById(
'meta-author'
) as HTMLInputElement;
const subjectInput = document.getElementById(
'meta-subject'
) as HTMLInputElement;
const keywordsInput = document.getElementById(
'meta-keywords'
) as HTMLInputElement;
const creatorInput = document.getElementById(
'meta-creator'
) as HTMLInputElement;
const producerInput = document.getElementById(
'meta-producer'
) as HTMLInputElement;
const creationDateInput = document.getElementById(
'meta-creation-date'
) as HTMLInputElement;
const modDateInput = document.getElementById(
'meta-mod-date'
) as HTMLInputElement;
pageState.pdfDoc.setTitle(titleInput.value);
pageState.pdfDoc.setAuthor(authorInput.value);
pageState.pdfDoc.setSubject(subjectInput.value);
pageState.pdfDoc.setCreator(creatorInput.value);
pageState.pdfDoc.setProducer(producerInput.value);
const keywords = keywordsInput.value;
pageState.pdfDoc.setKeywords(
keywords
.split(',')
.map(function (k) {
return k.trim();
})
.filter(Boolean)
);
// Handle creation date
if (creationDateInput.value) {
pageState.pdfDoc.setCreationDate(new Date(creationDateInput.value));
}
showLoader('Updating metadata...');
try {
const titleInput = document.getElementById('meta-title') as HTMLInputElement;
const authorInput = document.getElementById('meta-author') as HTMLInputElement;
const subjectInput = document.getElementById('meta-subject') as HTMLInputElement;
const keywordsInput = document.getElementById('meta-keywords') as HTMLInputElement;
const creatorInput = document.getElementById('meta-creator') as HTMLInputElement;
const producerInput = document.getElementById('meta-producer') as HTMLInputElement;
const creationDateInput = document.getElementById('meta-creation-date') as HTMLInputElement;
const modDateInput = document.getElementById('meta-mod-date') as HTMLInputElement;
pageState.pdfDoc.setTitle(titleInput.value);
pageState.pdfDoc.setAuthor(authorInput.value);
pageState.pdfDoc.setSubject(subjectInput.value);
pageState.pdfDoc.setCreator(creatorInput.value);
pageState.pdfDoc.setProducer(producerInput.value);
const keywords = keywordsInput.value;
pageState.pdfDoc.setKeywords(
keywords
.split(',')
.map(function (k) { return k.trim(); })
.filter(Boolean)
);
// Handle creation date
if (creationDateInput.value) {
pageState.pdfDoc.setCreationDate(new Date(creationDateInput.value));
}
// Handle modification date
if (modDateInput.value) {
pageState.pdfDoc.setModificationDate(new Date(modDateInput.value));
} else {
pageState.pdfDoc.setModificationDate(new Date());
}
// Handle custom fields
// @ts-expect-error getInfoDict is private but accessible at runtime
const infoDict = pageState.pdfDoc.getInfoDict();
const standardKeys = new Set([
'Title', 'Author', 'Subject', 'Keywords', 'Creator',
'Producer', 'CreationDate', 'ModDate'
]);
// Remove existing custom keys
const allKeys = infoDict
.keys()
.map(function (key: { asString: () => string }) {
return key.asString().substring(1);
});
allKeys.forEach(function (key: string) {
if (!standardKeys.has(key)) {
infoDict.delete(PDFName.of(key));
}
});
// Add new custom fields
const customKeys = document.querySelectorAll('.custom-meta-key');
const customValues = document.querySelectorAll('.custom-meta-value');
customKeys.forEach(function (keyInput, index) {
const key = (keyInput as HTMLInputElement).value.trim();
const value = (customValues[index] as HTMLInputElement).value.trim();
if (key && value) {
infoDict.set(PDFName.of(key), PDFString.of(value));
}
});
const newPdfBytes = await pageState.pdfDoc.save();
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`${originalName}_metadata-edited.pdf`
);
showAlert('Success', 'Metadata updated successfully!', 'success', function () {
resetState();
});
} catch (e) {
console.error(e);
showAlert('Error', 'Could not update metadata. Please check that date formats are correct.');
} finally {
hideLoader();
// Handle modification date
if (modDateInput.value) {
pageState.pdfDoc.setModificationDate(new Date(modDateInput.value));
} else {
pageState.pdfDoc.setModificationDate(new Date());
}
// Handle custom fields
// @ts-expect-error getInfoDict is private but accessible at runtime
const infoDict = pageState.pdfDoc.getInfoDict();
const standardKeys = new Set([
'Title',
'Author',
'Subject',
'Keywords',
'Creator',
'Producer',
'CreationDate',
'ModDate',
]);
// Remove existing custom keys
const allKeys = infoDict.keys().map(function (key: {
asString: () => string;
}) {
return key.asString().substring(1);
});
allKeys.forEach(function (key: string) {
if (!standardKeys.has(key)) {
infoDict.delete(PDFName.of(key));
}
});
// Add new custom fields
const customKeys = document.querySelectorAll('.custom-meta-key');
const customValues = document.querySelectorAll('.custom-meta-value');
customKeys.forEach(function (keyInput, index) {
const key = (keyInput as HTMLInputElement).value.trim();
const value = (customValues[index] as HTMLInputElement).value.trim();
if (key && value) {
infoDict.set(PDFName.of(key), PDFString.of(value));
}
});
const newPdfBytes = await pageState.pdfDoc.save();
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`${originalName}_metadata-edited.pdf`
);
showAlert(
'Success',
'Metadata updated successfully!',
'success',
function () {
resetState();
}
);
} catch (e) {
console.error(e);
showAlert(
'Error',
'Could not update metadata. Please check that date formats are correct.'
);
} finally {
hideLoader();
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
if (files && files.length > 0) {
const file = files[0];
if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const addCustomFieldBtn = document.getElementById('add-custom-field');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const addCustomFieldBtn = document.getElementById('add-custom-field');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (addCustomFieldBtn) {
addCustomFieldBtn.addEventListener('click', function () {
addCustomFieldRow();
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
});
}
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
if (addCustomFieldBtn) {
addCustomFieldBtn.addEventListener('click', function () {
addCustomFieldRow();
});
}
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', saveMetadata);
}
if (processBtn) {
processBtn.addEventListener('click', saveMetadata);
}
});

View File

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

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

View File

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

View File

@@ -1,209 +1,235 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { readFileAsArrayBuffer, formatBytes, downloadFile, parsePageRanges } from '../utils/helpers.js';
import {
formatBytes,
downloadFile,
parsePageRanges,
} from '../utils/helpers.js';
import { PDFDocument } from 'pdf-lib';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import JSZip from 'jszip';
interface ExtractState {
file: File | null;
pdfDoc: any;
totalPages: number;
file: File | null;
pdfDoc: any;
totalPages: number;
}
const extractState: ExtractState = {
file: null,
pdfDoc: null,
totalPages: 0,
file: null,
pdfDoc: null,
totalPages: 0,
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
document.addEventListener('DOMContentLoaded', initializePage);
} else {
initializePage();
initializePage();
}
function initializePage() {
createIcons({ icons });
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
}
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
}
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) {
handleFile(droppedFiles[0]);
}
});
// Clear value on click to allow re-selecting the same file
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', extractPages);
}
document.getElementById('back-to-tools')?.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) {
handleFile(droppedFiles[0]);
}
});
// Clear value on click to allow re-selecting the same file
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', extractPages);
}
document.getElementById('back-to-tools')?.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
handleFile(input.files[0]);
}
const input = e.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
handleFile(input.files[0]);
}
}
async function handleFile(file: File) {
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
showAlert('Invalid File', 'Please select a PDF file.');
return;
}
if (
file.type !== 'application/pdf' &&
!file.name.toLowerCase().endsWith('.pdf')
) {
showAlert('Invalid File', 'Please select a PDF file.');
return;
}
extractState.file = file;
try {
const result = await loadPdfWithPasswordPrompt(file);
if (!result) {
extractState.file = null;
return;
}
showLoader('Loading PDF...');
extractState.file = file;
extractState.file = result.file;
result.pdf.destroy();
extractState.pdfDoc = await PDFDocument.load(result.bytes, {
throwOnInvalidObject: false,
});
extractState.totalPages = extractState.pdfDoc.getPageCount();
try {
const arrayBuffer = await readFileAsArrayBuffer(file);
extractState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false,
});
extractState.totalPages = extractState.pdfDoc.getPageCount();
updateFileDisplay();
showOptions();
hideLoader();
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
}
updateFileDisplay();
showOptions();
hideLoader();
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
}
}
function updateFileDisplay() {
const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea || !extractState.file) return;
const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea || !extractState.file) return;
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = extractState.file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = extractState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(extractState.file.size)}${extractState.totalPages} pages`;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(extractState.file.size)}${extractState.totalPages} pages`;
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => resetState();
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => resetState();
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
}
function showOptions() {
const extractOptions = document.getElementById('extract-options');
const totalPagesSpan = document.getElementById('total-pages');
const extractOptions = document.getElementById('extract-options');
const totalPagesSpan = document.getElementById('total-pages');
if (extractOptions) {
extractOptions.classList.remove('hidden');
}
if (totalPagesSpan) {
totalPagesSpan.textContent = extractState.totalPages.toString();
}
if (extractOptions) {
extractOptions.classList.remove('hidden');
}
if (totalPagesSpan) {
totalPagesSpan.textContent = extractState.totalPages.toString();
}
}
async function extractPages() {
const pagesInput = document.getElementById('pages-to-extract') as HTMLInputElement;
if (!pagesInput || !pagesInput.value.trim()) {
showAlert('No Pages', 'Please enter page numbers to extract.');
return;
const pagesInput = document.getElementById(
'pages-to-extract'
) as HTMLInputElement;
if (!pagesInput || !pagesInput.value.trim()) {
showAlert('No Pages', 'Please enter page numbers to extract.');
return;
}
const pagesToExtract = parsePageRanges(
pagesInput.value,
extractState.totalPages
).map((i) => i + 1);
if (pagesToExtract.length === 0) {
showAlert('Invalid Pages', 'No valid page numbers found.');
return;
}
showLoader('Extracting pages...');
try {
const zip = new JSZip();
const baseName = extractState.file?.name.replace('.pdf', '') || 'document';
for (const pageNum of pagesToExtract) {
const newPdf = await PDFDocument.create();
const [copiedPage] = await newPdf.copyPages(extractState.pdfDoc, [
pageNum - 1,
]);
newPdf.addPage(copiedPage);
const pdfBytes = await newPdf.save();
zip.file(`${baseName}_page_${pageNum}.pdf`, pdfBytes);
}
const pagesToExtract = parsePageRanges(pagesInput.value, extractState.totalPages).map(i => i + 1);
if (pagesToExtract.length === 0) {
showAlert('Invalid Pages', 'No valid page numbers found.');
return;
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${baseName}_extracted_pages.zip`);
showLoader('Extracting pages...');
try {
const zip = new JSZip();
const baseName = extractState.file?.name.replace('.pdf', '') || 'document';
for (const pageNum of pagesToExtract) {
const newPdf = await PDFDocument.create();
const [copiedPage] = await newPdf.copyPages(extractState.pdfDoc, [pageNum - 1]);
newPdf.addPage(copiedPage);
const pdfBytes = await newPdf.save();
zip.file(`${baseName}_page_${pageNum}.pdf`, pdfBytes);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${baseName}_extracted_pages.zip`);
hideLoader();
showAlert('Success', `Extracted ${pagesToExtract.length} page(s) successfully!`, 'success', () => {
resetState();
});
} catch (error) {
console.error('Error extracting pages:', error);
hideLoader();
showAlert('Error', 'Failed to extract pages.');
}
hideLoader();
showAlert(
'Success',
`Extracted ${pagesToExtract.length} page(s) successfully!`,
'success',
() => {
resetState();
}
);
} catch (error) {
console.error('Error extracting pages:', error);
hideLoader();
showAlert('Error', 'Failed to extract pages.');
}
}
function resetState() {
extractState.file = null;
extractState.pdfDoc = null;
extractState.totalPages = 0;
extractState.file = null;
extractState.pdfDoc = null;
extractState.totalPages = 0;
const extractOptions = document.getElementById('extract-options');
if (extractOptions) {
extractOptions.classList.add('hidden');
}
const extractOptions = document.getElementById('extract-options');
if (extractOptions) {
extractOptions.classList.add('hidden');
}
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) {
fileDisplayArea.innerHTML = '';
}
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) {
fileDisplayArea.innerHTML = '';
}
const pagesInput = document.getElementById('pages-to-extract') as HTMLInputElement;
if (pagesInput) {
pagesInput.value = '';
}
const pagesInput = document.getElementById(
'pages-to-extract'
) as HTMLInputElement;
if (pagesInput) {
pagesInput.value = '';
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
// Self-contained Form Filler logic for standalone page
import { createIcons, icons } from 'lucide';
import { getPDFDocument } from '../utils/helpers.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
let viewerIframe: HTMLIFrameElement | null = null;
let viewerReady = false;
@@ -8,46 +9,52 @@ let currentFile: File | null = null;
// UI helpers
function showLoader(message: string = 'Processing...') {
const loader = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text');
if (loader) loader.classList.remove('hidden');
if (loaderText) loaderText.textContent = message;
const loader = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text');
if (loader) loader.classList.remove('hidden');
if (loaderText) loaderText.textContent = message;
}
function hideLoader() {
const loader = document.getElementById('loader-modal');
if (loader) loader.classList.add('hidden');
const loader = document.getElementById('loader-modal');
if (loader) loader.classList.add('hidden');
}
function showAlert(title: string, message: string, type: string = 'error', callback?: () => void) {
const modal = document.getElementById('alert-modal');
const alertTitle = document.getElementById('alert-title');
const alertMessage = document.getElementById('alert-message');
const okBtn = document.getElementById('alert-ok');
function showAlert(
title: string,
message: string,
type: string = 'error',
callback?: () => void
) {
const modal = document.getElementById('alert-modal');
const alertTitle = document.getElementById('alert-title');
const alertMessage = document.getElementById('alert-message');
const okBtn = document.getElementById('alert-ok');
if (alertTitle) alertTitle.textContent = title;
if (alertMessage) alertMessage.textContent = message;
if (modal) modal.classList.remove('hidden');
if (alertTitle) alertTitle.textContent = title;
if (alertMessage) alertMessage.textContent = message;
if (modal) modal.classList.remove('hidden');
if (okBtn) {
const newOkBtn = okBtn.cloneNode(true) as HTMLElement;
okBtn.replaceWith(newOkBtn);
newOkBtn.addEventListener('click', () => {
modal?.classList.add('hidden');
if (callback) callback();
});
}
if (okBtn) {
const newOkBtn = okBtn.cloneNode(true) as HTMLElement;
okBtn.replaceWith(newOkBtn);
newOkBtn.addEventListener('click', () => {
modal?.classList.add('hidden');
if (callback) callback();
});
}
}
function updateFileDisplay() {
const displayArea = document.getElementById('file-display-area');
if (!displayArea || !currentFile) return;
const displayArea = document.getElementById('file-display-area');
if (!displayArea || !currentFile) return;
const fileSize = currentFile.size < 1024 * 1024
? `${(currentFile.size / 1024).toFixed(1)} KB`
: `${(currentFile.size / 1024 / 1024).toFixed(2)} MB`;
const fileSize =
currentFile.size < 1024 * 1024
? `${(currentFile.size / 1024).toFixed(1)} KB`
: `${(currentFile.size / 1024 / 1024).toFixed(2)} MB`;
displayArea.innerHTML = `
displayArea.innerHTML = `
<div class="bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors">
<div class="flex items-center justify-between">
<div class="flex-1 min-w-0">
@@ -61,196 +68,226 @@ function updateFileDisplay() {
</div>
`;
createIcons({ icons });
createIcons({ icons });
document.getElementById('remove-file')?.addEventListener('click', () => resetState());
document
.getElementById('remove-file')
?.addEventListener('click', () => resetState());
}
function resetState() {
viewerIframe = null;
viewerReady = false;
currentFile = null;
const displayArea = document.getElementById('file-display-area');
if (displayArea) displayArea.innerHTML = '';
document.getElementById('form-filler-options')?.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
viewerIframe = null;
viewerReady = false;
currentFile = null;
const displayArea = document.getElementById('file-display-area');
if (displayArea) displayArea.innerHTML = '';
document.getElementById('form-filler-options')?.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
// Clear viewer
const viewerContainer = document.getElementById('pdf-viewer-container');
if (viewerContainer) {
viewerContainer.innerHTML = '';
viewerContainer.style.height = '';
viewerContainer.style.aspectRatio = '';
}
// Clear viewer
const viewerContainer = document.getElementById('pdf-viewer-container');
if (viewerContainer) {
viewerContainer.innerHTML = '';
viewerContainer.style.height = '';
viewerContainer.style.aspectRatio = '';
}
const toolUploader = document.getElementById('tool-uploader');
const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
if (toolUploader && !isFullWidth) {
toolUploader.classList.remove('max-w-6xl');
toolUploader.classList.add('max-w-2xl');
}
const toolUploader = document.getElementById('tool-uploader');
const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
if (toolUploader && !isFullWidth) {
toolUploader.classList.remove('max-w-6xl');
toolUploader.classList.add('max-w-2xl');
}
}
// File handling
async function handleFileUpload(file: File) {
if (!file || file.type !== 'application/pdf') {
showAlert('Error', 'Please upload a valid PDF file.');
return;
}
if (!file || file.type !== 'application/pdf') {
showAlert('Error', 'Please upload a valid PDF file.');
return;
}
currentFile = file;
try {
const result = await loadPdfWithPasswordPrompt(file);
if (!result) return;
result.pdf.destroy();
currentFile = result.file;
updateFileDisplay();
await setupFormViewer();
} catch (error) {
console.error(error);
showAlert('Error', 'Failed to load PDF file.');
hideLoader();
}
}
async function adjustViewerHeight(file: File) {
const viewerContainer = document.getElementById('pdf-viewer-container');
if (!viewerContainer) return;
const viewerContainer = document.getElementById('pdf-viewer-container');
if (!viewerContainer) return;
try {
const arrayBuffer = await file.arrayBuffer();
const loadingTask = getPDFDocument({ data: arrayBuffer });
const pdf = await loadingTask.promise;
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 1 });
try {
const arrayBuffer = await file.arrayBuffer();
const loadingTask = getPDFDocument({ data: arrayBuffer });
const pdf = await loadingTask.promise;
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 1 });
// Add ~50px for toolbar height
const aspectRatio = viewport.width / (viewport.height + 50);
// Add ~50px for toolbar height
const aspectRatio = viewport.width / (viewport.height + 50);
viewerContainer.style.height = 'auto';
viewerContainer.style.aspectRatio = `${aspectRatio}`;
} catch (e) {
console.error('Error adjusting viewer height:', e);
viewerContainer.style.height = '80vh';
}
viewerContainer.style.height = 'auto';
viewerContainer.style.aspectRatio = `${aspectRatio}`;
} catch (e) {
console.error('Error adjusting viewer height:', e);
viewerContainer.style.height = '80vh';
}
}
async function setupFormViewer() {
if (!currentFile) return;
if (!currentFile) return;
showLoader('Loading PDF form...');
const pdfViewerContainer = document.getElementById('pdf-viewer-container');
showLoader('Loading PDF form...');
const pdfViewerContainer = document.getElementById('pdf-viewer-container');
if (!pdfViewerContainer) {
console.error('PDF viewer container not found');
hideLoader();
return;
}
if (!pdfViewerContainer) {
console.error('PDF viewer container not found');
hideLoader();
return;
}
const toolUploader = document.getElementById('tool-uploader');
// Default to true if not set
const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
if (toolUploader && !isFullWidth) {
toolUploader.classList.remove('max-w-2xl');
toolUploader.classList.add('max-w-6xl');
}
const toolUploader = document.getElementById('tool-uploader');
// Default to true if not set
const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
if (toolUploader && !isFullWidth) {
toolUploader.classList.remove('max-w-2xl');
toolUploader.classList.add('max-w-6xl');
}
try {
// Apply dynamic height
await adjustViewerHeight(currentFile);
try {
// Apply dynamic height
await adjustViewerHeight(currentFile);
pdfViewerContainer.innerHTML = '';
pdfViewerContainer.innerHTML = '';
const arrayBuffer = await currentFile.arrayBuffer();
const blob = new Blob([arrayBuffer], { type: 'application/pdf' });
const blobUrl = URL.createObjectURL(blob);
const arrayBuffer = await currentFile.arrayBuffer();
const blob = new Blob([arrayBuffer], { type: 'application/pdf' });
const blobUrl = URL.createObjectURL(blob);
viewerIframe = document.createElement('iframe');
viewerIframe.src = `${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html?file=${encodeURIComponent(blobUrl)}`;
viewerIframe.style.width = '100%';
viewerIframe.style.height = '100%';
viewerIframe.style.border = 'none';
viewerIframe = document.createElement('iframe');
viewerIframe.src = `${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html?file=${encodeURIComponent(blobUrl)}`;
viewerIframe.style.width = '100%';
viewerIframe.style.height = '100%';
viewerIframe.style.border = 'none';
viewerIframe.onload = () => {
viewerReady = true;
hideLoader();
};
viewerIframe.onload = () => {
viewerReady = true;
hideLoader();
};
pdfViewerContainer.appendChild(viewerIframe);
pdfViewerContainer.appendChild(viewerIframe);
const formFillerOptions = document.getElementById('form-filler-options');
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
} catch (e) {
console.error('Critical error setting up form filler:', e);
showAlert('Error', 'Failed to load PDF form viewer.');
hideLoader();
}
const formFillerOptions = document.getElementById('form-filler-options');
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
} catch (e) {
console.error('Critical error setting up form filler:', e);
showAlert('Error', 'Failed to load PDF form viewer.');
hideLoader();
}
}
async function processAndDownloadForm() {
if (!viewerIframe || !viewerReady) {
showAlert('Viewer not ready', 'Please wait for the form to finish loading.');
return;
if (!viewerIframe || !viewerReady) {
showAlert(
'Viewer not ready',
'Please wait for the form to finish loading.'
);
return;
}
try {
const viewerWindow = viewerIframe.contentWindow;
if (!viewerWindow) {
console.error('Cannot access iframe window');
showAlert(
'Download',
'Please use the Download button in the PDF viewer toolbar above.'
);
return;
}
try {
const viewerWindow = viewerIframe.contentWindow;
if (!viewerWindow) {
console.error('Cannot access iframe window');
showAlert('Download', 'Please use the Download button in the PDF viewer toolbar above.');
return;
}
const viewerDoc = viewerWindow.document;
if (!viewerDoc) {
console.error('Cannot access iframe document');
showAlert('Download', 'Please use the Download button in the PDF viewer toolbar above.');
return;
}
const downloadBtn = viewerDoc.getElementById('downloadButton') as HTMLButtonElement | null;
if (downloadBtn) {
console.log('Clicking download button...');
downloadBtn.click();
} else {
console.error('Download button not found in viewer');
const secondaryDownload = viewerDoc.getElementById('secondaryDownload') as HTMLButtonElement | null;
if (secondaryDownload) {
console.log('Clicking secondary download button...');
secondaryDownload.click();
} else {
showAlert('Download', 'Please use the Download button in the PDF viewer toolbar above.');
}
}
} catch (e) {
console.error('Failed to trigger download:', e);
showAlert('Download', 'Cannot access viewer controls. Please use the Download button in the PDF viewer toolbar above.');
const viewerDoc = viewerWindow.document;
if (!viewerDoc) {
console.error('Cannot access iframe document');
showAlert(
'Download',
'Please use the Download button in the PDF viewer toolbar above.'
);
return;
}
const downloadBtn = viewerDoc.getElementById(
'downloadButton'
) as HTMLButtonElement | null;
if (downloadBtn) {
console.log('Clicking download button...');
downloadBtn.click();
} else {
console.error('Download button not found in viewer');
const secondaryDownload = viewerDoc.getElementById(
'secondaryDownload'
) as HTMLButtonElement | null;
if (secondaryDownload) {
console.log('Clicking secondary download button...');
secondaryDownload.click();
} else {
showAlert(
'Download',
'Please use the Download button in the PDF viewer toolbar above.'
);
}
}
} catch (e) {
console.error('Failed to trigger download:', e);
showAlert(
'Download',
'Cannot access viewer controls. Please use the Download button in the PDF viewer toolbar above.'
);
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
fileInput?.addEventListener('change', (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) handleFileUpload(file);
});
fileInput?.addEventListener('change', (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) handleFileUpload(file);
});
dropZone?.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-indigo-500');
});
dropZone?.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-indigo-500');
});
dropZone?.addEventListener('dragleave', () => {
dropZone.classList.remove('border-indigo-500');
});
dropZone?.addEventListener('dragleave', () => {
dropZone.classList.remove('border-indigo-500');
});
dropZone?.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-indigo-500');
const file = e.dataTransfer?.files[0];
if (file) handleFileUpload(file);
});
dropZone?.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-indigo-500');
const file = e.dataTransfer?.files[0];
if (file) handleFileUpload(file);
});
processBtn?.addEventListener('click', processAndDownloadForm);
processBtn?.addEventListener('click', processAndDownloadForm);
backBtn?.addEventListener('click', () => {
window.location.href = '../../index.html';
});
backBtn?.addEventListener('click', () => {
window.location.href = '../../index.html';
});
});

View File

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

View File

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

View File

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

View File

@@ -2,265 +2,304 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
interface NUpState {
file: File | null;
pdfDoc: PDFLibDocument | null;
file: File | null;
pdfDoc: PDFLibDocument | null;
}
const pageState: NUpState = {
file: null,
pdfDoc: null,
file: null,
pdfDoc: null,
};
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
pageState.file = null;
pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
try {
showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false
});
hideLoader();
try {
const result = await loadPdfWithPasswordPrompt(pageState.file);
if (!result) {
resetState();
return;
}
showLoader('Loading PDF...');
result.pdf.destroy();
pageState.file = result.file;
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
throwOnInvalidObject: false,
});
hideLoader();
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
async function nUpTool() {
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF first.');
return;
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF first.');
return;
}
const n = parseInt(
(document.getElementById('pages-per-sheet') as HTMLSelectElement).value
);
const pageSizeKey = (
document.getElementById('output-page-size') as HTMLSelectElement
).value as keyof typeof PageSizes;
let orientation = (
document.getElementById('output-orientation') as HTMLSelectElement
).value;
const useMargins = (
document.getElementById('add-margins') as HTMLInputElement
).checked;
const addBorder = (document.getElementById('add-border') as HTMLInputElement)
.checked;
const borderColor = hexToRgb(
(document.getElementById('border-color') as HTMLInputElement).value
);
showLoader('Creating N-Up PDF...');
try {
const sourceDoc = pageState.pdfDoc;
const newDoc = await PDFLibDocument.create();
const sourcePages = sourceDoc.getPages();
const gridDims: Record<number, [number, number]> = {
2: [2, 1],
4: [2, 2],
9: [3, 3],
16: [4, 4],
};
const dims = gridDims[n];
let [pageWidth, pageHeight] = PageSizes[pageSizeKey];
if (orientation === 'auto') {
const firstPage = sourcePages[0];
const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight();
orientation =
isSourceLandscape && dims[0] > dims[1] ? 'landscape' : 'portrait';
}
const n = parseInt((document.getElementById('pages-per-sheet') as HTMLSelectElement).value);
const pageSizeKey = (document.getElementById('output-page-size') as HTMLSelectElement).value as keyof typeof PageSizes;
let orientation = (document.getElementById('output-orientation') as HTMLSelectElement).value;
const useMargins = (document.getElementById('add-margins') as HTMLInputElement).checked;
const addBorder = (document.getElementById('add-border') as HTMLInputElement).checked;
const borderColor = hexToRgb((document.getElementById('border-color') as HTMLInputElement).value);
if (orientation === 'landscape' && pageWidth < pageHeight) {
[pageWidth, pageHeight] = [pageHeight, pageWidth];
}
showLoader('Creating N-Up PDF...');
const margin = useMargins ? 36 : 0;
const gutter = useMargins ? 10 : 0;
try {
const sourceDoc = pageState.pdfDoc;
const newDoc = await PDFLibDocument.create();
const sourcePages = sourceDoc.getPages();
const usableWidth = pageWidth - margin * 2;
const usableHeight = pageHeight - margin * 2;
const gridDims: Record<number, [number, number]> = { 2: [2, 1], 4: [2, 2], 9: [3, 3], 16: [4, 4] };
const dims = gridDims[n];
for (let i = 0; i < sourcePages.length; i += n) {
showLoader(`Processing sheet ${Math.floor(i / n) + 1}...`);
const chunk = sourcePages.slice(i, i + n);
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
let [pageWidth, pageHeight] = PageSizes[pageSizeKey];
const cellWidth = (usableWidth - gutter * (dims[0] - 1)) / dims[0];
const cellHeight = (usableHeight - gutter * (dims[1] - 1)) / dims[1];
if (orientation === 'auto') {
const firstPage = sourcePages[0];
const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight();
orientation = isSourceLandscape && dims[0] > dims[1] ? 'landscape' : 'portrait';
}
for (let j = 0; j < chunk.length; j++) {
const sourcePage = chunk[j];
const embeddedPage = await newDoc.embedPage(sourcePage);
if (orientation === 'landscape' && pageWidth < pageHeight) {
[pageWidth, pageHeight] = [pageHeight, pageWidth];
}
const margin = useMargins ? 36 : 0;
const gutter = useMargins ? 10 : 0;
const usableWidth = pageWidth - margin * 2;
const usableHeight = pageHeight - margin * 2;
for (let i = 0; i < sourcePages.length; i += n) {
showLoader(`Processing sheet ${Math.floor(i / n) + 1}...`);
const chunk = sourcePages.slice(i, i + n);
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
const cellWidth = (usableWidth - gutter * (dims[0] - 1)) / dims[0];
const cellHeight = (usableHeight - gutter * (dims[1] - 1)) / dims[1];
for (let j = 0; j < chunk.length; j++) {
const sourcePage = chunk[j];
const embeddedPage = await newDoc.embedPage(sourcePage);
const scale = Math.min(
cellWidth / embeddedPage.width,
cellHeight / embeddedPage.height
);
const scaledWidth = embeddedPage.width * scale;
const scaledHeight = embeddedPage.height * scale;
const row = Math.floor(j / dims[0]);
const col = j % dims[0];
const cellX = margin + col * (cellWidth + gutter);
const cellY = pageHeight - margin - (row + 1) * cellHeight - row * gutter;
const x = cellX + (cellWidth - scaledWidth) / 2;
const y = cellY + (cellHeight - scaledHeight) / 2;
outputPage.drawPage(embeddedPage, {
x,
y,
width: scaledWidth,
height: scaledHeight,
});
if (addBorder) {
outputPage.drawRectangle({
x,
y,
width: scaledWidth,
height: scaledHeight,
borderColor: rgb(borderColor.r, borderColor.g, borderColor.b),
borderWidth: 1,
});
}
}
}
const newPdfBytes = await newDoc.save();
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`${originalName}_${n}-up.pdf`
const scale = Math.min(
cellWidth / embeddedPage.width,
cellHeight / embeddedPage.height
);
const scaledWidth = embeddedPage.width * scale;
const scaledHeight = embeddedPage.height * scale;
showAlert('Success', 'N-Up PDF created successfully!', 'success', function () {
resetState();
const row = Math.floor(j / dims[0]);
const col = j % dims[0];
const cellX = margin + col * (cellWidth + gutter);
const cellY =
pageHeight - margin - (row + 1) * cellHeight - row * gutter;
const x = cellX + (cellWidth - scaledWidth) / 2;
const y = cellY + (cellHeight - scaledHeight) / 2;
outputPage.drawPage(embeddedPage, {
x,
y,
width: scaledWidth,
height: scaledHeight,
});
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
} finally {
hideLoader();
if (addBorder) {
outputPage.drawRectangle({
x,
y,
width: scaledWidth,
height: scaledHeight,
borderColor: rgb(borderColor.r, borderColor.g, borderColor.b),
borderWidth: 1,
});
}
}
}
const newPdfBytes = await newDoc.save();
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`${originalName}_${n}-up.pdf`
);
showAlert(
'Success',
'N-Up PDF created successfully!',
'success',
function () {
resetState();
}
);
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
} finally {
hideLoader();
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
if (files && files.length > 0) {
const file = files[0];
if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const addBorderCheckbox = document.getElementById('add-border');
const borderColorWrapper = document.getElementById('border-color-wrapper');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const addBorderCheckbox = document.getElementById('add-border');
const borderColorWrapper = document.getElementById('border-color-wrapper');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (addBorderCheckbox && borderColorWrapper) {
addBorderCheckbox.addEventListener('change', function () {
borderColorWrapper.classList.toggle(
'hidden',
!(addBorderCheckbox as HTMLInputElement).checked
);
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
});
}
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
if (addBorderCheckbox && borderColorWrapper) {
addBorderCheckbox.addEventListener('change', function () {
borderColorWrapper.classList.toggle('hidden', !(addBorderCheckbox as HTMLInputElement).checked);
});
}
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', nUpTool);
}
if (processBtn) {
processBtn.addEventListener('click', nUpTool);
}
});

View File

@@ -1,6 +1,7 @@
import { tesseractLanguages } from '../config/tesseract-languages.js';
import { showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import { icons, createIcons } from 'lucide';
import { OcrState } from '@/types';
import { performOcr } from '../utils/ocr.js';
@@ -231,14 +232,17 @@ async function updateUI() {
}
}
function handleFileSelect(files: FileList | null) {
async function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file;
const result = await loadPdfWithPasswordPrompt(file);
if (!result) return;
result.pdf.destroy();
pageState.file = result.file;
updateUI();
}
}

View File

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

View File

@@ -1,81 +1,86 @@
import { showAlert } from '../ui.js';
import { formatBytes, getStandardPageName, convertPoints } from '../utils/helpers.js';
import {
formatBytes,
getStandardPageName,
convertPoints,
} from '../utils/helpers.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import { PDFDocument } from 'pdf-lib';
import { icons, createIcons } from 'lucide';
import { PageDimensionsState } from '@/types';
const pageState: PageDimensionsState = {
file: null,
pdfDoc: null,
file: null,
pdfDoc: null,
};
let analyzedPagesData: any[] = [];
function calculateAspectRatio(width: number, height: number): string {
const ratio = width / height;
return ratio.toFixed(3);
const ratio = width / height;
return ratio.toFixed(3);
}
function calculateArea(width: number, height: number, unit: string): string {
const areaInPoints = width * height;
let convertedArea = 0;
let unitSuffix = '';
const areaInPoints = width * height;
let convertedArea: number;
let unitSuffix: string;
switch (unit) {
case 'in':
convertedArea = areaInPoints / (72 * 72);
unitSuffix = 'in²';
break;
case 'mm':
convertedArea = areaInPoints / (72 * 72) * (25.4 * 25.4);
unitSuffix = 'mm²';
break;
case 'px':
const pxPerPoint = 96 / 72;
convertedArea = areaInPoints * (pxPerPoint * pxPerPoint);
unitSuffix = 'px²';
break;
default:
convertedArea = areaInPoints;
unitSuffix = 'pt²';
break;
}
switch (unit) {
case 'in':
convertedArea = areaInPoints / (72 * 72);
unitSuffix = 'in²';
break;
case 'mm':
convertedArea = (areaInPoints / (72 * 72)) * (25.4 * 25.4);
unitSuffix = 'mm²';
break;
case 'px':
const pxPerPoint = 96 / 72;
convertedArea = areaInPoints * (pxPerPoint * pxPerPoint);
unitSuffix = 'px²';
break;
default:
convertedArea = areaInPoints;
unitSuffix = 'pt²';
break;
}
return `${convertedArea.toFixed(2)} ${unitSuffix}`;
return `${convertedArea.toFixed(2)} ${unitSuffix}`;
}
function getSummaryStats() {
const totalPages = analyzedPagesData.length;
const totalPages = analyzedPagesData.length;
const uniqueSizes = new Map();
analyzedPagesData.forEach((pageData: any) => {
const key = `${pageData.width.toFixed(2)}x${pageData.height.toFixed(2)}`;
const label = `${pageData.standardSize} (${pageData.orientation})`;
uniqueSizes.set(key, {
count: (uniqueSizes.get(key)?.count || 0) + 1,
label: label,
width: pageData.width,
height: pageData.height
});
const uniqueSizes = new Map();
analyzedPagesData.forEach((pageData: any) => {
const key = `${pageData.width.toFixed(2)}x${pageData.height.toFixed(2)}`;
const label = `${pageData.standardSize} (${pageData.orientation})`;
uniqueSizes.set(key, {
count: (uniqueSizes.get(key)?.count || 0) + 1,
label: label,
width: pageData.width,
height: pageData.height,
});
});
const hasMixedSizes = uniqueSizes.size > 1;
const hasMixedSizes = uniqueSizes.size > 1;
return {
totalPages,
uniqueSizesCount: uniqueSizes.size,
uniqueSizes: Array.from(uniqueSizes.values()),
hasMixedSizes
};
return {
totalPages,
uniqueSizesCount: uniqueSizes.size,
uniqueSizes: Array.from(uniqueSizes.values()),
hasMixedSizes,
};
}
function renderSummary() {
const summaryContainer = document.getElementById('dimensions-summary');
if (!summaryContainer) return;
const summaryContainer = document.getElementById('dimensions-summary');
if (!summaryContainer) return;
const stats = getSummaryStats();
const stats = getSummaryStats();
let summaryHTML = `
let summaryHTML = `
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
<p class="text-sm text-gray-400 mb-1">Total Pages</p>
@@ -94,8 +99,8 @@ function renderSummary() {
</div>
`;
if (stats.hasMixedSizes) {
summaryHTML += `
if (stats.hasMixedSizes) {
summaryHTML += `
<div class="bg-yellow-900/20 border border-yellow-500/30 rounded-lg p-4 mb-4">
<div class="flex items-start gap-3">
<i data-lucide="alert-triangle" class="w-5 h-5 text-yellow-400 mt-0.5 flex-shrink-0"></i>
@@ -103,253 +108,284 @@ function renderSummary() {
<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>
<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>
`).join('')}
`
)
.join('')}
</ul>
</div>
</div>
</div>
`;
}
}
summaryContainer.innerHTML = summaryHTML;
summaryContainer.innerHTML = summaryHTML;
if (stats.hasMixedSizes) {
createIcons({ icons });
}
if (stats.hasMixedSizes) {
createIcons({ icons });
}
}
function renderTable(unit: string) {
const tableBody = document.getElementById('dimensions-table-body');
if (!tableBody) return;
const tableBody = document.getElementById('dimensions-table-body');
if (!tableBody) return;
tableBody.textContent = '';
tableBody.textContent = '';
analyzedPagesData.forEach((pageData) => {
const width = convertPoints(pageData.width, unit);
const height = convertPoints(pageData.height, unit);
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
const area = calculateArea(pageData.width, pageData.height, unit);
analyzedPagesData.forEach((pageData) => {
const width = convertPoints(pageData.width, unit);
const height = convertPoints(pageData.height, unit);
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
const area = calculateArea(pageData.width, pageData.height, unit);
const row = document.createElement('tr');
const row = document.createElement('tr');
const pageNumCell = document.createElement('td');
pageNumCell.className = 'px-4 py-3 text-white';
pageNumCell.textContent = pageData.pageNum;
const pageNumCell = document.createElement('td');
pageNumCell.className = 'px-4 py-3 text-white';
pageNumCell.textContent = pageData.pageNum;
const dimensionsCell = document.createElement('td');
dimensionsCell.className = 'px-4 py-3 text-gray-300';
dimensionsCell.textContent = `${width} x ${height} ${unit}`;
const dimensionsCell = document.createElement('td');
dimensionsCell.className = 'px-4 py-3 text-gray-300';
dimensionsCell.textContent = `${width} x ${height} ${unit}`;
const sizeCell = document.createElement('td');
sizeCell.className = 'px-4 py-3 text-gray-300';
sizeCell.textContent = pageData.standardSize;
const sizeCell = document.createElement('td');
sizeCell.className = 'px-4 py-3 text-gray-300';
sizeCell.textContent = pageData.standardSize;
const orientationCell = document.createElement('td');
orientationCell.className = 'px-4 py-3 text-gray-300';
orientationCell.textContent = pageData.orientation;
const orientationCell = document.createElement('td');
orientationCell.className = 'px-4 py-3 text-gray-300';
orientationCell.textContent = pageData.orientation;
const aspectRatioCell = document.createElement('td');
aspectRatioCell.className = 'px-4 py-3 text-gray-300';
aspectRatioCell.textContent = aspectRatio;
const aspectRatioCell = document.createElement('td');
aspectRatioCell.className = 'px-4 py-3 text-gray-300';
aspectRatioCell.textContent = aspectRatio;
const areaCell = document.createElement('td');
areaCell.className = 'px-4 py-3 text-gray-300';
areaCell.textContent = area;
const areaCell = document.createElement('td');
areaCell.className = 'px-4 py-3 text-gray-300';
areaCell.textContent = area;
const rotationCell = document.createElement('td');
rotationCell.className = 'px-4 py-3 text-gray-300';
rotationCell.textContent = `${pageData.rotation}°`;
const rotationCell = document.createElement('td');
rotationCell.className = 'px-4 py-3 text-gray-300';
rotationCell.textContent = `${pageData.rotation}°`;
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell, aspectRatioCell, areaCell, rotationCell);
tableBody.appendChild(row);
});
row.append(
pageNumCell,
dimensionsCell,
sizeCell,
orientationCell,
aspectRatioCell,
areaCell,
rotationCell
);
tableBody.appendChild(row);
});
}
function exportToCSV() {
const unitsSelect = document.getElementById('units-select') as HTMLSelectElement;
const unit = unitsSelect?.value || 'pt';
const unitsSelect = document.getElementById(
'units-select'
) as HTMLSelectElement;
const unit = unitsSelect?.value || 'pt';
const headers = ['Page #', `Width (${unit})`, `Height (${unit})`, 'Standard Size', 'Orientation', 'Aspect Ratio', `Area (${unit}²)`, 'Rotation'];
const csvRows = [headers.join(',')];
const headers = [
'Page #',
`Width (${unit})`,
`Height (${unit})`,
'Standard Size',
'Orientation',
'Aspect Ratio',
`Area (${unit}²)`,
'Rotation',
];
const csvRows = [headers.join(',')];
analyzedPagesData.forEach((pageData: any) => {
const width = convertPoints(pageData.width, unit);
const height = convertPoints(pageData.height, unit);
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
const area = calculateArea(pageData.width, pageData.height, unit);
analyzedPagesData.forEach((pageData: any) => {
const width = convertPoints(pageData.width, unit);
const height = convertPoints(pageData.height, unit);
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
const area = calculateArea(pageData.width, pageData.height, unit);
const row = [
pageData.pageNum,
width,
height,
pageData.standardSize,
pageData.orientation,
aspectRatio,
area,
`${pageData.rotation}°`
];
csvRows.push(row.join(','));
});
const row = [
pageData.pageNum,
width,
height,
pageData.standardSize,
pageData.orientation,
aspectRatio,
area,
`${pageData.rotation}°`,
];
csvRows.push(row.join(','));
});
const csvContent = csvRows.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'page-dimensions.csv';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
const csvContent = csvRows.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'page-dimensions.csv';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
function analyzeAndDisplayDimensions() {
if (!pageState.pdfDoc) return;
if (!pageState.pdfDoc) return;
analyzedPagesData = [];
const pages = pageState.pdfDoc.getPages();
analyzedPagesData = [];
const pages = pageState.pdfDoc.getPages();
pages.forEach((page: any, index: number) => {
const { width, height } = page.getSize();
const rotation = page.getRotation().angle || 0;
pages.forEach((page: any, index: number) => {
const { width, height } = page.getSize();
const rotation = page.getRotation().angle || 0;
analyzedPagesData.push({
pageNum: index + 1,
width,
height,
orientation: width > height ? 'Landscape' : 'Portrait',
standardSize: getStandardPageName(width, height),
rotation: rotation
});
analyzedPagesData.push({
pageNum: index + 1,
width,
height,
orientation: width > height ? 'Landscape' : 'Portrait',
standardSize: getStandardPageName(width, height),
rotation: rotation,
});
});
const resultsContainer = document.getElementById('dimensions-results');
const unitsSelect = document.getElementById('units-select') as HTMLSelectElement;
const resultsContainer = document.getElementById('dimensions-results');
const unitsSelect = document.getElementById(
'units-select'
) as HTMLSelectElement;
renderSummary();
renderTable(unitsSelect.value);
renderSummary();
renderTable(unitsSelect.value);
if (resultsContainer) resultsContainer.classList.remove('hidden');
if (resultsContainer) resultsContainer.classList.remove('hidden');
unitsSelect.addEventListener('change', (e) => {
renderTable((e.target as HTMLSelectElement).value);
});
unitsSelect.addEventListener('change', (e) => {
renderTable((e.target as HTMLSelectElement).value);
});
const exportButton = document.getElementById('export-csv-btn');
if (exportButton) {
exportButton.addEventListener('click', exportToCSV);
}
const exportButton = document.getElementById('export-csv-btn');
if (exportButton) {
exportButton.addEventListener('click', exportToCSV);
}
createIcons({ icons });
createIcons({ icons });
}
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
analyzedPagesData = [];
pageState.file = null;
pageState.pdfDoc = null;
analyzedPagesData = [];
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const resultsContainer = document.getElementById('dimensions-results');
if (resultsContainer) resultsContainer.classList.add('hidden');
const resultsContainer = document.getElementById('dimensions-results');
if (resultsContainer) resultsContainer.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea) return;
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(pageState.file.size);
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(pageState.file.size);
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
}
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
}
}
async function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
if (files && files.length > 0) {
const file = files[0];
if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
try {
const result = await loadPdfWithPasswordPrompt(file);
if (!result) return;
result.pdf.destroy();
try {
const arrayBuffer = await file.arrayBuffer();
pageState.pdfDoc = await PDFDocument.load(arrayBuffer);
updateUI();
analyzeAndDisplayDimensions();
} catch (e) {
console.error('Error loading PDF:', e);
showAlert('Error', 'Failed to load PDF file.');
}
}
pageState.file = result.file;
pageState.pdfDoc = await PDFDocument.load(result.bytes);
updateUI();
analyzeAndDisplayDimensions();
} catch (e) {
console.error('Error loading PDF:', e);
showAlert('Error', 'Failed to load PDF file.');
}
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files);
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files);
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,393 +1,497 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, parsePageRanges, getPDFDocument, formatBytes } from '../utils/helpers.js';
import {
downloadFile,
parsePageRanges,
formatBytes,
} from '../utils/helpers.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import { PDFDocument, PageSizes } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
import { createIcons, icons } from 'lucide';
import { PosterizeState } from '@/types';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
const pageState: PosterizeState = {
file: null,
pdfJsDoc: null,
pdfBytes: null,
pageSnapshots: {},
currentPage: 1,
file: null,
pdfJsDoc: null,
pdfBytes: null,
pageSnapshots: {},
currentPage: 1,
};
function resetState() {
pageState.file = null;
pageState.pdfJsDoc = null;
pageState.pdfBytes = null;
pageState.pageSnapshots = {};
pageState.currentPage = 1;
pageState.file = null;
pageState.pdfJsDoc = null;
pageState.pdfBytes = null;
pageState.pageSnapshots = {};
pageState.currentPage = 1;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
if (processBtn) processBtn.disabled = true;
const processBtn = document.getElementById(
'process-btn'
) as HTMLButtonElement;
if (processBtn) processBtn.disabled = true;
const totalPages = document.getElementById('total-pages');
if (totalPages) totalPages.textContent = '0';
const totalPages = document.getElementById('total-pages');
if (totalPages) totalPages.textContent = '0';
}
async function renderPosterizePreview(pageNum: number) {
if (!pageState.pdfJsDoc) return;
if (!pageState.pdfJsDoc) return;
pageState.currentPage = pageNum;
showLoader(`Rendering preview for page ${pageNum}...`);
pageState.currentPage = pageNum;
showLoader(`Rendering preview for page ${pageNum}...`);
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
const context = canvas.getContext('2d');
const canvas = document.getElementById(
'posterize-preview-canvas'
) as HTMLCanvasElement;
const context = canvas.getContext('2d');
if (!context) {
hideLoader();
return;
}
if (pageState.pageSnapshots[pageNum]) {
canvas.width = pageState.pageSnapshots[pageNum].width;
canvas.height = pageState.pageSnapshots[pageNum].height;
context.putImageData(pageState.pageSnapshots[pageNum], 0, 0);
} else {
const page = await pageState.pdfJsDoc.getPage(pageNum);
const viewport = page.getViewport({ scale: 1.5 });
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: context, viewport, canvas }).promise;
pageState.pageSnapshots[pageNum] = context.getImageData(0, 0, canvas.width, canvas.height);
}
updatePreviewNav();
drawGridOverlay();
if (!context) {
hideLoader();
return;
}
if (pageState.pageSnapshots[pageNum]) {
canvas.width = pageState.pageSnapshots[pageNum].width;
canvas.height = pageState.pageSnapshots[pageNum].height;
context.putImageData(pageState.pageSnapshots[pageNum], 0, 0);
} else {
const page = await pageState.pdfJsDoc.getPage(pageNum);
const viewport = page.getViewport({ scale: 1.5 });
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: context, viewport, canvas }).promise;
pageState.pageSnapshots[pageNum] = context.getImageData(
0,
0,
canvas.width,
canvas.height
);
}
updatePreviewNav();
drawGridOverlay();
hideLoader();
}
function drawGridOverlay() {
if (!pageState.pageSnapshots[pageState.currentPage] || !pageState.pdfJsDoc) return;
if (!pageState.pageSnapshots[pageState.currentPage] || !pageState.pdfJsDoc)
return;
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
const context = canvas.getContext('2d');
const canvas = document.getElementById(
'posterize-preview-canvas'
) as HTMLCanvasElement;
const context = canvas.getContext('2d');
if (!context) return;
if (!context) return;
context.putImageData(pageState.pageSnapshots[pageState.currentPage], 0, 0);
context.putImageData(pageState.pageSnapshots[pageState.currentPage], 0, 0);
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value;
const pagesToProcess = parsePageRanges(pageRangeInput, pageState.pdfJsDoc.numPages);
const pageRangeInput = (
document.getElementById('page-range') as HTMLInputElement
).value;
const pagesToProcess = parsePageRanges(
pageRangeInput,
pageState.pdfJsDoc.numPages
);
if (pagesToProcess.includes(pageState.currentPage - 1)) {
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1;
if (pagesToProcess.includes(pageState.currentPage - 1)) {
const rows =
parseInt(
(document.getElementById('posterize-rows') as HTMLInputElement).value
) || 1;
const cols =
parseInt(
(document.getElementById('posterize-cols') as HTMLInputElement).value
) || 1;
context.strokeStyle = 'rgba(239, 68, 68, 0.9)';
context.lineWidth = 2;
context.setLineDash([10, 5]);
context.strokeStyle = 'rgba(239, 68, 68, 0.9)';
context.lineWidth = 2;
context.setLineDash([10, 5]);
const cellWidth = canvas.width / cols;
const cellHeight = canvas.height / rows;
const cellWidth = canvas.width / cols;
const cellHeight = canvas.height / rows;
for (let i = 1; i < cols; i++) {
context.beginPath();
context.moveTo(i * cellWidth, 0);
context.lineTo(i * cellWidth, canvas.height);
context.stroke();
}
for (let i = 1; i < rows; i++) {
context.beginPath();
context.moveTo(0, i * cellHeight);
context.lineTo(canvas.width, i * cellHeight);
context.stroke();
}
context.setLineDash([]);
for (let i = 1; i < cols; i++) {
context.beginPath();
context.moveTo(i * cellWidth, 0);
context.lineTo(i * cellWidth, canvas.height);
context.stroke();
}
for (let i = 1; i < rows; i++) {
context.beginPath();
context.moveTo(0, i * cellHeight);
context.lineTo(canvas.width, i * cellHeight);
context.stroke();
}
context.setLineDash([]);
}
}
function updatePreviewNav() {
if (!pageState.pdfJsDoc) return;
if (!pageState.pdfJsDoc) return;
const currentPageSpan = document.getElementById('current-preview-page');
const prevBtn = document.getElementById('prev-preview-page') as HTMLButtonElement;
const nextBtn = document.getElementById('next-preview-page') as HTMLButtonElement;
const currentPageSpan = document.getElementById('current-preview-page');
const prevBtn = document.getElementById(
'prev-preview-page'
) as HTMLButtonElement;
const nextBtn = document.getElementById(
'next-preview-page'
) as HTMLButtonElement;
if (currentPageSpan) currentPageSpan.textContent = pageState.currentPage.toString();
if (prevBtn) prevBtn.disabled = pageState.currentPage <= 1;
if (nextBtn) nextBtn.disabled = pageState.currentPage >= pageState.pdfJsDoc.numPages;
if (currentPageSpan)
currentPageSpan.textContent = pageState.currentPage.toString();
if (prevBtn) prevBtn.disabled = pageState.currentPage <= 1;
if (nextBtn)
nextBtn.disabled = pageState.currentPage >= pageState.pdfJsDoc.numPages;
}
async function posterize() {
if (!pageState.pdfJsDoc || !pageState.pdfBytes) {
showAlert('No File', 'Please upload a PDF file first.');
return;
if (!pageState.pdfJsDoc || !pageState.pdfBytes) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
showLoader('Posterizing PDF...');
try {
const rows =
parseInt(
(document.getElementById('posterize-rows') as HTMLInputElement).value
) || 1;
const cols =
parseInt(
(document.getElementById('posterize-cols') as HTMLInputElement).value
) || 1;
const pageSizeKey = (
document.getElementById('output-page-size') as HTMLSelectElement
).value as keyof typeof PageSizes;
const orientation = (
document.getElementById('output-orientation') as HTMLSelectElement
).value;
const scalingMode = (
document.querySelector(
'input[name="scaling-mode"]:checked'
) as HTMLInputElement
).value;
const overlap =
parseFloat(
(document.getElementById('overlap') as HTMLInputElement).value
) || 0;
const overlapUnits = (
document.getElementById('overlap-units') as HTMLSelectElement
).value;
const pageRangeInput = (
document.getElementById('page-range') as HTMLInputElement
).value;
let overlapInPoints = overlap;
if (overlapUnits === 'in') overlapInPoints = overlap * 72;
else if (overlapUnits === 'mm') overlapInPoints = overlap * (72 / 25.4);
const newDoc = await PDFDocument.create();
const totalPages = pageState.pdfJsDoc.numPages;
const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages);
if (pageIndicesToProcess.length === 0) {
throw new Error('Invalid page range specified.');
}
showLoader('Posterizing PDF...');
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
try {
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1;
const pageSizeKey = (document.getElementById('output-page-size') as HTMLSelectElement).value as keyof typeof PageSizes;
const orientation = (document.getElementById('output-orientation') as HTMLSelectElement).value;
const scalingMode = (document.querySelector('input[name="scaling-mode"]:checked') as HTMLInputElement).value;
const overlap = parseFloat((document.getElementById('overlap') as HTMLInputElement).value) || 0;
const overlapUnits = (document.getElementById('overlap-units') as HTMLSelectElement).value;
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value;
let overlapInPoints = overlap;
if (overlapUnits === 'in') overlapInPoints = overlap * 72;
else if (overlapUnits === 'mm') overlapInPoints = overlap * (72 / 25.4);
const newDoc = await PDFDocument.create();
const totalPages = pageState.pdfJsDoc.numPages;
const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages);
if (pageIndicesToProcess.length === 0) {
throw new Error('Invalid page range specified.');
}
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
if (!tempCtx) {
throw new Error('Could not create canvas context.');
}
for (const pageIndex of pageIndicesToProcess) {
const page = await pageState.pdfJsDoc.getPage(Number(pageIndex) + 1);
const viewport = page.getViewport({ scale: 2.0 });
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport, canvas: tempCanvas }).promise;
let [targetWidth, targetHeight] = PageSizes[pageSizeKey] || PageSizes.A4;
let currentOrientation = orientation;
if (currentOrientation === 'auto') {
currentOrientation = viewport.width > viewport.height ? 'landscape' : 'portrait';
}
if (currentOrientation === 'landscape' && targetWidth < targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
} else if (currentOrientation === 'portrait' && targetWidth > targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
}
const tileWidth = tempCanvas.width / cols;
const tileHeight = tempCanvas.height / rows;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0);
const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0);
const sWidth = tileWidth + (c > 0 ? overlapInPoints : 0) + (c < cols - 1 ? overlapInPoints : 0);
const sHeight = tileHeight + (r > 0 ? overlapInPoints : 0) + (r < rows - 1 ? overlapInPoints : 0);
const tileCanvas = document.createElement('canvas');
tileCanvas.width = sWidth;
tileCanvas.height = sHeight;
const tileCtx = tileCanvas.getContext('2d');
if (tileCtx) {
tileCtx.drawImage(tempCanvas, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
const tileImage = await newDoc.embedPng(tileCanvas.toDataURL('image/png'));
const newPage = newDoc.addPage([targetWidth, targetHeight]);
const scaleX = newPage.getWidth() / sWidth;
const scaleY = newPage.getHeight() / sHeight;
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
const scaledWidth = sWidth * scale;
const scaledHeight = sHeight * scale;
newPage.drawImage(tileImage, {
x: (newPage.getWidth() - scaledWidth) / 2,
y: (newPage.getHeight() - scaledHeight) / 2,
width: scaledWidth,
height: scaledHeight,
});
}
}
}
}
const newPdfBytes = await newDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'posterized.pdf'
);
showAlert('Success', 'Your PDF has been posterized.');
} catch (e) {
console.error(e);
showAlert('Error', (e as Error).message || 'Could not posterize the PDF.');
} finally {
hideLoader();
if (!tempCtx) {
throw new Error('Could not create canvas context.');
}
for (const pageIndex of pageIndicesToProcess) {
const page = await pageState.pdfJsDoc.getPage(Number(pageIndex) + 1);
const viewport = page.getViewport({ scale: 2.0 });
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({
canvasContext: tempCtx,
viewport,
canvas: tempCanvas,
}).promise;
let [targetWidth, targetHeight] = PageSizes[pageSizeKey] || PageSizes.A4;
let currentOrientation = orientation;
if (currentOrientation === 'auto') {
currentOrientation =
viewport.width > viewport.height ? 'landscape' : 'portrait';
}
if (currentOrientation === 'landscape' && targetWidth < targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
} else if (
currentOrientation === 'portrait' &&
targetWidth > targetHeight
) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
}
const tileWidth = tempCanvas.width / cols;
const tileHeight = tempCanvas.height / rows;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0);
const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0);
const sWidth =
tileWidth +
(c > 0 ? overlapInPoints : 0) +
(c < cols - 1 ? overlapInPoints : 0);
const sHeight =
tileHeight +
(r > 0 ? overlapInPoints : 0) +
(r < rows - 1 ? overlapInPoints : 0);
const tileCanvas = document.createElement('canvas');
tileCanvas.width = sWidth;
tileCanvas.height = sHeight;
const tileCtx = tileCanvas.getContext('2d');
if (tileCtx) {
tileCtx.drawImage(
tempCanvas,
sx,
sy,
sWidth,
sHeight,
0,
0,
sWidth,
sHeight
);
const tileImage = await newDoc.embedPng(
tileCanvas.toDataURL('image/png')
);
const newPage = newDoc.addPage([targetWidth, targetHeight]);
const scaleX = newPage.getWidth() / sWidth;
const scaleY = newPage.getHeight() / sHeight;
const scale =
scalingMode === 'fit'
? Math.min(scaleX, scaleY)
: Math.max(scaleX, scaleY);
const scaledWidth = sWidth * scale;
const scaledHeight = sHeight * scale;
newPage.drawImage(tileImage, {
x: (newPage.getWidth() - scaledWidth) / 2,
y: (newPage.getHeight() - scaledHeight) / 2,
width: scaledWidth,
height: scaledHeight,
});
}
}
}
}
const newPdfBytes = await newDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'posterized.pdf'
);
showAlert('Success', 'Your PDF has been posterized.');
} catch (e) {
console.error(e);
showAlert('Error', (e as Error).message || 'Could not posterize the PDF.');
} finally {
hideLoader();
}
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const processBtn = document.getElementById(
'process-btn'
) as HTMLButtonElement;
if (!fileDisplayArea) return;
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(pageState.file.size);
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(pageState.file.size);
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
if (toolOptions) toolOptions.classList.remove('hidden');
if (processBtn) processBtn.disabled = false;
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
if (toolOptions) toolOptions.classList.remove('hidden');
if (processBtn) processBtn.disabled = false;
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
async function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
pageState.pdfBytes = new Uint8Array(await file.arrayBuffer());
pageState.pdfJsDoc = await getPDFDocument({ data: pageState.pdfBytes }).promise;
pageState.pageSnapshots = {};
pageState.currentPage = 1;
if (files && files.length > 0) {
const file = files[0];
if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
const result = await loadPdfWithPasswordPrompt(file);
if (!result) return;
const totalPagesSpan = document.getElementById('total-pages');
const totalPreviewPages = document.getElementById('total-preview-pages');
pageState.file = result.file;
pageState.pdfBytes = new Uint8Array(result.bytes);
pageState.pdfJsDoc = result.pdf;
pageState.pageSnapshots = {};
pageState.currentPage = 1;
if (totalPagesSpan && pageState.pdfJsDoc) {
totalPagesSpan.textContent = pageState.pdfJsDoc.numPages.toString();
}
if (totalPreviewPages && pageState.pdfJsDoc) {
totalPreviewPages.textContent = pageState.pdfJsDoc.numPages.toString();
}
const totalPagesSpan = document.getElementById('total-pages');
const totalPreviewPages = document.getElementById('total-preview-pages');
await updateUI();
await renderPosterizePreview(1);
}
if (totalPagesSpan && pageState.pdfJsDoc) {
totalPagesSpan.textContent = pageState.pdfJsDoc.numPages.toString();
}
if (totalPreviewPages && pageState.pdfJsDoc) {
totalPreviewPages.textContent = pageState.pdfJsDoc.numPages.toString();
}
await updateUI();
await renderPosterizePreview(1);
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
const backBtn = document.getElementById('back-to-tools');
const prevBtn = document.getElementById('prev-preview-page');
const nextBtn = document.getElementById('next-preview-page');
const rowsInput = document.getElementById('posterize-rows');
const colsInput = document.getElementById('posterize-cols');
const pageRangeInput = document.getElementById('page-range');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById(
'process-btn'
) as HTMLButtonElement;
const backBtn = document.getElementById('back-to-tools');
const prevBtn = document.getElementById('prev-preview-page');
const nextBtn = document.getElementById('next-preview-page');
const rowsInput = document.getElementById('posterize-rows');
const colsInput = document.getElementById('posterize-cols');
const pageRangeInput = document.getElementById('page-range');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
});
}
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
// Preview navigation
if (prevBtn) {
prevBtn.addEventListener('click', function () {
if (pageState.currentPage > 1) {
renderPosterizePreview(pageState.currentPage - 1);
}
});
}
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
if (nextBtn) {
nextBtn.addEventListener('click', function () {
if (
pageState.pdfJsDoc &&
pageState.currentPage < pageState.pdfJsDoc.numPages
) {
renderPosterizePreview(pageState.currentPage + 1);
}
});
}
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
// Grid input changes trigger overlay redraw
if (rowsInput) {
rowsInput.addEventListener('input', drawGridOverlay);
}
if (colsInput) {
colsInput.addEventListener('input', drawGridOverlay);
}
if (pageRangeInput) {
pageRangeInput.addEventListener('input', drawGridOverlay);
}
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
// Preview navigation
if (prevBtn) {
prevBtn.addEventListener('click', function () {
if (pageState.currentPage > 1) {
renderPosterizePreview(pageState.currentPage - 1);
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', function () {
if (pageState.pdfJsDoc && pageState.currentPage < pageState.pdfJsDoc.numPages) {
renderPosterizePreview(pageState.currentPage + 1);
}
});
}
// Grid input changes trigger overlay redraw
if (rowsInput) {
rowsInput.addEventListener('input', drawGridOverlay);
}
if (colsInput) {
colsInput.addEventListener('input', drawGridOverlay);
}
if (pageRangeInput) {
pageRangeInput.addEventListener('input', drawGridOverlay);
}
// Process button
if (processBtn) {
processBtn.addEventListener('click', posterize);
}
// Process button
if (processBtn) {
processBtn.addEventListener('click', posterize);
}
});

View File

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

View File

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

View File

@@ -1,64 +1,71 @@
import { PDFDocument, PDFName } from 'pdf-lib';
import { createIcons, icons } from 'lucide';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
// State management
const pageState: { pdfDoc: PDFDocument | null; file: File | null } = {
pdfDoc: null,
file: null,
pdfDoc: null,
file: null,
};
// UI helpers
function showLoader(message: string = 'Processing...') {
const loader = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text');
if (loader) loader.classList.remove('hidden');
if (loaderText) loaderText.textContent = message;
const loader = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text');
if (loader) loader.classList.remove('hidden');
if (loaderText) loaderText.textContent = message;
}
function hideLoader() {
const loader = document.getElementById('loader-modal');
if (loader) loader.classList.add('hidden');
const loader = document.getElementById('loader-modal');
if (loader) loader.classList.add('hidden');
}
function showAlert(title: string, message: string, type: string = 'error', callback?: () => void) {
const modal = document.getElementById('alert-modal');
const alertTitle = document.getElementById('alert-title');
const alertMessage = document.getElementById('alert-message');
const okBtn = document.getElementById('alert-ok');
function showAlert(
title: string,
message: string,
type: string = 'error',
callback?: () => void
) {
const modal = document.getElementById('alert-modal');
const alertTitle = document.getElementById('alert-title');
const alertMessage = document.getElementById('alert-message');
const okBtn = document.getElementById('alert-ok');
if (alertTitle) alertTitle.textContent = title;
if (alertMessage) alertMessage.textContent = message;
if (modal) modal.classList.remove('hidden');
if (alertTitle) alertTitle.textContent = title;
if (alertMessage) alertMessage.textContent = message;
if (modal) modal.classList.remove('hidden');
if (okBtn) {
const newOkBtn = okBtn.cloneNode(true) as HTMLElement;
okBtn.replaceWith(newOkBtn);
newOkBtn.addEventListener('click', () => {
modal?.classList.add('hidden');
if (callback) callback();
});
}
if (okBtn) {
const newOkBtn = okBtn.cloneNode(true) as HTMLElement;
okBtn.replaceWith(newOkBtn);
newOkBtn.addEventListener('click', () => {
modal?.classList.add('hidden');
if (callback) callback();
});
}
}
function downloadFile(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
function updateFileDisplay() {
const displayArea = document.getElementById('file-display-area');
if (!displayArea || !pageState.file || !pageState.pdfDoc) return;
const displayArea = document.getElementById('file-display-area');
if (!displayArea || !pageState.file || !pageState.pdfDoc) return;
const fileSize = pageState.file.size < 1024 * 1024
? `${(pageState.file.size / 1024).toFixed(1)} KB`
: `${(pageState.file.size / 1024 / 1024).toFixed(2)} MB`;
const pageCount = pageState.pdfDoc.getPageCount();
const fileSize =
pageState.file.size < 1024 * 1024
? `${(pageState.file.size / 1024).toFixed(1)} KB`
: `${(pageState.file.size / 1024 / 1024).toFixed(2)} MB`;
const pageCount = pageState.pdfDoc.getPageCount();
displayArea.innerHTML = `
displayArea.innerHTML = `
<div class="bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors">
<div class="flex items-center justify-between">
<div class="flex-1 min-w-0">
@@ -72,105 +79,114 @@ function updateFileDisplay() {
</div>
`;
createIcons({ icons });
createIcons({ icons });
document.getElementById('remove-file')?.addEventListener('click', () => resetState());
document
.getElementById('remove-file')
?.addEventListener('click', () => resetState());
}
function resetState() {
pageState.pdfDoc = null;
pageState.file = null;
const displayArea = document.getElementById('file-display-area');
if (displayArea) displayArea.innerHTML = '';
document.getElementById('options-panel')?.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
pageState.pdfDoc = null;
pageState.file = null;
const displayArea = document.getElementById('file-display-area');
if (displayArea) displayArea.innerHTML = '';
document.getElementById('options-panel')?.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
// File handling
async function handleFileUpload(file: File) {
if (!file || file.type !== 'application/pdf') {
showAlert('Error', 'Please upload a valid PDF file.');
return;
}
if (!file || file.type !== 'application/pdf') {
showAlert('Error', 'Please upload a valid PDF file.');
return;
}
try {
const result = await loadPdfWithPasswordPrompt(file);
if (!result) return;
showLoader('Loading PDF...');
try {
const arrayBuffer = await file.arrayBuffer();
pageState.pdfDoc = await PDFDocument.load(arrayBuffer);
pageState.file = file;
updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden');
} catch (error) {
console.error(error);
showAlert('Error', 'Failed to load PDF file.');
} finally {
hideLoader();
}
result.pdf.destroy();
pageState.pdfDoc = await PDFDocument.load(result.bytes);
pageState.file = result.file;
updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden');
} catch (error) {
console.error(error);
showAlert('Error', 'Failed to load PDF file.');
} finally {
hideLoader();
}
}
// Process function
async function processRemoveAnnotations() {
if (!pageState.pdfDoc) {
showAlert('Error', 'Please upload a PDF file first.');
return;
if (!pageState.pdfDoc) {
showAlert('Error', 'Please upload a PDF file first.');
return;
}
showLoader('Removing annotations...');
try {
const pages = pageState.pdfDoc.getPages();
// Remove all annotations from all pages
for (let i = 0; i < pages.length; i++) {
const page = pages[i];
const annotRefs = page.node.Annots()?.asArray() || [];
if (annotRefs.length > 0) {
page.node.delete(PDFName.of('Annots'));
}
}
showLoader('Removing annotations...');
try {
const pages = pageState.pdfDoc.getPages();
// Remove all annotations from all pages
for (let i = 0; i < pages.length; i++) {
const page = pages[i];
const annotRefs = page.node.Annots()?.asArray() || [];
if (annotRefs.length > 0) {
page.node.delete(PDFName.of('Annots'));
}
}
const newPdfBytes = await pageState.pdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'annotations-removed.pdf');
showAlert('Success', 'Annotations removed successfully!', 'success', () => { resetState(); });
} catch (e) {
console.error(e);
showAlert('Error', 'Could not remove annotations.');
} finally {
hideLoader();
}
const newPdfBytes = await pageState.pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'annotations-removed.pdf'
);
showAlert('Success', 'Annotations removed successfully!', 'success', () => {
resetState();
});
} catch (e) {
console.error(e);
showAlert('Error', 'Could not remove annotations.');
} finally {
hideLoader();
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
fileInput?.addEventListener('change', (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) handleFileUpload(file);
});
fileInput?.addEventListener('change', (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) handleFileUpload(file);
});
dropZone?.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-indigo-500');
});
dropZone?.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-indigo-500');
});
dropZone?.addEventListener('dragleave', () => {
dropZone.classList.remove('border-indigo-500');
});
dropZone?.addEventListener('dragleave', () => {
dropZone.classList.remove('border-indigo-500');
});
dropZone?.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-indigo-500');
const file = e.dataTransfer?.files[0];
if (file) handleFileUpload(file);
});
dropZone?.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-indigo-500');
const file = e.dataTransfer?.files[0];
if (file) handleFileUpload(file);
});
processBtn?.addEventListener('click', processRemoveAnnotations);
processBtn?.addEventListener('click', processRemoveAnnotations);
backBtn?.addEventListener('click', () => {
window.location.href = '../../index.html';
});
backBtn?.addEventListener('click', () => {
window.location.href = '../../index.html';
});
});

View File

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

View File

@@ -2,202 +2,226 @@ import { showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { PDFDocument, PDFName } from 'pdf-lib';
import { icons, createIcons } from 'lucide';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
interface PageState {
file: File | null;
file: File | null;
}
const pageState: PageState = {
file: null,
file: null,
};
function removeMetadataFromDoc(pdfDoc: PDFDocument) {
const infoDict = (pdfDoc as any).getInfoDict();
const allKeys = infoDict.keys();
allKeys.forEach((key: any) => {
infoDict.delete(key);
});
// @ts-expect-error getInfoDict is private but accessible at runtime
const infoDict = pdfDoc.getInfoDict();
const allKeys = infoDict.keys();
allKeys.forEach((key: { asString: () => string }) => {
infoDict.delete(key);
});
pdfDoc.setTitle('');
pdfDoc.setAuthor('');
pdfDoc.setSubject('');
pdfDoc.setKeywords([]);
pdfDoc.setCreator('');
pdfDoc.setProducer('');
pdfDoc.setTitle('');
pdfDoc.setAuthor('');
pdfDoc.setSubject('');
pdfDoc.setKeywords([]);
pdfDoc.setCreator('');
pdfDoc.setProducer('');
try {
const catalogDict = (pdfDoc.catalog as any).dict;
if (catalogDict.has(PDFName.of('Metadata'))) {
catalogDict.delete(PDFName.of('Metadata'));
}
} catch (e: any) {
console.warn('Could not remove XMP metadata:', e.message);
try {
// @ts-expect-error catalog.dict is private but accessible at runtime
const catalogDict = pdfDoc.catalog.dict;
if (catalogDict.has(PDFName.of('Metadata'))) {
catalogDict.delete(PDFName.of('Metadata'));
}
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
console.warn('Could not remove XMP metadata:', msg);
}
try {
const context = pdfDoc.context;
if ((context as any).trailerInfo) {
delete (context as any).trailerInfo.ID;
}
} catch (e: any) {
console.warn('Could not remove document IDs:', e.message);
try {
const context = pdfDoc.context;
if (context.trailerInfo) {
delete context.trailerInfo.ID;
}
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
console.warn('Could not remove document IDs:', msg);
}
try {
const catalogDict = (pdfDoc.catalog as any).dict;
if (catalogDict.has(PDFName.of('PieceInfo'))) {
catalogDict.delete(PDFName.of('PieceInfo'));
}
} catch (e: any) {
console.warn('Could not remove PieceInfo:', e.message);
try {
// @ts-expect-error catalog.dict is private but accessible at runtime
const catalogDict = pdfDoc.catalog.dict;
if (catalogDict.has(PDFName.of('PieceInfo'))) {
catalogDict.delete(PDFName.of('PieceInfo'));
}
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
console.warn('Could not remove PieceInfo:', msg);
}
}
function resetState() {
pageState.file = null;
pageState.file = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(pageState.file.size);
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(pageState.file.size);
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
if (toolOptions) toolOptions.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
if (toolOptions) toolOptions.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
if (files && files.length > 0) {
const file = files[0];
if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file;
updateUI();
}
}
}
async function removeMetadata() {
if (!pageState.file) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
if (!pageState.file) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
const loaderModal = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text');
const loaderModal = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text');
if (loaderModal) loaderModal.classList.remove('hidden');
if (loaderText) loaderText.textContent = 'Removing all metadata...';
try {
if (loaderModal) loaderModal.classList.add('hidden');
const result = await loadPdfWithPasswordPrompt(pageState.file);
if (!result) {
if (loaderModal) loaderModal.classList.add('hidden');
return;
}
if (loaderModal) loaderModal.classList.remove('hidden');
if (loaderText) loaderText.textContent = 'Removing all metadata...';
result.pdf.destroy();
const pdfDoc = await PDFDocument.load(result.bytes);
try {
const arrayBuffer = await pageState.file.arrayBuffer();
const pdfDoc = await PDFDocument.load(arrayBuffer);
removeMetadataFromDoc(pdfDoc);
removeMetadataFromDoc(pdfDoc);
const newPdfBytes = await pdfDoc.save();
downloadFile(
new Blob([newPdfBytes as BlobPart], { type: 'application/pdf' }),
'metadata-removed.pdf'
);
showAlert('Success', 'Metadata removed successfully!', 'success', () => { resetState(); });
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while trying to remove metadata.');
} finally {
if (loaderModal) loaderModal.classList.add('hidden');
}
const newPdfBytes = await pdfDoc.save();
downloadFile(
new Blob([newPdfBytes as BlobPart], { type: 'application/pdf' }),
'metadata-removed.pdf'
);
showAlert('Success', 'Metadata removed successfully!', 'success', () => {
resetState();
});
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while trying to remove metadata.');
} finally {
if (loaderModal) loaderModal.classList.add('hidden');
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
});
}
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', removeMetadata);
}
if (processBtn) {
processBtn.addEventListener('click', removeMetadata);
}
});

View File

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

View File

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

View File

@@ -1,386 +1,443 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument, degrees } from 'pdf-lib';
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js';
import {
renderPagesProgressively,
cleanupLazyRendering,
} from '../utils/render-utils.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
interface RotateState {
file: File | null;
pdfDoc: PDFLibDocument | null;
pdfJsDoc: pdfjsLib.PDFDocumentProxy | null;
rotations: number[];
file: File | null;
pdfDoc: PDFLibDocument | null;
pdfJsDoc: pdfjsLib.PDFDocumentProxy | null;
rotations: number[];
}
const pageState: RotateState = {
file: null,
pdfDoc: null,
pdfJsDoc: null,
rotations: [],
file: null,
pdfDoc: null,
pdfJsDoc: null,
rotations: [],
};
function resetState() {
cleanupLazyRendering();
pageState.file = null;
pageState.pdfDoc = null;
pageState.pdfJsDoc = null;
pageState.rotations = [];
cleanupLazyRendering();
pageState.file = null;
pageState.pdfDoc = null;
pageState.pdfJsDoc = null;
pageState.rotations = [];
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const pageThumbnails = document.getElementById('page-thumbnails');
if (pageThumbnails) pageThumbnails.innerHTML = '';
const pageThumbnails = document.getElementById('page-thumbnails');
if (pageThumbnails) pageThumbnails.innerHTML = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const batchAngle = document.getElementById('batch-custom-angle') as HTMLInputElement;
if (batchAngle) batchAngle.value = '0';
const batchAngle = document.getElementById(
'batch-custom-angle'
) as HTMLInputElement;
if (batchAngle) batchAngle.value = '0';
}
function updateAllRotationDisplays() {
for (let i = 0; i < pageState.rotations.length; i++) {
const input = document.getElementById(`page-angle-${i}`) as HTMLInputElement;
if (input) input.value = pageState.rotations[i].toString();
const container = document.querySelector(`[data-page-index="${i}"]`);
if (container) {
const wrapper = container.querySelector('.thumbnail-wrapper') as HTMLElement;
if (wrapper) wrapper.style.transform = `rotate(${-pageState.rotations[i]}deg)`;
}
for (let i = 0; i < pageState.rotations.length; i++) {
const input = document.getElementById(
`page-angle-${i}`
) as HTMLInputElement;
if (input) input.value = pageState.rotations[i].toString();
const container = document.querySelector(`[data-page-index="${i}"]`);
if (container) {
const wrapper = container.querySelector(
'.thumbnail-wrapper'
) as HTMLElement;
if (wrapper)
wrapper.style.transform = `rotate(${-pageState.rotations[i]}deg)`;
}
}
}
function createPageWrapper(canvas: HTMLCanvasElement, pageNumber: number): HTMLElement {
const pageIndex = pageNumber - 1;
function createPageWrapper(
canvas: HTMLCanvasElement,
pageNumber: number
): HTMLElement {
const pageIndex = pageNumber - 1;
const container = document.createElement('div');
container.className = 'page-thumbnail relative bg-gray-700 rounded-lg overflow-hidden';
container.dataset.pageIndex = pageIndex.toString();
container.dataset.pageNumber = pageNumber.toString();
const container = document.createElement('div');
container.className =
'page-thumbnail relative bg-gray-700 rounded-lg overflow-hidden';
container.dataset.pageIndex = pageIndex.toString();
container.dataset.pageNumber = pageNumber.toString();
const canvasWrapper = document.createElement('div');
canvasWrapper.className = 'thumbnail-wrapper flex items-center justify-center p-2 h-36';
canvasWrapper.style.transition = 'transform 0.3s ease';
// Apply initial rotation if it exists (negated for canvas display)
const initialRotation = pageState.rotations[pageIndex] || 0;
canvasWrapper.style.transform = `rotate(${-initialRotation}deg)`;
const canvasWrapper = document.createElement('div');
canvasWrapper.className =
'thumbnail-wrapper flex items-center justify-center p-2 h-36';
canvasWrapper.style.transition = 'transform 0.3s ease';
// Apply initial rotation if it exists (negated for canvas display)
const initialRotation = pageState.rotations[pageIndex] || 0;
canvasWrapper.style.transform = `rotate(${-initialRotation}deg)`;
canvas.className = 'max-w-full max-h-full object-contain';
canvasWrapper.appendChild(canvas);
canvas.className = 'max-w-full max-h-full object-contain';
canvasWrapper.appendChild(canvas);
const pageLabel = document.createElement('div');
pageLabel.className = 'absolute top-1 left-1 bg-black bg-opacity-60 text-white text-xs px-2 py-1 rounded';
pageLabel.textContent = `${pageNumber}`;
const pageLabel = document.createElement('div');
pageLabel.className =
'absolute top-1 left-1 bg-black bg-opacity-60 text-white text-xs px-2 py-1 rounded';
pageLabel.textContent = `${pageNumber}`;
container.appendChild(canvasWrapper);
container.appendChild(pageLabel);
container.appendChild(canvasWrapper);
container.appendChild(pageLabel);
// Per-page rotation controls - Custom angle input
const controls = document.createElement('div');
controls.className = 'flex items-center justify-center gap-1 p-2 bg-gray-800';
// Per-page rotation controls - Custom angle input
const controls = document.createElement('div');
controls.className = 'flex items-center justify-center gap-1 p-2 bg-gray-800';
const decrementBtn = document.createElement('button');
decrementBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-sm';
decrementBtn.textContent = '-';
decrementBtn.onclick = function (e) {
e.stopPropagation();
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement;
const current = parseInt(input.value) || 0;
input.value = (current - 1).toString();
};
const decrementBtn = document.createElement('button');
decrementBtn.className =
'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-sm';
decrementBtn.textContent = '-';
decrementBtn.onclick = function (e) {
e.stopPropagation();
const input = document.getElementById(
`page-angle-${pageIndex}`
) as HTMLInputElement;
const current = parseInt(input.value) || 0;
input.value = (current - 1).toString();
};
const angleInput = document.createElement('input');
angleInput.type = 'number';
angleInput.id = `page-angle-${pageIndex}`;
angleInput.value = pageState.rotations[pageIndex]?.toString() || '0';
angleInput.className = 'w-12 h-8 text-center bg-gray-700 border border-gray-600 text-white rounded text-xs';
const angleInput = document.createElement('input');
angleInput.type = 'number';
angleInput.id = `page-angle-${pageIndex}`;
angleInput.value = pageState.rotations[pageIndex]?.toString() || '0';
angleInput.className =
'w-12 h-8 text-center bg-gray-700 border border-gray-600 text-white rounded text-xs';
const incrementBtn = document.createElement('button');
incrementBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-sm';
incrementBtn.textContent = '+';
incrementBtn.onclick = function (e) {
e.stopPropagation();
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement;
const current = parseInt(input.value) || 0;
input.value = (current + 1).toString();
};
const incrementBtn = document.createElement('button');
incrementBtn.className =
'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-sm';
incrementBtn.textContent = '+';
incrementBtn.onclick = function (e) {
e.stopPropagation();
const input = document.getElementById(
`page-angle-${pageIndex}`
) as HTMLInputElement;
const current = parseInt(input.value) || 0;
input.value = (current + 1).toString();
};
const applyBtn = document.createElement('button');
applyBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600';
applyBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-4 h-4"></i>';
applyBtn.onclick = function (e) {
e.stopPropagation();
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement;
const angle = parseInt(input.value) || 0;
pageState.rotations[pageIndex] = angle;
const wrapper = container.querySelector('.thumbnail-wrapper') as HTMLElement;
if (wrapper) wrapper.style.transform = `rotate(${-angle}deg)`;
};
const applyBtn = document.createElement('button');
applyBtn.className =
'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600';
applyBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-4 h-4"></i>';
applyBtn.onclick = function (e) {
e.stopPropagation();
const input = document.getElementById(
`page-angle-${pageIndex}`
) as HTMLInputElement;
const angle = parseInt(input.value) || 0;
pageState.rotations[pageIndex] = angle;
const wrapper = container.querySelector(
'.thumbnail-wrapper'
) as HTMLElement;
if (wrapper) wrapper.style.transform = `rotate(${-angle}deg)`;
};
controls.append(decrementBtn, angleInput, incrementBtn, applyBtn);
container.appendChild(controls);
controls.append(decrementBtn, angleInput, incrementBtn, applyBtn);
container.appendChild(controls);
// Re-create icons for the new element
setTimeout(function () {
createIcons({ icons });
}, 0);
// Re-create icons for the new element
setTimeout(function () {
createIcons({ icons });
}, 0);
return container;
return container;
}
async function renderThumbnails() {
const pageThumbnails = document.getElementById('page-thumbnails');
if (!pageThumbnails || !pageState.pdfJsDoc) return;
const pageThumbnails = document.getElementById('page-thumbnails');
if (!pageThumbnails || !pageState.pdfJsDoc) return;
pageThumbnails.innerHTML = '';
pageThumbnails.innerHTML = '';
await renderPagesProgressively(
pageState.pdfJsDoc,
pageThumbnails,
createPageWrapper,
{
batchSize: 8,
useLazyLoading: true,
lazyLoadMargin: '200px',
eagerLoadBatches: 2,
onBatchComplete: function () {
createIcons({ icons });
}
}
);
await renderPagesProgressively(
pageState.pdfJsDoc,
pageThumbnails,
createPageWrapper,
{
batchSize: 8,
useLazyLoading: true,
lazyLoadMargin: '200px',
eagerLoadBatches: 2,
onBatchComplete: function () {
createIcons({ icons });
},
}
);
createIcons({ icons });
createIcons({ icons });
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
try {
showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
try {
const result = await loadPdfWithPasswordPrompt(pageState.file);
if (!result) {
resetState();
return;
}
showLoader('Loading PDF...');
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer.slice(0), {
ignoreEncryption: true,
throwOnInvalidObject: false
});
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
throwOnInvalidObject: false,
});
pageState.pdfJsDoc = await getPDFDocument({ data: arrayBuffer.slice(0) }).promise;
pageState.pdfJsDoc = result.pdf;
const pageCount = pageState.pdfDoc.getPageCount();
pageState.rotations = new Array(pageCount).fill(0);
const pageCount = pageState.pdfDoc.getPageCount();
pageState.rotations = new Array(pageCount).fill(0);
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
await renderThumbnails();
hideLoader();
await renderThumbnails();
hideLoader();
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
async function applyRotations() {
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF first.');
return;
}
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF first.');
return;
}
showLoader('Applying rotations...');
showLoader('Applying rotations...');
try {
const pageCount = pageState.pdfDoc.getPageCount();
const newPdfDoc = await PDFLibDocument.create();
try {
const pageCount = pageState.pdfDoc.getPageCount();
const newPdfDoc = await PDFLibDocument.create();
for (let i = 0; i < pageCount; i++) {
const rotation = pageState.rotations[i] || 0;
const originalPage = pageState.pdfDoc.getPage(i);
const currentRotation = originalPage.getRotation().angle;
const totalRotation = currentRotation + rotation;
for (let i = 0; i < pageCount; i++) {
const rotation = pageState.rotations[i] || 0;
const originalPage = pageState.pdfDoc.getPage(i);
const currentRotation = originalPage.getRotation().angle;
const totalRotation = currentRotation + rotation;
console.log(`Page ${i}: rotation=${rotation}, currentRotation=${currentRotation}, totalRotation=${totalRotation}, applying=${-totalRotation}`);
console.log(
`Page ${i}: rotation=${rotation}, currentRotation=${currentRotation}, totalRotation=${totalRotation}, applying=${-totalRotation}`
);
if (totalRotation % 90 === 0) {
const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
copiedPage.setRotation(degrees(totalRotation));
newPdfDoc.addPage(copiedPage);
} else {
const embeddedPage = await newPdfDoc.embedPage(originalPage);
const { width, height } = embeddedPage.scale(1);
if (totalRotation % 90 === 0) {
const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
copiedPage.setRotation(degrees(totalRotation));
newPdfDoc.addPage(copiedPage);
} else {
const embeddedPage = await newPdfDoc.embedPage(originalPage);
const { width, height } = embeddedPage.scale(1);
const angleRad = (totalRotation * Math.PI) / 180;
const absCos = Math.abs(Math.cos(angleRad));
const absSin = Math.abs(Math.sin(angleRad));
const angleRad = (totalRotation * Math.PI) / 180;
const absCos = Math.abs(Math.cos(angleRad));
const absSin = Math.abs(Math.sin(angleRad));
const newWidth = width * absCos + height * absSin;
const newHeight = width * absSin + height * absCos;
const newWidth = width * absCos + height * absSin;
const newHeight = width * absSin + height * absCos;
const newPage = newPdfDoc.addPage([newWidth, newHeight]);
const newPage = newPdfDoc.addPage([newWidth, newHeight]);
const x = newWidth / 2 - (width / 2 * Math.cos(angleRad) - height / 2 * Math.sin(angleRad));
const y = newHeight / 2 - (width / 2 * Math.sin(angleRad) + height / 2 * Math.cos(angleRad));
const x =
newWidth / 2 -
((width / 2) * Math.cos(angleRad) -
(height / 2) * Math.sin(angleRad));
const y =
newHeight / 2 -
((width / 2) * Math.sin(angleRad) +
(height / 2) * Math.cos(angleRad));
newPage.drawPage(embeddedPage, {
x,
y,
width,
height,
rotate: degrees(totalRotation),
});
}
}
const rotatedPdfBytes = await newPdfDoc.save();
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
downloadFile(
new Blob([new Uint8Array(rotatedPdfBytes)], { type: 'application/pdf' }),
`${originalName}_rotated.pdf`
);
showAlert('Success', 'Rotations applied successfully!', 'success', function () {
resetState();
newPage.drawPage(embeddedPage, {
x,
y,
width,
height,
rotate: degrees(totalRotation),
});
} catch (e) {
console.error(e);
showAlert('Error', 'Could not apply rotations.');
} finally {
hideLoader();
}
}
const rotatedPdfBytes = await newPdfDoc.save();
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
downloadFile(
new Blob([new Uint8Array(rotatedPdfBytes)], { type: 'application/pdf' }),
`${originalName}_rotated.pdf`
);
showAlert(
'Success',
'Rotations applied successfully!',
'success',
function () {
resetState();
}
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not apply rotations.');
} finally {
hideLoader();
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
if (files && files.length > 0) {
const file = files[0];
if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const batchDecrement = document.getElementById('batch-decrement');
const batchIncrement = document.getElementById('batch-increment');
const batchApply = document.getElementById('batch-apply');
const batchAngleInput = document.getElementById('batch-custom-angle') as HTMLInputElement;
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const batchDecrement = document.getElementById('batch-decrement');
const batchIncrement = document.getElementById('batch-increment');
const batchApply = document.getElementById('batch-apply');
const batchAngleInput = document.getElementById(
'batch-custom-angle'
) as HTMLInputElement;
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (batchDecrement && batchAngleInput) {
batchDecrement.addEventListener('click', function () {
const current = parseInt(batchAngleInput.value) || 0;
batchAngleInput.value = (current - 1).toString();
});
}
if (batchIncrement && batchAngleInput) {
batchIncrement.addEventListener('click', function () {
const current = parseInt(batchAngleInput.value) || 0;
batchAngleInput.value = (current + 1).toString();
});
}
if (batchApply && batchAngleInput) {
batchApply.addEventListener('click', function () {
const angle = parseInt(batchAngleInput.value) || 0;
for (let i = 0; i < pageState.rotations.length; i++) {
pageState.rotations[i] = angle;
}
updateAllRotationDisplays();
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
});
}
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
if (batchDecrement && batchAngleInput) {
batchDecrement.addEventListener('click', function () {
const current = parseInt(batchAngleInput.value) || 0;
batchAngleInput.value = (current - 1).toString();
});
}
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (batchIncrement && batchAngleInput) {
batchIncrement.addEventListener('click', function () {
const current = parseInt(batchAngleInput.value) || 0;
batchAngleInput.value = (current + 1).toString();
});
}
if (batchApply && batchAngleInput) {
batchApply.addEventListener('click', function () {
const angle = parseInt(batchAngleInput.value) || 0;
for (let i = 0; i < pageState.rotations.length; i++) {
pageState.rotations[i] = angle;
}
updateAllRotationDisplays();
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', applyRotations);
}
if (processBtn) {
processBtn.addEventListener('click', applyRotations);
}
});

View File

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

View File

@@ -3,6 +3,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
import { icons, createIcons } from 'lucide';
import { SanitizePdfState } from '@/types';
import { sanitizePdf } from '../utils/sanitize.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
const pageState: SanitizePdfState = {
file: null,
@@ -132,8 +133,17 @@ async function runSanitize() {
return;
}
const arrayBuffer = await pageState.file.arrayBuffer();
const result = await sanitizePdf(new Uint8Array(arrayBuffer), options);
if (loaderModal) loaderModal.classList.add('hidden');
const loaded = await loadPdfWithPasswordPrompt(pageState.file);
if (!loaded) {
if (loaderModal) loaderModal.classList.add('hidden');
return;
}
if (loaderModal) loaderModal.classList.remove('hidden');
if (loaderText) loaderText.textContent = 'Sanitizing PDF...';
loaded.pdf.destroy();
pageState.file = loaded.file;
const result = await sanitizePdf(new Uint8Array(loaded.bytes), options);
downloadFile(
new Blob([new Uint8Array(result.bytes)], { type: 'application/pdf' }),
@@ -147,9 +157,10 @@ async function runSanitize() {
resetState();
}
);
} catch (e: any) {
} catch (e: unknown) {
console.error('Sanitization Error:', e);
showAlert('Error', `An error occurred during sanitization: ${e.message}`);
const msg = e instanceof Error ? e.message : String(e);
showAlert('Error', `An error occurred during sanitization: ${msg}`);
} finally {
if (loaderModal) loaderModal.classList.add('hidden');
}

View File

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

View File

@@ -1,313 +1,358 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { readFileAsArrayBuffer, formatBytes, downloadFile, getPDFDocument } from '../utils/helpers.js';
import {
readFileAsArrayBuffer,
formatBytes,
downloadFile,
} from '../utils/helpers.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
import { PDFDocument } from 'pdf-lib';
import { t } from '../i18n/i18n';
interface SignState {
file: File | null;
pdfDoc: any;
viewerIframe: HTMLIFrameElement | null;
viewerReady: boolean;
blobUrl: string | null;
file: File | null;
pdfDoc: any;
viewerIframe: HTMLIFrameElement | null;
viewerReady: boolean;
blobUrl: string | null;
}
const signState: SignState = {
file: null,
pdfDoc: null,
viewerIframe: null,
viewerReady: false,
blobUrl: null,
file: null,
pdfDoc: null,
viewerIframe: null,
viewerReady: false,
blobUrl: null,
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
document.addEventListener('DOMContentLoaded', initializePage);
} else {
initializePage();
initializePage();
}
function initializePage() {
createIcons({ icons });
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
}
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
}
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) {
handleFile(droppedFiles[0]);
}
});
// Clear value on click to allow re-selecting the same file
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', applyAndSaveSignatures);
}
document.getElementById('back-to-tools')?.addEventListener('click', () => {
cleanup();
window.location.href = import.meta.env.BASE_URL;
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) {
handleFile(droppedFiles[0]);
}
});
// Clear value on click to allow re-selecting the same file
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', applyAndSaveSignatures);
}
document.getElementById('back-to-tools')?.addEventListener('click', () => {
cleanup();
window.location.href = import.meta.env.BASE_URL;
});
}
function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
handleFile(input.files[0]);
}
const input = e.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
handleFile(input.files[0]);
}
}
function handleFile(file: File) {
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
showAlert('Invalid File', 'Please select a PDF file.');
return;
}
if (
file.type !== 'application/pdf' &&
!file.name.toLowerCase().endsWith('.pdf')
) {
showAlert('Invalid File', 'Please select a PDF file.');
return;
}
signState.file = file;
updateFileDisplay();
setupSignTool();
signState.file = file;
updateFileDisplay();
setupSignTool();
}
async function updateFileDisplay() {
const fileDisplayArea = document.getElementById('file-display-area');
const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea || !signState.file) return;
if (!fileDisplayArea || !signState.file) return;
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = signState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(signState.file.size)}${t('common.loadingPageCount')}`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide=\"trash-2\" class=\"w-4 h-4\"></i>';
removeBtn.onclick = () => {
signState.file = null;
signState.pdfDoc = null;
fileDisplayArea.innerHTML = '';
document.getElementById('signature-editor')?.classList.add('hidden');
};
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = signState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(signState.file.size)}${t('common.loadingPageCount')}`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide=\"trash-2\" class=\"w-4 h-4\"></i>';
removeBtn.onclick = () => {
signState.file = null;
signState.pdfDoc = null;
fileDisplayArea.innerHTML = '';
document.getElementById('signature-editor')?.classList.add('hidden');
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
// Load page count
try {
const arrayBuffer = await readFileAsArrayBuffer(signState.file);
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
metaSpan.textContent = `${formatBytes(signState.file.size)}${pdfDoc.numPages} pages`;
} catch (error) {
console.error('Error loading PDF:', error);
}
const result = await loadPdfWithPasswordPrompt(signState.file);
if (!result) {
signState.file = null;
signState.pdfDoc = null;
fileDisplayArea.innerHTML = '';
document.getElementById('signature-editor')?.classList.add('hidden');
return;
}
signState.file = result.file;
nameSpan.textContent = result.file.name;
metaSpan.textContent = `${formatBytes(result.file.size)}${result.pdf.numPages} pages`;
result.pdf.destroy();
}
async function setupSignTool() {
const signatureEditor = document.getElementById('signature-editor');
if (signatureEditor) {
signatureEditor.classList.remove('hidden');
}
const signatureEditor = document.getElementById('signature-editor');
if (signatureEditor) {
signatureEditor.classList.remove('hidden');
}
showLoader('Loading PDF viewer...');
showLoader('Loading PDF viewer...');
const container = document.getElementById('canvas-container-sign');
if (!container) {
console.error('Sign tool canvas container not found');
hideLoader();
return;
}
const container = document.getElementById('canvas-container-sign');
if (!container) {
console.error('Sign tool canvas container not found');
hideLoader();
return;
}
if (!signState.file) {
console.error('No file loaded for signing');
hideLoader();
return;
}
if (!signState.file) {
console.error('No file loaded for signing');
hideLoader();
return;
}
container.textContent = '';
const iframe = document.createElement('iframe');
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.border = 'none';
container.appendChild(iframe);
signState.viewerIframe = iframe;
container.textContent = '';
const iframe = document.createElement('iframe');
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.border = 'none';
container.appendChild(iframe);
signState.viewerIframe = iframe;
const pdfBytes = await readFileAsArrayBuffer(signState.file);
const blob = new Blob([pdfBytes as BlobPart], { type: 'application/pdf' });
signState.blobUrl = URL.createObjectURL(blob);
const pdfBytes = await readFileAsArrayBuffer(signState.file);
const blob = new Blob([pdfBytes as BlobPart], { type: 'application/pdf' });
signState.blobUrl = URL.createObjectURL(blob);
try {
const existingPrefsRaw = localStorage.getItem('pdfjs.preferences');
const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {};
delete (existingPrefs as any).annotationEditorMode;
const newPrefs = {
...existingPrefs,
enableSignatureEditor: true,
enablePermissions: false,
};
localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs));
} catch { }
const viewerUrl = new URL(`${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html`, window.location.origin);
const query = new URLSearchParams({ file: signState.blobUrl });
iframe.src = `${viewerUrl.toString()}?${query.toString()}`;
iframe.onload = () => {
hideLoader();
signState.viewerReady = true;
try {
const viewerWindow: any = iframe.contentWindow;
if (viewerWindow && viewerWindow.PDFViewerApplication) {
const app = viewerWindow.PDFViewerApplication;
const doc = viewerWindow.document;
const eventBus = app.eventBus;
eventBus?._on('annotationeditoruimanager', () => {
const editorModeButtons = doc.getElementById('editorModeButtons');
editorModeButtons?.classList.remove('hidden');
const editorSignature = doc.getElementById('editorSignature');
editorSignature?.removeAttribute('hidden');
const editorSignatureButton = doc.getElementById('editorSignatureButton') as HTMLButtonElement | null;
if (editorSignatureButton) {
editorSignatureButton.disabled = false;
}
const editorStamp = doc.getElementById('editorStamp');
editorStamp?.removeAttribute('hidden');
const editorStampButton = doc.getElementById('editorStampButton') as HTMLButtonElement | null;
if (editorStampButton) {
editorStampButton.disabled = false;
}
try {
const highlightBtn = doc.getElementById('editorHighlightButton') as HTMLButtonElement | null;
highlightBtn?.click();
} catch { }
});
}
} catch (e) {
console.error('Could not initialize PDF.js viewer for signing:', e);
}
const saveBtn = document.getElementById('process-btn') as HTMLButtonElement | null;
if (saveBtn) {
saveBtn.style.display = '';
}
try {
const existingPrefsRaw = localStorage.getItem('pdfjs.preferences');
const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {};
delete (existingPrefs as any).annotationEditorMode;
const newPrefs = {
...existingPrefs,
enableSignatureEditor: true,
enablePermissions: false,
};
localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs));
} catch {}
const viewerUrl = new URL(
`${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html`,
window.location.origin
);
const query = new URLSearchParams({ file: signState.blobUrl });
iframe.src = `${viewerUrl.toString()}?${query.toString()}`;
iframe.onload = () => {
hideLoader();
signState.viewerReady = true;
try {
const viewerWindow: any = iframe.contentWindow;
if (viewerWindow && viewerWindow.PDFViewerApplication) {
const app = viewerWindow.PDFViewerApplication;
const doc = viewerWindow.document;
const eventBus = app.eventBus;
eventBus?._on('annotationeditoruimanager', () => {
const editorModeButtons = doc.getElementById('editorModeButtons');
editorModeButtons?.classList.remove('hidden');
const editorSignature = doc.getElementById('editorSignature');
editorSignature?.removeAttribute('hidden');
const editorSignatureButton = doc.getElementById(
'editorSignatureButton'
) as HTMLButtonElement | null;
if (editorSignatureButton) {
editorSignatureButton.disabled = false;
}
const editorStamp = doc.getElementById('editorStamp');
editorStamp?.removeAttribute('hidden');
const editorStampButton = doc.getElementById(
'editorStampButton'
) as HTMLButtonElement | null;
if (editorStampButton) {
editorStampButton.disabled = false;
}
try {
const highlightBtn = doc.getElementById(
'editorHighlightButton'
) as HTMLButtonElement | null;
highlightBtn?.click();
} catch {}
});
}
} catch (e) {
console.error('Could not initialize PDF.js viewer for signing:', e);
}
const saveBtn = document.getElementById(
'process-btn'
) as HTMLButtonElement | null;
if (saveBtn) {
saveBtn.style.display = '';
}
};
}
async function applyAndSaveSignatures() {
if (!signState.viewerReady || !signState.viewerIframe) {
showAlert('Viewer not ready', 'Please wait for the PDF viewer to load.');
return;
if (!signState.viewerReady || !signState.viewerIframe) {
showAlert('Viewer not ready', 'Please wait for the PDF viewer to load.');
return;
}
try {
const viewerWindow: any = signState.viewerIframe.contentWindow;
if (!viewerWindow || !viewerWindow.PDFViewerApplication) {
showAlert('Viewer not ready', 'The PDF viewer is still initializing.');
return;
}
try {
const viewerWindow: any = signState.viewerIframe.contentWindow;
if (!viewerWindow || !viewerWindow.PDFViewerApplication) {
showAlert('Viewer not ready', 'The PDF viewer is still initializing.');
return;
const app = viewerWindow.PDFViewerApplication;
const flattenCheckbox = document.getElementById(
'flatten-signature-toggle'
) as HTMLInputElement | null;
const shouldFlatten = flattenCheckbox?.checked;
if (shouldFlatten) {
showLoader('Flattening and saving PDF...');
const rawPdfBytes = await app.pdfDocument.saveDocument(
app.pdfDocument.annotationStorage
);
const pdfBytes = new Uint8Array(rawPdfBytes);
const pdfDoc = await PDFDocument.load(pdfBytes);
pdfDoc.getForm().flatten();
const flattenedPdfBytes = await pdfDoc.save();
const blob = new Blob([flattenedPdfBytes as BlobPart], {
type: 'application/pdf',
});
downloadFile(
blob,
`signed_flattened_${signState.file?.name || 'document.pdf'}`
);
hideLoader();
showAlert('Success', 'Signed PDF saved successfully!', 'success', () => {
resetState();
});
} else {
app.eventBus?.dispatch('download', { source: app });
showAlert(
'Success',
'Signed PDF downloaded successfully!',
'success',
() => {
resetState();
}
const app = viewerWindow.PDFViewerApplication;
const flattenCheckbox = document.getElementById('flatten-signature-toggle') as HTMLInputElement | null;
const shouldFlatten = flattenCheckbox?.checked;
if (shouldFlatten) {
showLoader('Flattening and saving PDF...');
const rawPdfBytes = await app.pdfDocument.saveDocument(app.pdfDocument.annotationStorage);
const pdfBytes = new Uint8Array(rawPdfBytes);
const pdfDoc = await PDFDocument.load(pdfBytes);
pdfDoc.getForm().flatten();
const flattenedPdfBytes = await pdfDoc.save();
const blob = new Blob([flattenedPdfBytes as BlobPart], { type: 'application/pdf' });
downloadFile(blob, `signed_flattened_${signState.file?.name || 'document.pdf'}`);
hideLoader();
showAlert('Success', 'Signed PDF saved successfully!', 'success', () => {
resetState();
});
} else {
app.eventBus?.dispatch('download', { source: app });
showAlert('Success', 'Signed PDF downloaded successfully!', 'success', () => {
resetState();
});
}
} catch (error) {
console.error('Failed to export the signed PDF:', error);
hideLoader();
showAlert('Export failed', 'Could not export the signed PDF. Please try again.');
);
}
} catch (error) {
console.error('Failed to export the signed PDF:', error);
hideLoader();
showAlert(
'Export failed',
'Could not export the signed PDF. Please try again.'
);
}
}
function resetState() {
cleanup();
signState.file = null;
signState.viewerIframe = null;
signState.viewerReady = false;
cleanup();
signState.file = null;
signState.viewerIframe = null;
signState.viewerReady = false;
const signatureEditor = document.getElementById('signature-editor');
if (signatureEditor) {
signatureEditor.classList.add('hidden');
}
const signatureEditor = document.getElementById('signature-editor');
if (signatureEditor) {
signatureEditor.classList.add('hidden');
}
const container = document.getElementById('canvas-container-sign');
if (container) {
container.textContent = '';
}
const container = document.getElementById('canvas-container-sign');
if (container) {
container.textContent = '';
}
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) {
fileDisplayArea.innerHTML = '';
}
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) {
fileDisplayArea.innerHTML = '';
}
const processBtn = document.getElementById('process-btn') as HTMLButtonElement | null;
if (processBtn) {
processBtn.style.display = 'none';
}
const processBtn = document.getElementById(
'process-btn'
) as HTMLButtonElement | null;
if (processBtn) {
processBtn.style.display = 'none';
}
const flattenCheckbox = document.getElementById('flatten-signature-toggle') as HTMLInputElement | null;
if (flattenCheckbox) {
flattenCheckbox.checked = false;
}
const flattenCheckbox = document.getElementById(
'flatten-signature-toggle'
) as HTMLInputElement | null;
if (flattenCheckbox) {
flattenCheckbox.checked = false;
}
}
function cleanup() {
if (signState.blobUrl) {
URL.revokeObjectURL(signState.blobUrl);
signState.blobUrl = null;
}
if (signState.blobUrl) {
URL.revokeObjectURL(signState.blobUrl);
signState.blobUrl = null;
}
}

View File

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

View File

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

View File

@@ -1,135 +1,199 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { downloadFile, hexToRgb, formatBytes, getPDFDocument, readFileAsArrayBuffer } from '../utils/helpers.js';
import {
downloadFile,
hexToRgb,
formatBytes,
getPDFDocument,
readFileAsArrayBuffer,
} from '../utils/helpers.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
import { TextColorState } from '@/types';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
const pageState: TextColorState = { file: null, pdfDoc: null };
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializePage); }
else { initializePage(); }
function initializePage() {
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const backBtn = document.getElementById('back-to-tools');
const processBtn = document.getElementById('process-btn');
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
fileInput.addEventListener('click', () => { fileInput.value = ''; });
}
if (dropZone) {
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-indigo-500'); });
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('border-indigo-500'); });
dropZone.addEventListener('drop', (e) => {
e.preventDefault(); dropZone.classList.remove('border-indigo-500');
if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files);
});
}
if (backBtn) backBtn.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; });
if (processBtn) processBtn.addEventListener('click', changeTextColor);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
} else {
initializePage();
}
function handleFileUpload(e: Event) { const input = e.target as HTMLInputElement; if (input.files?.length) handleFiles(input.files); }
function initializePage() {
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const backBtn = document.getElementById('back-to-tools');
const processBtn = document.getElementById('process-btn');
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-indigo-500');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('border-indigo-500');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-indigo-500');
if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files);
});
}
if (backBtn)
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
if (processBtn) processBtn.addEventListener('click', changeTextColor);
}
function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement;
if (input.files?.length) handleFiles(input.files);
}
async function handleFiles(files: FileList) {
const file = files[0];
if (!file || file.type !== 'application/pdf') { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; }
const file = files[0];
if (!file || file.type !== 'application/pdf') {
showAlert('Invalid File', 'Please upload a valid PDF file.');
return;
}
try {
const result = await loadPdfWithPasswordPrompt(file);
if (!result) return;
showLoader('Loading PDF...');
try {
const arrayBuffer = await file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer);
pageState.file = file;
updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden');
} catch (error) { console.error(error); showAlert('Error', 'Failed to load PDF file.'); }
finally { hideLoader(); }
result.pdf.destroy();
pageState.pdfDoc = await PDFLibDocument.load(result.bytes);
pageState.file = result.file;
updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden');
} catch (error) {
console.error(error);
showAlert('Error', 'Failed to load PDF file.');
} finally {
hideLoader();
}
}
function updateFileDisplay() {
const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageState.pdfDoc.getPageCount()} pages`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = resetState;
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageState.pdfDoc.getPageCount()} pages`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = resetState;
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
}
function resetState() {
pageState.file = null; pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
document.getElementById('options-panel')?.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
pageState.file = null;
pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
document.getElementById('options-panel')?.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function changeTextColor() {
if (!pageState.pdfDoc || !pageState.file) { showAlert('Error', 'Please upload a PDF file first.'); return; }
const colorHex = (document.getElementById('text-color-input') as HTMLInputElement).value;
const { r, g, b } = hexToRgb(colorHex);
const darknessThreshold = 120;
showLoader('Changing text color...');
try {
const newPdfDoc = await PDFLibDocument.create();
const pdf = await getPDFDocument(await readFileAsArrayBuffer(pageState.file)).promise;
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF file first.');
return;
}
const colorHex = (
document.getElementById('text-color-input') as HTMLInputElement
).value;
const { r, g, b } = hexToRgb(colorHex);
const darknessThreshold = 120;
showLoader('Changing text color...');
try {
const newPdfDoc = await PDFLibDocument.create();
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(pageState.file)
).promise;
for (let i = 1; i <= pdf.numPages; i++) {
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d')!;
await page.render({ canvasContext: context, viewport, canvas }).promise;
for (let i = 1; i <= pdf.numPages; i++) {
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d')!;
await page.render({ canvasContext: context, viewport, canvas }).promise;
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let j = 0; j < data.length; j += 4) {
if (data[j] < darknessThreshold && data[j + 1] < darknessThreshold && data[j + 2] < darknessThreshold) {
data[j] = r * 255;
data[j + 1] = g * 255;
data[j + 2] = b * 255;
}
}
context.putImageData(imageData, 0, 0);
const pngImageBytes = await new Promise<Uint8Array>((resolve) =>
canvas.toBlob((blob) => {
const reader = new FileReader();
reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer));
reader.readAsArrayBuffer(blob!);
}, 'image/png')
);
const pngImage = await newPdfDoc.embedPng(pngImageBytes);
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
newPage.drawImage(pngImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let j = 0; j < data.length; j += 4) {
if (
data[j] < darknessThreshold &&
data[j + 1] < darknessThreshold &&
data[j + 2] < darknessThreshold
) {
data[j] = r * 255;
data[j + 1] = g * 255;
data[j + 2] = b * 255;
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'text-color-changed.pdf');
showAlert('Success', 'Text color changed successfully!', 'success', () => { resetState(); });
} catch (e) { console.error(e); showAlert('Error', 'Could not change text color.'); }
finally { hideLoader(); }
}
context.putImageData(imageData, 0, 0);
const pngImageBytes = await new Promise<Uint8Array>((resolve) =>
canvas.toBlob((blob) => {
const reader = new FileReader();
reader.onload = () =>
resolve(new Uint8Array(reader.result as ArrayBuffer));
reader.readAsArrayBuffer(blob!);
}, 'image/png')
);
const pngImage = await newPdfDoc.embedPng(pngImageBytes);
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
newPage.drawImage(pngImage, {
x: 0,
y: 0,
width: viewport.width,
height: viewport.height,
});
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'text-color-changed.pdf'
);
showAlert('Success', 'Text color changed successfully!', 'success', () => {
resetState();
});
} catch (e) {
console.error(e);
showAlert('Error', 'Could not change text color.');
} finally {
hideLoader();
}
}

View File

@@ -1,360 +1,404 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { formatBytes, formatIsoDate, getPDFDocument } from '../utils/helpers.js';
import { formatBytes, formatIsoDate } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { ViewMetadataState } from '@/types';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
const pageState: ViewMetadataState = {
file: null,
metadata: {},
file: null,
metadata: {},
};
function resetState() {
pageState.file = null;
pageState.metadata = {};
pageState.file = null;
pageState.metadata = {};
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const metadataDisplay = document.getElementById('metadata-display');
if (metadataDisplay) metadataDisplay.innerHTML = '';
const metadataDisplay = document.getElementById('metadata-display');
if (metadataDisplay) metadataDisplay.innerHTML = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
function createSection(title: string): { wrapper: HTMLDivElement; ul: HTMLUListElement } {
const wrapper = document.createElement('div');
wrapper.className = 'mb-6';
const h3 = document.createElement('h3');
h3.className = 'text-lg font-semibold text-white mb-2';
h3.textContent = title;
const ul = document.createElement('ul');
ul.className = 'space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700';
wrapper.append(h3, ul);
return { wrapper, ul };
function createSection(title: string): {
wrapper: HTMLDivElement;
ul: HTMLUListElement;
} {
const wrapper = document.createElement('div');
wrapper.className = 'mb-6';
const h3 = document.createElement('h3');
h3.className = 'text-lg font-semibold text-white mb-2';
h3.textContent = title;
const ul = document.createElement('ul');
ul.className =
'space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700';
wrapper.append(h3, ul);
return { wrapper, ul };
}
function createListItem(key: string, value: string): HTMLLIElement {
const li = document.createElement('li');
li.className = 'flex flex-col sm:flex-row';
const strong = document.createElement('strong');
strong.className = 'w-40 flex-shrink-0 text-gray-400';
strong.textContent = key;
const div = document.createElement('div');
div.className = 'flex-grow text-white break-all';
div.textContent = value;
li.append(strong, div);
return li;
const li = document.createElement('li');
li.className = 'flex flex-col sm:flex-row';
const strong = document.createElement('strong');
strong.className = 'w-40 flex-shrink-0 text-gray-400';
strong.textContent = key;
const div = document.createElement('div');
div.className = 'flex-grow text-white break-all';
div.textContent = value;
li.append(strong, div);
return li;
}
function parsePdfDate(pdfDate: string | unknown): string {
if (!pdfDate || typeof pdfDate !== 'string' || !pdfDate.startsWith('D:')) {
return String(pdfDate || '');
}
try {
const year = pdfDate.substring(2, 6);
const month = pdfDate.substring(6, 8);
const day = pdfDate.substring(8, 10);
const hour = pdfDate.substring(10, 12);
const minute = pdfDate.substring(12, 14);
const second = pdfDate.substring(14, 16);
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`).toLocaleString();
} catch {
return pdfDate;
}
if (!pdfDate || typeof pdfDate !== 'string' || !pdfDate.startsWith('D:')) {
return String(pdfDate || '');
}
try {
const year = pdfDate.substring(2, 6);
const month = pdfDate.substring(6, 8);
const day = pdfDate.substring(8, 10);
const hour = pdfDate.substring(10, 12);
const minute = pdfDate.substring(12, 14);
const second = pdfDate.substring(14, 16);
return new Date(
`${year}-${month}-${day}T${hour}:${minute}:${second}Z`
).toLocaleString();
} catch {
return pdfDate;
}
}
function createXmpListItem(key: string, value: string, indent: number = 0): HTMLLIElement {
const li = document.createElement('li');
li.className = 'flex flex-col sm:flex-row';
function createXmpListItem(
key: string,
value: string,
indent: number = 0
): HTMLLIElement {
const li = document.createElement('li');
li.className = 'flex flex-col sm:flex-row';
const strong = document.createElement('strong');
strong.className = 'w-56 flex-shrink-0 text-gray-400';
strong.textContent = key;
strong.style.paddingLeft = `${indent * 1.2}rem`;
const strong = document.createElement('strong');
strong.className = 'w-56 flex-shrink-0 text-gray-400';
strong.textContent = key;
strong.style.paddingLeft = `${indent * 1.2}rem`;
const div = document.createElement('div');
div.className = 'flex-grow text-white break-all';
div.textContent = value;
const div = document.createElement('div');
div.className = 'flex-grow text-white break-all';
div.textContent = value;
li.append(strong, div);
return li;
li.append(strong, div);
return li;
}
function createXmpHeaderItem(key: string, indent: number = 0): HTMLLIElement {
const li = document.createElement('li');
li.className = 'flex pt-2';
const strong = document.createElement('strong');
strong.className = 'w-full flex-shrink-0 text-gray-300 font-medium';
strong.textContent = key;
strong.style.paddingLeft = `${indent * 1.2}rem`;
li.append(strong);
return li;
const li = document.createElement('li');
li.className = 'flex pt-2';
const strong = document.createElement('strong');
strong.className = 'w-full flex-shrink-0 text-gray-300 font-medium';
strong.textContent = key;
strong.style.paddingLeft = `${indent * 1.2}rem`;
li.append(strong);
return li;
}
function appendXmpNodes(xmlNode: Element, ulElement: HTMLUListElement, indentLevel: number) {
const xmpDateKeys = ['xap:CreateDate', 'xap:ModifyDate', 'xap:MetadataDate'];
function appendXmpNodes(
xmlNode: Element,
ulElement: HTMLUListElement,
indentLevel: number
) {
const xmpDateKeys = ['xap:CreateDate', 'xap:ModifyDate', 'xap:MetadataDate'];
const childNodes = Array.from(xmlNode.children);
const childNodes = Array.from(xmlNode.children);
for (const child of childNodes) {
if (child.nodeType !== 1) continue;
for (const child of childNodes) {
if (child.nodeType !== 1) continue;
let key = child.tagName;
const elementChildren = Array.from(child.children).filter(function (c) {
return c.nodeType === 1;
});
let key = child.tagName;
const elementChildren = Array.from(child.children).filter(function (c) {
return c.nodeType === 1;
});
if (key === 'rdf:li') {
appendXmpNodes(child, ulElement, indentLevel);
continue;
}
if (key === 'rdf:Alt') {
key = '(alt container)';
}
if (child.getAttribute('rdf:parseType') === 'Resource' && elementChildren.length === 0) {
ulElement.appendChild(createXmpListItem(key, '(Empty Resource)', indentLevel));
continue;
}
if (elementChildren.length > 0) {
ulElement.appendChild(createXmpHeaderItem(key, indentLevel));
appendXmpNodes(child, ulElement, indentLevel + 1);
} else {
let value = (child.textContent || '').trim();
if (value) {
if (xmpDateKeys.includes(key)) {
value = formatIsoDate(value);
}
ulElement.appendChild(createXmpListItem(key, value, indentLevel));
}
}
if (key === 'rdf:li') {
appendXmpNodes(child, ulElement, indentLevel);
continue;
}
if (key === 'rdf:Alt') {
key = '(alt container)';
}
if (
child.getAttribute('rdf:parseType') === 'Resource' &&
elementChildren.length === 0
) {
ulElement.appendChild(
createXmpListItem(key, '(Empty Resource)', indentLevel)
);
continue;
}
if (elementChildren.length > 0) {
ulElement.appendChild(createXmpHeaderItem(key, indentLevel));
appendXmpNodes(child, ulElement, indentLevel + 1);
} else {
let value = (child.textContent || '').trim();
if (value) {
if (xmpDateKeys.includes(key)) {
value = formatIsoDate(value);
}
ulElement.appendChild(createXmpListItem(key, value, indentLevel));
}
}
}
}
async function displayMetadata() {
const metadataDisplay = document.getElementById('metadata-display');
if (!metadataDisplay || !pageState.file) return;
const metadataDisplay = document.getElementById('metadata-display');
if (!metadataDisplay || !pageState.file) return;
metadataDisplay.innerHTML = '';
pageState.metadata = {};
metadataDisplay.innerHTML = '';
pageState.metadata = {};
try {
const result = await loadPdfWithPasswordPrompt(pageState.file);
if (!result) return;
showLoader('Analyzing full PDF metadata...');
const { pdf: pdfjsDoc, file: currentFile } = result;
pageState.file = currentFile;
try {
const pdfBytes = await pageState.file.arrayBuffer();
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
const [metadataResult, fieldObjects] = await Promise.all([
pdfjsDoc.getMetadata(),
pdfjsDoc.getFieldObjects(),
]);
const [metadataResult, fieldObjects] = await Promise.all([
pdfjsDoc.getMetadata(),
pdfjsDoc.getFieldObjects(),
]);
const { info, metadata } = metadataResult;
const rawXmpString = metadata ? metadata.getRaw() : null;
const { info, metadata } = metadataResult;
const rawXmpString = metadata ? metadata.getRaw() : null;
// Info Dictionary Section
const infoSection = createSection('Info Dictionary');
if (info && Object.keys(info).length > 0) {
for (const key in info) {
const value = (info as Record<string, unknown>)[key];
let displayValue: string;
// Info Dictionary Section
const infoSection = createSection('Info Dictionary');
if (info && Object.keys(info).length > 0) {
for (const key in info) {
const value = (info as Record<string, unknown>)[key];
let displayValue: string;
if (value === null || typeof value === 'undefined') {
displayValue = '- Not Set -';
} else if (typeof value === 'object' && value !== null && 'name' in value) {
displayValue = String((value as { name: string }).name);
} else if (typeof value === 'object') {
try {
displayValue = JSON.stringify(value);
} catch {
displayValue = '[object Object]';
}
} else if ((key === 'CreationDate' || key === 'ModDate') && typeof value === 'string') {
displayValue = parsePdfDate(value);
} else {
displayValue = String(value);
}
pageState.metadata[key] = displayValue;
infoSection.ul.appendChild(createListItem(key, displayValue));
}
if (value === null || typeof value === 'undefined') {
displayValue = '- Not Set -';
} else if (
typeof value === 'object' &&
value !== null &&
'name' in value
) {
displayValue = String((value as { name: string }).name);
} else if (typeof value === 'object') {
try {
displayValue = JSON.stringify(value);
} catch {
displayValue = '[object Object]';
}
} else if (
(key === 'CreationDate' || key === 'ModDate') &&
typeof value === 'string'
) {
displayValue = parsePdfDate(value);
} else {
infoSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
displayValue = String(value);
}
metadataDisplay.appendChild(infoSection.wrapper);
// Interactive Form Fields Section
const fieldsSection = createSection('Interactive Form Fields');
if (fieldObjects && Object.keys(fieldObjects).length > 0) {
for (const fieldName in fieldObjects) {
const field = (fieldObjects as Record<string, Array<{ fieldValue?: unknown }>>)[fieldName][0];
const value = field.fieldValue || '- Not Set -';
fieldsSection.ul.appendChild(createListItem(fieldName, String(value)));
}
} else {
fieldsSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No interactive form fields found -</span></li>`;
}
metadataDisplay.appendChild(fieldsSection.wrapper);
// XMP Metadata Section
const xmpSection = createSection('XMP Metadata');
if (rawXmpString) {
try {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(rawXmpString, 'application/xml');
const descriptions = xmlDoc.getElementsByTagName('rdf:Description');
if (descriptions.length > 0) {
for (let i = 0; i < descriptions.length; i++) {
appendXmpNodes(descriptions[i], xmpSection.ul, 0);
}
} else {
appendXmpNodes(xmlDoc.documentElement, xmpSection.ul, 0);
}
if (xmpSection.ul.children.length === 0) {
xmpSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No parseable XMP properties found -</span></li>`;
}
} catch (xmlError) {
console.error('Failed to parse XMP XML:', xmlError);
xmpSection.ul.innerHTML = `<li><span class="text-red-500 italic">- Error parsing XMP XML. Displaying raw. -</span></li>`;
const pre = document.createElement('pre');
pre.className = 'text-xs text-gray-300 whitespace-pre-wrap break-all';
pre.textContent = rawXmpString;
xmpSection.ul.appendChild(pre);
}
} else {
xmpSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No XMP metadata found -</span></li>`;
}
metadataDisplay.appendChild(xmpSection.wrapper);
createIcons({ icons });
} catch (e) {
console.error('Failed to view metadata or fields:', e);
showAlert('Error', 'Could not fully analyze the PDF. It may be corrupted or have an unusual structure.');
} finally {
hideLoader();
pageState.metadata[key] = displayValue;
infoSection.ul.appendChild(createListItem(key, displayValue));
}
} else {
infoSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
}
metadataDisplay.appendChild(infoSection.wrapper);
// Interactive Form Fields Section
const fieldsSection = createSection('Interactive Form Fields');
if (fieldObjects && Object.keys(fieldObjects).length > 0) {
for (const fieldName in fieldObjects) {
const field = (
fieldObjects as Record<string, Array<{ fieldValue?: unknown }>>
)[fieldName][0];
const value = field.fieldValue || '- Not Set -';
fieldsSection.ul.appendChild(createListItem(fieldName, String(value)));
}
} else {
fieldsSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No interactive form fields found -</span></li>`;
}
metadataDisplay.appendChild(fieldsSection.wrapper);
// XMP Metadata Section
const xmpSection = createSection('XMP Metadata');
if (rawXmpString) {
try {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(rawXmpString, 'application/xml');
const descriptions = xmlDoc.getElementsByTagName('rdf:Description');
if (descriptions.length > 0) {
for (let i = 0; i < descriptions.length; i++) {
appendXmpNodes(descriptions[i], xmpSection.ul, 0);
}
} else {
appendXmpNodes(xmlDoc.documentElement, xmpSection.ul, 0);
}
if (xmpSection.ul.children.length === 0) {
xmpSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No parseable XMP properties found -</span></li>`;
}
} catch (xmlError) {
console.error('Failed to parse XMP XML:', xmlError);
xmpSection.ul.innerHTML = `<li><span class="text-red-500 italic">- Error parsing XMP XML. Displaying raw. -</span></li>`;
const pre = document.createElement('pre');
pre.className = 'text-xs text-gray-300 whitespace-pre-wrap break-all';
pre.textContent = rawXmpString;
xmpSection.ul.appendChild(pre);
}
} else {
xmpSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No XMP metadata found -</span></li>`;
}
metadataDisplay.appendChild(xmpSection.wrapper);
pdfjsDoc.destroy();
createIcons({ icons });
} catch (e) {
console.error('Failed to view metadata or fields:', e);
showAlert(
'Error',
'Could not fully analyze the PDF. It may be corrupted or have an unusual structure.'
);
} finally {
hideLoader();
}
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)}`;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)}`;
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
await displayMetadata();
await displayMetadata();
if (toolOptions) toolOptions.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
if (toolOptions) toolOptions.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
function copyMetadataAsJson() {
const jsonString = JSON.stringify(pageState.metadata, null, 2);
navigator.clipboard.writeText(jsonString).then(function () {
showAlert('Copied', 'Metadata copied to clipboard as JSON.');
}).catch(function (err) {
console.error('Failed to copy:', err);
showAlert('Error', 'Failed to copy metadata to clipboard.');
const jsonString = JSON.stringify(pageState.metadata, null, 2);
navigator.clipboard
.writeText(jsonString)
.then(function () {
showAlert('Copied', 'Metadata copied to clipboard as JSON.');
})
.catch(function (err) {
console.error('Failed to copy:', err);
showAlert('Error', 'Failed to copy metadata to clipboard.');
});
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
if (files && files.length > 0) {
const file = files[0];
if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const copyBtn = document.getElementById('copy-metadata');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const copyBtn = document.getElementById('copy-metadata');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
});
}
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (copyBtn) {
copyBtn.addEventListener('click', copyMetadataAsJson);
}
if (copyBtn) {
copyBtn.addEventListener('click', copyMetadataAsJson);
}
});

View File

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

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