Add password prompt functionality and tests while uploading encrypted PDF
This commit is contained in:
67
index.html
67
index.html
@@ -863,6 +863,73 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="password-modal"
|
||||||
|
class="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 hidden items-center justify-center p-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-gray-800 rounded-xl border border-gray-700 shadow-2xl max-w-md w-full overflow-hidden"
|
||||||
|
>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex items-start gap-4 mb-4">
|
||||||
|
<div
|
||||||
|
class="w-12 h-12 flex items-center justify-center rounded-full bg-indigo-500/10 flex-shrink-0"
|
||||||
|
>
|
||||||
|
<i data-lucide="lock" class="w-6 h-6 text-indigo-400"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3
|
||||||
|
id="password-modal-title"
|
||||||
|
class="text-xl font-bold text-white mb-1"
|
||||||
|
>
|
||||||
|
Password Required
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
id="password-modal-filename"
|
||||||
|
class="text-gray-400 text-sm truncate"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password-modal-input"
|
||||||
|
class="w-full bg-gray-700 border border-gray-600 text-gray-200 rounded-lg px-4 py-2.5 pr-10 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||||
|
placeholder="Enter password"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
id="password-modal-toggle"
|
||||||
|
type="button"
|
||||||
|
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-200"
|
||||||
|
>
|
||||||
|
<i data-lucide="eye" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
id="password-modal-error"
|
||||||
|
class="text-xs text-red-400 mt-2 hidden"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 p-4 border-t border-gray-700">
|
||||||
|
<button
|
||||||
|
id="password-modal-cancel"
|
||||||
|
class="flex-1 px-4 py-2.5 bg-gray-700 hover:bg-gray-600 text-white rounded-lg font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Skip
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="password-modal-submit"
|
||||||
|
class="flex-1 px-4 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Unlock
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="section-divider hide-section mb-20 mt-10"></div>
|
<div class="section-divider hide-section mb-20 mt-10"></div>
|
||||||
|
|
||||||
<!-- COMPLIANCE SECTION START -->
|
<!-- COMPLIANCE SECTION START -->
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
|||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
import { makeUniqueFileKey } from '../utils/deduplicate-filename.js';
|
import { makeUniqueFileKey } from '../utils/deduplicate-filename.js';
|
||||||
|
import {
|
||||||
|
promptAndDecryptFile,
|
||||||
|
handleEncryptedFiles,
|
||||||
|
} from '../utils/password-prompt.js';
|
||||||
import {
|
import {
|
||||||
multiFileTools,
|
multiFileTools,
|
||||||
simpleTools,
|
simpleTools,
|
||||||
@@ -31,7 +35,6 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
|||||||
import.meta.url
|
import.meta.url
|
||||||
).toString();
|
).toString();
|
||||||
|
|
||||||
// Re-export rotation state utilities
|
|
||||||
export {
|
export {
|
||||||
getRotationState,
|
getRotationState,
|
||||||
updateRotationState,
|
updateRotationState,
|
||||||
@@ -43,10 +46,9 @@ const rotationState: number[] = [];
|
|||||||
let imageSortableInstance: Sortable | null = null;
|
let imageSortableInstance: Sortable | null = null;
|
||||||
const activeImageUrls = new Map<File, string>();
|
const activeImageUrls = new Map<File, string>();
|
||||||
|
|
||||||
async function handleSinglePdfUpload(toolId, file) {
|
async function handleSinglePdfUpload(toolId: string, file: File) {
|
||||||
showLoader('Loading PDF...');
|
showLoader('Loading PDF...');
|
||||||
try {
|
try {
|
||||||
// For form-filler, bypass pdf-lib (can't handle XFA) and use PDF.js
|
|
||||||
if (toolId === 'form-filler') {
|
if (toolId === 'form-filler') {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
@@ -80,12 +82,14 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
toolId !== 'change-permissions' &&
|
toolId !== 'change-permissions' &&
|
||||||
toolId !== 'remove-restrictions'
|
toolId !== 'remove-restrictions'
|
||||||
) {
|
) {
|
||||||
showAlert(
|
const decryptedFile = await promptAndDecryptFile(file);
|
||||||
'Protected PDF',
|
if (!decryptedFile) {
|
||||||
'This PDF is password-protected. Please use the Decrypt or Change Permissions tool first.'
|
switchView('grid');
|
||||||
);
|
return;
|
||||||
switchView('grid');
|
}
|
||||||
return;
|
const decryptedBytes = await readFileAsArrayBuffer(decryptedFile);
|
||||||
|
state.pdfDoc = await PDFLibDocument.load(decryptedBytes as ArrayBuffer);
|
||||||
|
state.files = [decryptedFile];
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionsDiv = document.querySelector(
|
const optionsDiv = document.querySelector(
|
||||||
@@ -127,7 +131,6 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
await renderPageThumbnails(toolId, state.pdfDoc);
|
await renderPageThumbnails(toolId, state.pdfDoc);
|
||||||
|
|
||||||
if (toolId === 'rotate') {
|
if (toolId === 'rotate') {
|
||||||
// Initialize rotation state for all pages
|
|
||||||
rotationState.length = 0;
|
rotationState.length = 0;
|
||||||
for (let i = 0; i < state.pdfDoc.getPageCount(); i++) {
|
for (let i = 0; i < state.pdfDoc.getPageCount(); i++) {
|
||||||
rotationState.push(0);
|
rotationState.push(0);
|
||||||
@@ -157,12 +160,10 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
const rotateAll = (angle: number) => {
|
const rotateAll = (angle: number) => {
|
||||||
// Update rotation state for ALL pages (including unrendered ones)
|
|
||||||
for (let i = 0; i < rotationState.length; i++) {
|
for (let i = 0; i < rotationState.length; i++) {
|
||||||
rotationState[i] = rotationState[i] + angle;
|
rotationState[i] = rotationState[i] + angle;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update DOM for currently rendered pages
|
|
||||||
document.querySelectorAll('.page-rotator-item').forEach((item) => {
|
document.querySelectorAll('.page-rotator-item').forEach((item) => {
|
||||||
const pageIndex = parseInt(
|
const pageIndex = parseInt(
|
||||||
(item as HTMLElement).dataset.pageIndex || '0'
|
(item as HTMLElement).dataset.pageIndex || '0'
|
||||||
@@ -236,7 +237,7 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
|
|
||||||
resultsDiv.textContent = ''; // Clear safely
|
resultsDiv.textContent = ''; // Clear safely
|
||||||
|
|
||||||
const createSection = (title) => {
|
const createSection = (title: string) => {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'mb-4';
|
wrapper.className = 'mb-4';
|
||||||
const h3 = document.createElement('h3');
|
const h3 = document.createElement('h3');
|
||||||
@@ -249,7 +250,7 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
return { wrapper, ul };
|
return { wrapper, ul };
|
||||||
};
|
};
|
||||||
|
|
||||||
const createListItem = (key, value) => {
|
const createListItem = (key: string, value: string) => {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = 'flex flex-col sm:flex-row';
|
li.className = 'flex flex-col sm:flex-row';
|
||||||
const strong = document.createElement('strong');
|
const strong = document.createElement('strong');
|
||||||
@@ -262,13 +263,8 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
return li;
|
return li;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsePdfDate = (pdfDate) => {
|
const parsePdfDate = (pdfDate: string): string => {
|
||||||
if (
|
if (!pdfDate || !pdfDate.startsWith('D:')) return pdfDate;
|
||||||
!pdfDate ||
|
|
||||||
typeof pdfDate !== 'string' ||
|
|
||||||
!pdfDate.startsWith('D:')
|
|
||||||
)
|
|
||||||
return pdfDate;
|
|
||||||
try {
|
try {
|
||||||
const year = pdfDate.substring(2, 6);
|
const year = pdfDate.substring(2, 6);
|
||||||
const month = pdfDate.substring(6, 8);
|
const month = pdfDate.substring(6, 8);
|
||||||
@@ -319,8 +315,8 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
const fieldsSection = createSection('Interactive Form Fields');
|
const fieldsSection = createSection('Interactive Form Fields');
|
||||||
if (fieldObjects && Object.keys(fieldObjects).length > 0) {
|
if (fieldObjects && Object.keys(fieldObjects).length > 0) {
|
||||||
for (const fieldName in fieldObjects) {
|
for (const fieldName in fieldObjects) {
|
||||||
const field = fieldObjects[fieldName][0];
|
const field = fieldObjects[fieldName][0] as Record<string, unknown>;
|
||||||
const value = (field as any).fieldValue || '- Not Set -';
|
const value = field.fieldValue || '- Not Set -';
|
||||||
fieldsSection.ul.appendChild(
|
fieldsSection.ul.appendChild(
|
||||||
createListItem(fieldName, String(value))
|
createListItem(fieldName, String(value))
|
||||||
);
|
);
|
||||||
@@ -330,7 +326,7 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
}
|
}
|
||||||
resultsDiv.appendChild(fieldsSection.wrapper);
|
resultsDiv.appendChild(fieldsSection.wrapper);
|
||||||
|
|
||||||
const createXmpListItem = (key, value, indent = 0) => {
|
const createXmpListItem = (key: string, value: string, indent = 0) => {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = 'flex flex-col sm:flex-row';
|
li.className = 'flex flex-col sm:flex-row';
|
||||||
|
|
||||||
@@ -347,7 +343,7 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
return li;
|
return li;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createXmpHeaderItem = (key, indent = 0) => {
|
const createXmpHeaderItem = (key: string, indent = 0) => {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = 'flex pt-2';
|
li.className = 'flex pt-2';
|
||||||
const strong = document.createElement('strong');
|
const strong = document.createElement('strong');
|
||||||
@@ -358,7 +354,11 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
return li;
|
return li;
|
||||||
};
|
};
|
||||||
|
|
||||||
const appendXmpNodes = (xmlNode, ulElement, indentLevel) => {
|
const appendXmpNodes = (
|
||||||
|
xmlNode: Element,
|
||||||
|
ulElement: HTMLUListElement,
|
||||||
|
indentLevel: number
|
||||||
|
) => {
|
||||||
const xmpDateKeys = [
|
const xmpDateKeys = [
|
||||||
'xap:CreateDate',
|
'xap:CreateDate',
|
||||||
'xap:ModifyDate',
|
'xap:ModifyDate',
|
||||||
@@ -368,12 +368,12 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
const childNodes = Array.from(xmlNode.children);
|
const childNodes = Array.from(xmlNode.children);
|
||||||
|
|
||||||
for (const child of childNodes) {
|
for (const child of childNodes) {
|
||||||
if ((child as Element).nodeType !== 1) continue;
|
if (child.nodeType !== 1) continue;
|
||||||
|
|
||||||
let key = (child as Element).tagName;
|
let key = child.tagName;
|
||||||
const elementChildren = Array.from(
|
const elementChildren = Array.from(child.children).filter(
|
||||||
(child as Element).children
|
(c) => c.nodeType === 1
|
||||||
).filter((c) => c.nodeType === 1);
|
);
|
||||||
|
|
||||||
if (key === 'rdf:li') {
|
if (key === 'rdf:li') {
|
||||||
appendXmpNodes(child, ulElement, indentLevel);
|
appendXmpNodes(child, ulElement, indentLevel);
|
||||||
@@ -384,7 +384,7 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(child as Element).getAttribute('rdf:parseType') === 'Resource' &&
|
child.getAttribute('rdf:parseType') === 'Resource' &&
|
||||||
elementChildren.length === 0
|
elementChildren.length === 0
|
||||||
) {
|
) {
|
||||||
ulElement.appendChild(
|
ulElement.appendChild(
|
||||||
@@ -397,7 +397,7 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
ulElement.appendChild(createXmpHeaderItem(key, indentLevel));
|
ulElement.appendChild(createXmpHeaderItem(key, indentLevel));
|
||||||
appendXmpNodes(child, ulElement, indentLevel + 1);
|
appendXmpNodes(child, ulElement, indentLevel + 1);
|
||||||
} else {
|
} else {
|
||||||
let value = (child as Element).textContent.trim();
|
let value = (child.textContent ?? '').trim();
|
||||||
if (value) {
|
if (value) {
|
||||||
if (xmpDateKeys.includes(key)) {
|
if (xmpDateKeys.includes(key)) {
|
||||||
value = formatIsoDate(value);
|
value = formatIsoDate(value);
|
||||||
@@ -462,9 +462,9 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
const container = document.getElementById('custom-metadata-container');
|
const container = document.getElementById('custom-metadata-container');
|
||||||
const addBtn = document.getElementById('add-custom-meta-btn');
|
const addBtn = document.getElementById('add-custom-meta-btn');
|
||||||
|
|
||||||
const formatDateForInput = (date) => {
|
const formatDateForInput = (date: Date | undefined) => {
|
||||||
if (!date) return '';
|
if (!date) return '';
|
||||||
const pad = (num) => num.toString().padStart(2, '0');
|
const pad = (num: number) => num.toString().padStart(2, '0');
|
||||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -529,7 +529,6 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
toolLogic['page-dimensions']();
|
toolLogic['page-dimensions']();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup quality sliders for image conversion tools
|
|
||||||
if (toolId === 'pdf-to-jpg') {
|
if (toolId === 'pdf-to-jpg') {
|
||||||
const qualitySlider = document.getElementById(
|
const qualitySlider = document.getElementById(
|
||||||
'jpg-quality'
|
'jpg-quality'
|
||||||
@@ -585,7 +584,7 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMultiFileUpload(toolId) {
|
async function handleMultiFileUpload(toolId: string) {
|
||||||
if (
|
if (
|
||||||
toolId === 'merge' ||
|
toolId === 'merge' ||
|
||||||
toolId === 'alternate-merge' ||
|
toolId === 'alternate-merge' ||
|
||||||
@@ -615,24 +614,44 @@ async function handleMultiFileUpload(toolId) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const foundEncryptedPDFs = pdfFilesLoaded.filter(
|
const encryptedIndices: number[] = [];
|
||||||
(pdf) => pdf.pdfDoc.isEncrypted
|
pdfFilesLoaded.forEach((pdf, index) => {
|
||||||
);
|
if (pdf.pdfDoc.isEncrypted) {
|
||||||
|
encryptedIndices.push(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (foundEncryptedPDFs.length > 0) {
|
if (encryptedIndices.length > 0) {
|
||||||
const encryptedPDFFileNames = [];
|
hideLoader();
|
||||||
foundEncryptedPDFs.forEach((encryptedPDF) => {
|
const decryptedFiles = await handleEncryptedFiles(
|
||||||
encryptedPDFFileNames.push(encryptedPDF.file.name);
|
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
|
const skippedFiles = new Set(
|
||||||
showAlert('Protected PDFs', errorMessage);
|
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') {
|
if (toolId === 'alternate-merge') {
|
||||||
toolLogic['alternate-merge'].setup();
|
toolLogic['alternate-merge'].setup();
|
||||||
} else if (toolId === 'image-to-pdf') {
|
} else if (toolId === 'image-to-pdf') {
|
||||||
@@ -791,12 +806,12 @@ async function handleMultiFileUpload(toolId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupFileInputHandler(toolId) {
|
export function setupFileInputHandler(toolId: string) {
|
||||||
const fileInput = document.getElementById('file-input');
|
const fileInput = document.getElementById('file-input');
|
||||||
const isMultiFileTool = multiFileTools.includes(toolId);
|
const isMultiFileTool = multiFileTools.includes(toolId);
|
||||||
let isFirstUpload = true;
|
let isFirstUpload = true;
|
||||||
|
|
||||||
const processFiles = async (newFiles) => {
|
const processFiles = async (newFiles: File[]) => {
|
||||||
if (newFiles.length === 0) return;
|
if (newFiles.length === 0) return;
|
||||||
|
|
||||||
if (toolId === 'image-to-pdf') {
|
if (toolId === 'image-to-pdf') {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
showWasmRequiredDialog,
|
showWasmRequiredDialog,
|
||||||
WasmProvider,
|
WasmProvider,
|
||||||
} from '../utils/wasm-provider.js';
|
} from '../utils/wasm-provider.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
const worker = new Worker(
|
const worker = new Worker(
|
||||||
import.meta.env.BASE_URL + 'workers/add-attachments.worker.js'
|
import.meta.env.BASE_URL + 'workers/add-attachments.worker.js'
|
||||||
@@ -129,13 +130,16 @@ async function updateUI() {
|
|||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const result = await loadPdfWithPasswordPrompt(pageState.file);
|
||||||
|
if (!result) {
|
||||||
|
resetState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.pdf.destroy();
|
||||||
|
pageState.file = result.file;
|
||||||
showLoader('Loading PDF...');
|
showLoader('Loading PDF...');
|
||||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
|
||||||
|
|
||||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
pageState.pdfDoc = await PDFLibDocument.load(result.bytes);
|
||||||
ignoreEncryption: true,
|
|
||||||
throwOnInvalidObject: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageCount = pageState.pdfDoc.getPageCount();
|
const pageCount = pageState.pdfDoc.getPageCount();
|
||||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||||
@@ -269,10 +273,13 @@ async function addAttachments() {
|
|||||||
|
|
||||||
const transferables = [pdfBuffer, ...attachmentBuffers];
|
const transferables = [pdfBuffer, ...attachmentBuffers];
|
||||||
worker.postMessage(message, transferables);
|
worker.postMessage(message, transferables);
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
console.error('Error attaching files:', error);
|
console.error('Error attaching files:', error);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', `Failed to attach files: ${error.message}`);
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`Failed to attach files: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,230 +2,274 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import { AddBlankPageState } from '@/types';
|
import { AddBlankPageState } from '@/types';
|
||||||
|
|
||||||
|
|
||||||
const pageState: AddBlankPageState = {
|
const pageState: AddBlankPageState = {
|
||||||
file: null,
|
file: null,
|
||||||
pdfDoc: null,
|
pdfDoc: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
pageState.file = null;
|
pageState.file = null;
|
||||||
pageState.pdfDoc = null;
|
pageState.pdfDoc = null;
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
|
|
||||||
const pagePositionInput = document.getElementById('page-position') as HTMLInputElement;
|
const pagePositionInput = document.getElementById(
|
||||||
if (pagePositionInput) pagePositionInput.value = '0';
|
'page-position'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
if (pagePositionInput) pagePositionInput.value = '0';
|
||||||
|
|
||||||
const pageCountInput = document.getElementById('page-count') as HTMLInputElement;
|
const pageCountInput = document.getElementById(
|
||||||
if (pageCountInput) pageCountInput.value = '1';
|
'page-count'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
if (pageCountInput) pageCountInput.value = '1';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUI() {
|
async function updateUI() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
const pagePositionHint = document.getElementById('page-position-hint');
|
const pagePositionHint = document.getElementById('page-position-hint');
|
||||||
const pagePositionInput = document.getElementById('page-position') as HTMLInputElement;
|
const pagePositionInput = document.getElementById(
|
||||||
|
'page-position'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
|
||||||
if (!fileDisplayArea) return;
|
if (!fileDisplayArea) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (pageState.file) {
|
if (pageState.file) {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = pageState.file.name;
|
nameSpan.textContent = pageState.file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
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.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.onclick = function () {
|
removeBtn.onclick = function () {
|
||||||
resetState();
|
resetState();
|
||||||
};
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
// Load PDF document
|
// Load PDF document
|
||||||
try {
|
try {
|
||||||
showLoader('Loading PDF...');
|
const result = await loadPdfWithPasswordPrompt(pageState.file);
|
||||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
if (!result) {
|
||||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
resetState();
|
||||||
ignoreEncryption: true,
|
return;
|
||||||
throwOnInvalidObject: false
|
}
|
||||||
});
|
showLoader('Loading PDF...');
|
||||||
hideLoader();
|
pageState.file = result.file;
|
||||||
|
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||||
|
throwOnInvalidObject: false,
|
||||||
|
});
|
||||||
|
result.pdf.destroy();
|
||||||
|
hideLoader();
|
||||||
|
|
||||||
const pageCount = pageState.pdfDoc.getPageCount();
|
const pageCount = pageState.pdfDoc.getPageCount();
|
||||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||||
|
|
||||||
if (pagePositionHint) {
|
if (pagePositionHint) {
|
||||||
pagePositionHint.textContent = `Enter 0 to insert at the beginning, or ${pageCount} to insert at the end.`;
|
pagePositionHint.textContent = `Enter 0 to insert at the beginning, or ${pageCount} to insert at the end.`;
|
||||||
}
|
}
|
||||||
if (pagePositionInput) {
|
if (pagePositionInput) {
|
||||||
pagePositionInput.max = pageCount.toString();
|
pagePositionInput.max = pageCount.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading PDF:', error);
|
console.error('Error loading PDF:', error);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', 'Failed to load PDF file.');
|
showAlert('Error', 'Failed to load PDF file.');
|
||||||
resetState();
|
resetState();
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addBlankPages() {
|
async function addBlankPages() {
|
||||||
if (!pageState.pdfDoc || !pageState.file) {
|
if (!pageState.pdfDoc || !pageState.file) {
|
||||||
showAlert('Error', 'Please upload a PDF first.');
|
showAlert('Error', 'Please upload a PDF first.');
|
||||||
return;
|
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;
|
// Add the specified number of blank pages
|
||||||
const pageCountInput = document.getElementById('page-count') as HTMLInputElement;
|
for (let i = 0; i < insertCount; i++) {
|
||||||
|
newPdf.addPage([width, height]);
|
||||||
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) {
|
if (indicesAfter.length > 0) {
|
||||||
showAlert('Invalid Input', 'Please enter a valid number of pages (1 or more).');
|
const copied = await newPdf.copyPages(pageState.pdfDoc, indicesAfter);
|
||||||
return;
|
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 {
|
downloadFile(
|
||||||
const newPdf = await PDFLibDocument.create();
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
const { width, height } = pageState.pdfDoc.getPage(0).getSize();
|
`${originalName}_blank-pages-added.pdf`
|
||||||
const allIndices = Array.from({ length: totalPages }, function (_, i) { return i; });
|
);
|
||||||
|
|
||||||
const indicesBefore = allIndices.slice(0, position);
|
showAlert(
|
||||||
const indicesAfter = allIndices.slice(position);
|
'Success',
|
||||||
|
`Added ${insertCount} blank page${insertCount > 1 ? 's' : ''} successfully!`,
|
||||||
if (indicesBefore.length > 0) {
|
'success',
|
||||||
const copied = await newPdf.copyPages(pageState.pdfDoc, indicesBefore);
|
function () {
|
||||||
copied.forEach(function (p) { newPdf.addPage(p); });
|
resetState();
|
||||||
}
|
}
|
||||||
|
);
|
||||||
// Add the specified number of blank pages
|
} catch (e) {
|
||||||
for (let i = 0; i < insertCount; i++) {
|
console.error(e);
|
||||||
newPdf.addPage([width, height]);
|
showAlert(
|
||||||
}
|
'Error',
|
||||||
|
`Could not add blank page${insertCount > 1 ? 's' : ''}.`
|
||||||
if (indicesAfter.length > 0) {
|
);
|
||||||
const copied = await newPdf.copyPages(pageState.pdfDoc, indicesAfter);
|
} finally {
|
||||||
copied.forEach(function (p) { newPdf.addPage(p); });
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileSelect(files: FileList | null) {
|
function handleFileSelect(files: FileList | null) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
if (
|
||||||
pageState.file = file;
|
file.type === 'application/pdf' ||
|
||||||
updateUI();
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
}
|
) {
|
||||||
|
pageState.file = file;
|
||||||
|
updateUI();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', function () {
|
backBtn.addEventListener('click', function () {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
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('click', function () {
|
||||||
fileInput.addEventListener('change', function (e) {
|
fileInput.value = '';
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', function (e) {
|
if (processBtn) {
|
||||||
e.preventDefault();
|
processBtn.addEventListener('click', addBlankPages);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,290 +1,342 @@
|
|||||||
import { formatBytes, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers'
|
import {
|
||||||
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'
|
formatBytes,
|
||||||
import { createIcons, icons } from 'lucide'
|
readFileAsArrayBuffer,
|
||||||
|
getPDFDocument,
|
||||||
|
} from '../utils/helpers';
|
||||||
|
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
||||||
|
import { createIcons, icons } from 'lucide';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
let selectedFile: File | null = null
|
let selectedFile: File | null = null;
|
||||||
let viewerIframe: HTMLIFrameElement | null = null
|
let viewerIframe: HTMLIFrameElement | null = null;
|
||||||
let viewerReady = false
|
let viewerReady = false;
|
||||||
let currentBlobUrl: string | null = null
|
let currentBlobUrl: string | null = null;
|
||||||
|
|
||||||
const pdfInput = document.getElementById('pdfFile') as HTMLInputElement
|
const pdfInput = document.getElementById('pdfFile') as HTMLInputElement;
|
||||||
const fileListDiv = document.getElementById('fileList') as HTMLDivElement
|
const fileListDiv = document.getElementById('fileList') as HTMLDivElement;
|
||||||
const viewerContainer = document.getElementById('stamp-viewer-container') as HTMLDivElement
|
const viewerContainer = document.getElementById(
|
||||||
const viewerCard = document.getElementById('viewer-card') as HTMLDivElement | null
|
'stamp-viewer-container'
|
||||||
const saveStampedBtn = document.getElementById('save-stamped-btn') as HTMLButtonElement
|
) as HTMLDivElement;
|
||||||
const backToToolsBtn = document.getElementById('back-to-tools') as HTMLButtonElement | null
|
const viewerCard = document.getElementById(
|
||||||
const toolUploader = document.getElementById('tool-uploader') as HTMLDivElement | null
|
'viewer-card'
|
||||||
const usernameInput = document.getElementById('stamp-username') as HTMLInputElement | null
|
) as HTMLDivElement | null;
|
||||||
|
const saveStampedBtn = document.getElementById(
|
||||||
|
'save-stamped-btn'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
const backToToolsBtn = document.getElementById(
|
||||||
|
'back-to-tools'
|
||||||
|
) as HTMLButtonElement | null;
|
||||||
|
const toolUploader = document.getElementById(
|
||||||
|
'tool-uploader'
|
||||||
|
) as HTMLDivElement | null;
|
||||||
|
const usernameInput = document.getElementById(
|
||||||
|
'stamp-username'
|
||||||
|
) as HTMLInputElement | null;
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
selectedFile = null
|
selectedFile = null;
|
||||||
if (currentBlobUrl) {
|
if (currentBlobUrl) {
|
||||||
URL.revokeObjectURL(currentBlobUrl)
|
URL.revokeObjectURL(currentBlobUrl);
|
||||||
currentBlobUrl = null
|
currentBlobUrl = null;
|
||||||
}
|
}
|
||||||
if (viewerIframe && viewerContainer && viewerIframe.parentElement === viewerContainer) {
|
if (
|
||||||
viewerContainer.removeChild(viewerIframe)
|
viewerIframe &&
|
||||||
|
viewerContainer &&
|
||||||
|
viewerIframe.parentElement === viewerContainer
|
||||||
|
) {
|
||||||
|
viewerContainer.removeChild(viewerIframe);
|
||||||
}
|
}
|
||||||
viewerIframe = null
|
viewerIframe = null;
|
||||||
viewerReady = false
|
viewerReady = false;
|
||||||
if (viewerCard) viewerCard.classList.add('hidden')
|
if (viewerCard) viewerCard.classList.add('hidden');
|
||||||
if (saveStampedBtn) saveStampedBtn.classList.add('hidden')
|
if (saveStampedBtn) saveStampedBtn.classList.add('hidden');
|
||||||
|
|
||||||
if (viewerContainer) {
|
if (viewerContainer) {
|
||||||
viewerContainer.style.height = ''
|
viewerContainer.style.height = '';
|
||||||
viewerContainer.style.aspectRatio = ''
|
viewerContainer.style.aspectRatio = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false'
|
const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
|
||||||
if (toolUploader && !isFullWidth) {
|
if (toolUploader && !isFullWidth) {
|
||||||
toolUploader.classList.remove('max-w-6xl')
|
toolUploader.classList.remove('max-w-6xl');
|
||||||
toolUploader.classList.add('max-w-2xl')
|
toolUploader.classList.add('max-w-2xl');
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFileList()
|
updateFileList();
|
||||||
if (pdfInput) pdfInput.value = ''
|
if (pdfInput) pdfInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFileList() {
|
function updateFileList() {
|
||||||
if (!selectedFile) {
|
if (!selectedFile) {
|
||||||
fileListDiv.classList.add('hidden')
|
fileListDiv.classList.add('hidden');
|
||||||
fileListDiv.innerHTML = ''
|
fileListDiv.innerHTML = '';
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fileListDiv.classList.remove('hidden')
|
fileListDiv.classList.remove('hidden');
|
||||||
fileListDiv.innerHTML = ''
|
fileListDiv.innerHTML = '';
|
||||||
|
|
||||||
// Expand container width for viewer if NOT in full width mode (default to true if not set)
|
// Expand container width for viewer if NOT in full width mode (default to true if not set)
|
||||||
const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false'
|
const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
|
||||||
if (toolUploader && !isFullWidth) {
|
if (toolUploader && !isFullWidth) {
|
||||||
toolUploader.classList.remove('max-w-2xl')
|
toolUploader.classList.remove('max-w-2xl');
|
||||||
toolUploader.classList.add('max-w-6xl')
|
toolUploader.classList.add('max-w-6xl');
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrapper = document.createElement('div')
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors'
|
wrapper.className =
|
||||||
|
'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors';
|
||||||
|
|
||||||
const innerDiv = document.createElement('div')
|
const innerDiv = document.createElement('div');
|
||||||
innerDiv.className = 'flex items-center justify-between'
|
innerDiv.className = 'flex items-center justify-between';
|
||||||
|
|
||||||
const infoDiv = document.createElement('div')
|
const infoDiv = document.createElement('div');
|
||||||
infoDiv.className = 'flex-1 min-w-0'
|
infoDiv.className = 'flex-1 min-w-0';
|
||||||
|
|
||||||
const nameSpan = document.createElement('p')
|
const nameSpan = document.createElement('p');
|
||||||
nameSpan.className = 'truncate font-medium text-white'
|
nameSpan.className = 'truncate font-medium text-white';
|
||||||
nameSpan.textContent = selectedFile.name
|
nameSpan.textContent = selectedFile.name;
|
||||||
|
|
||||||
const sizeSpan = document.createElement('p')
|
const sizeSpan = document.createElement('p');
|
||||||
sizeSpan.className = 'text-gray-400 text-sm'
|
sizeSpan.className = 'text-gray-400 text-sm';
|
||||||
sizeSpan.textContent = formatBytes(selectedFile.size)
|
sizeSpan.textContent = formatBytes(selectedFile.size);
|
||||||
|
|
||||||
infoDiv.append(nameSpan, sizeSpan)
|
infoDiv.append(nameSpan, sizeSpan);
|
||||||
|
|
||||||
const deleteBtn = document.createElement('button')
|
const deleteBtn = document.createElement('button');
|
||||||
deleteBtn.className = 'text-red-400 hover:text-red-300 p-2 flex-shrink-0 ml-2'
|
deleteBtn.className =
|
||||||
deleteBtn.title = 'Remove file'
|
'text-red-400 hover:text-red-300 p-2 flex-shrink-0 ml-2';
|
||||||
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>'
|
deleteBtn.title = 'Remove file';
|
||||||
|
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
deleteBtn.onclick = (e) => {
|
deleteBtn.onclick = (e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation();
|
||||||
resetState()
|
resetState();
|
||||||
}
|
};
|
||||||
|
|
||||||
innerDiv.append(infoDiv, deleteBtn)
|
innerDiv.append(infoDiv, deleteBtn);
|
||||||
wrapper.appendChild(innerDiv)
|
wrapper.appendChild(innerDiv);
|
||||||
fileListDiv.appendChild(wrapper)
|
fileListDiv.appendChild(wrapper);
|
||||||
|
|
||||||
createIcons({ icons })
|
createIcons({ icons });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function adjustViewerHeight(file: File) {
|
async function adjustViewerHeight(file: File) {
|
||||||
if (!viewerContainer) return
|
if (!viewerContainer) return;
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = await file.arrayBuffer()
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
const loadingTask = getPDFDocument({ data: arrayBuffer })
|
const loadingTask = getPDFDocument({ data: arrayBuffer });
|
||||||
const pdf = await loadingTask.promise
|
const pdf = await loadingTask.promise;
|
||||||
const page = await pdf.getPage(1)
|
const page = await pdf.getPage(1);
|
||||||
const viewport = page.getViewport({ scale: 1 })
|
const viewport = page.getViewport({ scale: 1 });
|
||||||
|
|
||||||
// Add ~50px for toolbar height relative to page height
|
// Add ~50px for toolbar height relative to page height
|
||||||
const aspectRatio = viewport.width / (viewport.height + 50)
|
const aspectRatio = viewport.width / (viewport.height + 50);
|
||||||
|
|
||||||
viewerContainer.style.height = 'auto'
|
viewerContainer.style.height = 'auto';
|
||||||
viewerContainer.style.aspectRatio = `${aspectRatio}`
|
viewerContainer.style.aspectRatio = `${aspectRatio}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error adjusting viewer height:', e)
|
console.error('Error adjusting viewer height:', e);
|
||||||
// Fallback if calculation fails
|
// Fallback if calculation fails
|
||||||
viewerContainer.style.height = '70vh'
|
viewerContainer.style.height = '70vh';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPdfInViewer(file: File) {
|
async function loadPdfInViewer(file: File) {
|
||||||
if (!viewerContainer) return
|
if (!viewerContainer) return;
|
||||||
|
|
||||||
if (viewerCard) {
|
if (viewerCard) {
|
||||||
viewerCard.classList.remove('hidden')
|
viewerCard.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear existing iframe and blob URL
|
// Clear existing iframe and blob URL
|
||||||
if (viewerIframe && viewerIframe.parentElement === viewerContainer) {
|
if (viewerIframe && viewerIframe.parentElement === viewerContainer) {
|
||||||
viewerContainer.removeChild(viewerIframe)
|
viewerContainer.removeChild(viewerIframe);
|
||||||
}
|
}
|
||||||
if (currentBlobUrl) {
|
if (currentBlobUrl) {
|
||||||
URL.revokeObjectURL(currentBlobUrl)
|
URL.revokeObjectURL(currentBlobUrl);
|
||||||
currentBlobUrl = null
|
currentBlobUrl = null;
|
||||||
}
|
}
|
||||||
viewerIframe = null
|
viewerIframe = null;
|
||||||
viewerReady = false
|
viewerReady = false;
|
||||||
|
|
||||||
// Calculate and apply dynamic height
|
// Calculate and apply dynamic height
|
||||||
await adjustViewerHeight(file)
|
await adjustViewerHeight(file);
|
||||||
|
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file)
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
const blob = new Blob([arrayBuffer as BlobPart], { type: 'application/pdf' })
|
const blob = new Blob([arrayBuffer as BlobPart], { type: 'application/pdf' });
|
||||||
currentBlobUrl = URL.createObjectURL(blob)
|
currentBlobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const existingPrefsRaw = localStorage.getItem('pdfjs.preferences')
|
const existingPrefsRaw = localStorage.getItem('pdfjs.preferences');
|
||||||
const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {}
|
const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {};
|
||||||
delete (existingPrefs as any).annotationEditorMode
|
delete (existingPrefs as any).annotationEditorMode;
|
||||||
const newPrefs = {
|
const newPrefs = {
|
||||||
...existingPrefs,
|
...existingPrefs,
|
||||||
enablePermissions: false,
|
enablePermissions: false,
|
||||||
}
|
};
|
||||||
localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs))
|
localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs));
|
||||||
} catch { }
|
} catch {}
|
||||||
|
|
||||||
const iframe = document.createElement('iframe')
|
const iframe = document.createElement('iframe');
|
||||||
iframe.className = 'w-full h-full border-0'
|
iframe.className = 'w-full h-full border-0';
|
||||||
iframe.allowFullscreen = true
|
iframe.allowFullscreen = true;
|
||||||
|
|
||||||
const viewerUrl = new URL(import.meta.env.BASE_URL + 'pdfjs-annotation-viewer/web/viewer.html', window.location.origin)
|
const viewerUrl = new URL(
|
||||||
const stampUserName = usernameInput?.value?.trim() || ''
|
import.meta.env.BASE_URL + 'pdfjs-annotation-viewer/web/viewer.html',
|
||||||
|
window.location.origin
|
||||||
|
);
|
||||||
|
const stampUserName = usernameInput?.value?.trim() || '';
|
||||||
// ae_username is the hash parameter used by pdfjs-annotation-extension to set the username
|
// ae_username is the hash parameter used by pdfjs-annotation-extension to set the username
|
||||||
const hashParams = stampUserName ? `#ae_username=${encodeURIComponent(stampUserName)}` : ''
|
const hashParams = stampUserName
|
||||||
iframe.src = `${viewerUrl.toString()}?file=${encodeURIComponent(currentBlobUrl)}${hashParams}`
|
? `#ae_username=${encodeURIComponent(stampUserName)}`
|
||||||
|
: '';
|
||||||
|
iframe.src = `${viewerUrl.toString()}?file=${encodeURIComponent(currentBlobUrl)}${hashParams}`;
|
||||||
|
|
||||||
iframe.addEventListener('load', () => {
|
iframe.addEventListener('load', () => {
|
||||||
setupAnnotationViewer(iframe)
|
setupAnnotationViewer(iframe);
|
||||||
})
|
});
|
||||||
|
|
||||||
viewerContainer.appendChild(iframe)
|
viewerContainer.appendChild(iframe);
|
||||||
viewerIframe = iframe
|
viewerIframe = iframe;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupAnnotationViewer(iframe: HTMLIFrameElement) {
|
function setupAnnotationViewer(iframe: HTMLIFrameElement) {
|
||||||
try {
|
try {
|
||||||
const win = iframe.contentWindow as any
|
const win = iframe.contentWindow as any;
|
||||||
const doc = win?.document as Document | null
|
const doc = win?.document as Document | null;
|
||||||
if (!win || !doc) return
|
if (!win || !doc) return;
|
||||||
|
|
||||||
const initialize = async () => {
|
const initialize = async () => {
|
||||||
try {
|
try {
|
||||||
const app = win.PDFViewerApplication
|
const app = win.PDFViewerApplication;
|
||||||
if (app?.initializedPromise) {
|
if (app?.initializedPromise) {
|
||||||
await app.initializedPromise
|
await app.initializedPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventBus = app?.eventBus
|
const eventBus = app?.eventBus;
|
||||||
if (eventBus && typeof eventBus._on === 'function') {
|
if (eventBus && typeof eventBus._on === 'function') {
|
||||||
eventBus._on('annotationeditoruimanager', () => {
|
eventBus._on('annotationeditoruimanager', () => {
|
||||||
try {
|
try {
|
||||||
const stampBtn = doc.getElementById('editorStampButton') as HTMLButtonElement | null
|
const stampBtn = doc.getElementById(
|
||||||
stampBtn?.click()
|
'editorStampButton'
|
||||||
} catch { }
|
) as HTMLButtonElement | null;
|
||||||
})
|
stampBtn?.click();
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const root = doc.querySelector('.PdfjsAnnotationExtension') as HTMLElement | null
|
const root = doc.querySelector(
|
||||||
|
'.PdfjsAnnotationExtension'
|
||||||
|
) as HTMLElement | null;
|
||||||
if (root) {
|
if (root) {
|
||||||
root.classList.add('PdfjsAnnotationExtension_Comment_hidden')
|
root.classList.add('PdfjsAnnotationExtension_Comment_hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
viewerReady = true
|
viewerReady = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to initialize annotation viewer for Add Stamps:', e)
|
console.error(
|
||||||
|
'Failed to initialize annotation viewer for Add Stamps:',
|
||||||
|
e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
void initialize()
|
void initialize();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error wiring Add Stamps viewer:', e)
|
console.error('Error wiring Add Stamps viewer:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onPdfSelected(file: File) {
|
async function onPdfSelected(file: File) {
|
||||||
selectedFile = file
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
updateFileList()
|
if (!result) return;
|
||||||
if (saveStampedBtn) saveStampedBtn.classList.remove('hidden')
|
result.pdf.destroy();
|
||||||
await loadPdfInViewer(file)
|
selectedFile = result.file;
|
||||||
|
updateFileList();
|
||||||
|
if (saveStampedBtn) saveStampedBtn.classList.remove('hidden');
|
||||||
|
await loadPdfInViewer(result.file);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pdfInput) {
|
if (pdfInput) {
|
||||||
pdfInput.addEventListener('change', async (e) => {
|
pdfInput.addEventListener('change', async (e) => {
|
||||||
const target = e.target as HTMLInputElement
|
const target = e.target as HTMLInputElement;
|
||||||
if (target.files && target.files.length > 0) {
|
if (target.files && target.files.length > 0) {
|
||||||
const file = target.files[0]
|
const file = target.files[0];
|
||||||
await onPdfSelected(file)
|
await onPdfSelected(file);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add drag/drop support
|
// Add drag/drop support
|
||||||
const dropZone = document.getElementById('drop-zone')
|
const dropZone = document.getElementById('drop-zone');
|
||||||
if (dropZone) {
|
if (dropZone) {
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
dropZone.classList.add('border-indigo-500')
|
dropZone.classList.add('border-indigo-500');
|
||||||
})
|
});
|
||||||
dropZone.addEventListener('dragleave', () => {
|
dropZone.addEventListener('dragleave', () => {
|
||||||
dropZone.classList.remove('border-indigo-500')
|
dropZone.classList.remove('border-indigo-500');
|
||||||
})
|
});
|
||||||
dropZone.addEventListener('drop', async (e) => {
|
dropZone.addEventListener('drop', async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
dropZone.classList.remove('border-indigo-500')
|
dropZone.classList.remove('border-indigo-500');
|
||||||
const file = e.dataTransfer?.files[0]
|
const file = e.dataTransfer?.files[0];
|
||||||
if (file && file.type === 'application/pdf') {
|
if (file && file.type === 'application/pdf') {
|
||||||
await onPdfSelected(file)
|
await onPdfSelected(file);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saveStampedBtn) {
|
if (saveStampedBtn) {
|
||||||
saveStampedBtn.addEventListener('click', () => {
|
saveStampedBtn.addEventListener('click', () => {
|
||||||
if (!viewerIframe) {
|
if (!viewerIframe) {
|
||||||
alert('Viewer not ready. Please upload a PDF and wait for it to finish loading.')
|
alert(
|
||||||
return
|
'Viewer not ready. Please upload a PDF and wait for it to finish loading.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const win = viewerIframe.contentWindow as any
|
const win = viewerIframe.contentWindow as any;
|
||||||
const extensionInstance = win?.pdfjsAnnotationExtensionInstance as any
|
const extensionInstance = win?.pdfjsAnnotationExtensionInstance as any;
|
||||||
|
|
||||||
if (extensionInstance && typeof extensionInstance.exportPdf === 'function') {
|
if (
|
||||||
const result = extensionInstance.exportPdf()
|
extensionInstance &&
|
||||||
|
typeof extensionInstance.exportPdf === 'function'
|
||||||
|
) {
|
||||||
|
const result = extensionInstance.exportPdf();
|
||||||
if (result && typeof result.then === 'function') {
|
if (result && typeof result.then === 'function') {
|
||||||
result.then(() => {
|
result
|
||||||
// Reset state after successful export
|
.then(() => {
|
||||||
setTimeout(() => resetState(), 500)
|
// Reset state after successful export
|
||||||
}).catch((err: unknown) => {
|
setTimeout(() => resetState(), 500);
|
||||||
console.error('Error while exporting stamped PDF via annotation extension:', err)
|
})
|
||||||
})
|
.catch((err: unknown) => {
|
||||||
|
console.error(
|
||||||
|
'Error while exporting stamped PDF via annotation extension:',
|
||||||
|
err
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
alert('Could not access the stamped-PDF exporter. Please use the Export → PDF button in the viewer toolbar as a fallback.')
|
alert(
|
||||||
|
'Could not access the stamped-PDF exporter. Please use the Export → PDF button in the viewer toolbar as a fallback.'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to trigger stamped PDF export:', e)
|
console.error('Failed to trigger stamped PDF export:', e);
|
||||||
alert('Could not export the stamped PDF. Please use the Export → PDF button in the viewer toolbar as a fallback.')
|
alert(
|
||||||
|
'Could not export the stamped PDF. Please use the Export → PDF button in the viewer toolbar as a fallback.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (backToToolsBtn) {
|
if (backToToolsBtn) {
|
||||||
backToToolsBtn.addEventListener('click', () => {
|
backToToolsBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeGlobalShortcuts()
|
initializeGlobalShortcuts();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from '../utils/pdf-operations.js';
|
} from '../utils/pdf-operations.js';
|
||||||
import { AddWatermarkState, PageWatermarkConfig } from '@/types';
|
import { AddWatermarkState, PageWatermarkConfig } from '@/types';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -115,16 +116,16 @@ async function handleFiles(files: FileList) {
|
|||||||
showAlert('Invalid File', 'Please upload a valid PDF file.');
|
showAlert('Invalid File', 'Please upload a valid PDF file.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader('Loading PDF...');
|
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
const pdfBytes = new Uint8Array(arrayBuffer);
|
if (!result) return;
|
||||||
|
showLoader('Loading PDF...');
|
||||||
|
const pdfBytes = new Uint8Array(result.bytes);
|
||||||
pageState.pdfDoc = await PDFLibDocument.load(pdfBytes);
|
pageState.pdfDoc = await PDFLibDocument.load(pdfBytes);
|
||||||
pageState.file = file;
|
pageState.file = result.file;
|
||||||
pageState.pdfBytes = pdfBytes;
|
pageState.pdfBytes = pdfBytes;
|
||||||
|
|
||||||
cachedPdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes.slice() })
|
cachedPdfjsDoc = result.pdf;
|
||||||
.promise;
|
|
||||||
totalPageCount = cachedPdfjsDoc.numPages;
|
totalPageCount = cachedPdfjsDoc.numPages;
|
||||||
currentPageNum = 1;
|
currentPageNum = 1;
|
||||||
pageWatermarks.clear();
|
pageWatermarks.clear();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { applyColorAdjustments } from '../utils/image-effects.js';
|
|||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import type { AdjustColorsSettings } from '../types/adjust-colors-type.js';
|
import type { AdjustColorsSettings } from '../types/adjust-colors-type.js';
|
||||||
import { t } from '../i18n/i18n';
|
import { t } from '../i18n/i18n';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -357,13 +358,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
files = [validFiles[0]];
|
|
||||||
updateUI();
|
|
||||||
|
|
||||||
showLoader('Loading preview...');
|
|
||||||
try {
|
try {
|
||||||
const buffer = await readFileAsArrayBuffer(validFiles[0]);
|
const result = await loadPdfWithPasswordPrompt(validFiles[0]);
|
||||||
pdfjsDoc = await getPDFDocument({ data: buffer }).promise;
|
if (!result) return;
|
||||||
|
showLoader('Loading preview...');
|
||||||
|
files = [result.file];
|
||||||
|
updateUI();
|
||||||
|
pdfjsDoc = result.pdf;
|
||||||
await renderPreview();
|
await renderPreview();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
showWasmRequiredDialog,
|
showWasmRequiredDialog,
|
||||||
WasmProvider,
|
WasmProvider,
|
||||||
} from '../utils/wasm-provider.js';
|
} from '../utils/wasm-provider.js';
|
||||||
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
const pageState: AlternateMergeState = {
|
const pageState: AlternateMergeState = {
|
||||||
files: [],
|
files: [],
|
||||||
@@ -69,20 +70,21 @@ async function updateUI() {
|
|||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
// Load PDFs and populate list
|
// Load PDFs and populate list
|
||||||
|
hideLoader();
|
||||||
|
pageState.files = await batchDecryptIfNeeded(pageState.files);
|
||||||
showLoader('Loading PDF files...');
|
showLoader('Loading PDF files...');
|
||||||
fileList.innerHTML = '';
|
fileList.innerHTML = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (let i = 0; i < pageState.files.length; i++) {
|
for (let i = 0; i < pageState.files.length; i++) {
|
||||||
const file = pageState.files[i];
|
const file = pageState.files[i];
|
||||||
const fileKey = makeUniqueFileKey(i, file.name);
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
|
||||||
pageState.pdfBytes.set(fileKey, arrayBuffer);
|
|
||||||
|
|
||||||
const bytesForPdfJs = arrayBuffer.slice(0);
|
const fileKey = makeUniqueFileKey(i, file.name);
|
||||||
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
|
const bytes = await file.arrayBuffer();
|
||||||
pageState.pdfDocs.set(fileKey, pdfjsDoc);
|
const pdf = await getPDFDocument({ data: bytes.slice(0) }).promise;
|
||||||
const pageCount = pdfjsDoc.numPages;
|
pageState.pdfBytes.set(fileKey, bytes);
|
||||||
|
pageState.pdfDocs.set(fileKey, pdf);
|
||||||
|
const pageCount = pdf.numPages;
|
||||||
|
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className =
|
li.className =
|
||||||
|
|||||||
@@ -3,102 +3,156 @@ import { showAlert, showLoader, hideLoader } from '../ui.js';
|
|||||||
import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js';
|
||||||
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
||||||
import { BackgroundColorState } from '@/types';
|
import { BackgroundColorState } from '@/types';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
const pageState: BackgroundColorState = { file: null, pdfDoc: null };
|
const pageState: BackgroundColorState = { file: null, pdfDoc: null };
|
||||||
|
|
||||||
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializePage); }
|
if (document.readyState === 'loading') {
|
||||||
else { initializePage(); }
|
document.addEventListener('DOMContentLoaded', initializePage);
|
||||||
|
} else {
|
||||||
function initializePage() {
|
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); }
|
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) {
|
async function handleFiles(files: FileList) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (!file || file.type !== 'application/pdf') { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; }
|
if (!file || file.type !== 'application/pdf') {
|
||||||
|
showAlert('Invalid File', 'Please upload a valid PDF file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
|
if (!result) return;
|
||||||
showLoader('Loading PDF...');
|
showLoader('Loading PDF...');
|
||||||
try {
|
result.pdf.destroy();
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
pageState.pdfDoc = await PDFLibDocument.load(result.bytes);
|
||||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
pageState.file = result.file;
|
||||||
pageState.file = file;
|
updateFileDisplay();
|
||||||
updateFileDisplay();
|
document.getElementById('options-panel')?.classList.remove('hidden');
|
||||||
document.getElementById('options-panel')?.classList.remove('hidden');
|
} catch (error) {
|
||||||
} catch (error) { console.error(error); showAlert('Error', 'Failed to load PDF file.'); }
|
console.error(error);
|
||||||
finally { hideLoader(); }
|
showAlert('Error', 'Failed to load PDF file.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFileDisplay() {
|
function updateFileDisplay() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
|
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
fileDiv.className =
|
||||||
const infoContainer = document.createElement('div');
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
||||||
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
const infoContainer = document.createElement('div');
|
||||||
const nameSpan = document.createElement('div');
|
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.textContent = pageState.file.name;
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
const metaSpan = document.createElement('div');
|
nameSpan.textContent = pageState.file.name;
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`;
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`;
|
||||||
const removeBtn = document.createElement('button');
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.onclick = resetState;
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
removeBtn.onclick = resetState;
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
createIcons({ icons });
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
createIcons({ icons });
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
pageState.file = null; pageState.pdfDoc = null;
|
pageState.file = null;
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
pageState.pdfDoc = null;
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
document.getElementById('options-panel')?.classList.add('hidden');
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
document.getElementById('options-panel')?.classList.add('hidden');
|
||||||
if (fileInput) fileInput.value = '';
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
|
if (fileInput) fileInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeBackgroundColor() {
|
async function changeBackgroundColor() {
|
||||||
if (!pageState.pdfDoc) { showAlert('Error', 'Please upload a PDF file first.'); return; }
|
if (!pageState.pdfDoc) {
|
||||||
const colorHex = (document.getElementById('background-color') as HTMLInputElement).value;
|
showAlert('Error', 'Please upload a PDF file first.');
|
||||||
const color = hexToRgb(colorHex);
|
return;
|
||||||
showLoader('Changing background color...');
|
}
|
||||||
try {
|
const colorHex = (
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
document.getElementById('background-color') as HTMLInputElement
|
||||||
for (let i = 0; i < pageState.pdfDoc.getPageCount(); i++) {
|
).value;
|
||||||
const [originalPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
|
const color = hexToRgb(colorHex);
|
||||||
const { width, height } = originalPage.getSize();
|
showLoader('Changing background color...');
|
||||||
const newPage = newPdfDoc.addPage([width, height]);
|
try {
|
||||||
newPage.drawRectangle({ x: 0, y: 0, width, height, color: rgb(color.r, color.g, color.b) });
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
const embeddedPage = await newPdfDoc.embedPage(originalPage);
|
for (let i = 0; i < pageState.pdfDoc.getPageCount(); i++) {
|
||||||
newPage.drawPage(embeddedPage, { x: 0, y: 0, width, height });
|
const [originalPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
|
||||||
}
|
const { width, height } = originalPage.getSize();
|
||||||
const newPdfBytes = await newPdfDoc.save();
|
const newPage = newPdfDoc.addPage([width, height]);
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'background-changed.pdf');
|
newPage.drawRectangle({
|
||||||
showAlert('Success', 'Background color changed successfully!', 'success', () => { resetState(); });
|
x: 0,
|
||||||
} catch (e) { console.error(e); showAlert('Error', 'Could not change the background color.'); }
|
y: 0,
|
||||||
finally { hideLoader(); }
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createIcons, icons } from 'lucide';
|
|||||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||||
import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js';
|
||||||
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
|
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
import { FileEntry, Position, StylePreset } from '@/types';
|
import { FileEntry, Position, StylePreset } from '@/types';
|
||||||
@@ -178,9 +179,13 @@ async function handleFiles(fileList: FileList) {
|
|||||||
try {
|
try {
|
||||||
for (const file of Array.from(fileList)) {
|
for (const file of Array.from(fileList)) {
|
||||||
if (file.type !== 'application/pdf') continue;
|
if (file.type !== 'application/pdf') continue;
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
hideLoader();
|
||||||
const pdfDoc = await PDFDocument.load(arrayBuffer);
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
files.push({ file, pageCount: pdfDoc.getPageCount() });
|
if (!result) continue;
|
||||||
|
showLoader('Loading PDFs...');
|
||||||
|
result.pdf.destroy();
|
||||||
|
const pdfDoc = await PDFDocument.load(result.bytes);
|
||||||
|
files.push({ file: result.file, pageCount: pdfDoc.getPageCount() });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
escapeHtml,
|
escapeHtml,
|
||||||
hexToRgb,
|
hexToRgb,
|
||||||
} from '../utils/helpers.js';
|
} from '../utils/helpers.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import {
|
import {
|
||||||
BookmarkNode,
|
BookmarkNode,
|
||||||
BookmarkTree,
|
BookmarkTree,
|
||||||
@@ -1223,7 +1224,14 @@ async function loadPDF(e?: Event): Promise<void> {
|
|||||||
if (filenameDisplay)
|
if (filenameDisplay)
|
||||||
filenameDisplay.textContent = truncateFilename(file.name);
|
filenameDisplay.textContent = truncateFilename(file.name);
|
||||||
renderFileDisplay(file);
|
renderFileDisplay(file);
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
|
||||||
|
loaderModal?.classList.add('hidden');
|
||||||
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
|
if (!result) {
|
||||||
|
loaderModal?.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loaderModal?.classList.remove('hidden');
|
||||||
|
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
bookmarkTree = [];
|
bookmarkTree = [];
|
||||||
@@ -1232,12 +1240,8 @@ async function loadPDF(e?: Event): Promise<void> {
|
|||||||
selectedBookmarks.clear();
|
selectedBookmarks.clear();
|
||||||
collapsedNodes.clear();
|
collapsedNodes.clear();
|
||||||
|
|
||||||
pdfLibDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryption: true });
|
pdfLibDoc = await PDFDocument.load(result.bytes, { ignoreEncryption: true });
|
||||||
|
pdfJsDoc = result.pdf;
|
||||||
const loadingTask = getPDFDocument({
|
|
||||||
data: new Uint8Array(arrayBuffer),
|
|
||||||
});
|
|
||||||
pdfJsDoc = await loadingTask.promise;
|
|
||||||
|
|
||||||
if (gotoPageInput) gotoPageInput.max = String(pdfJsDoc.numPages);
|
if (gotoPageInput) gotoPageInput.max = String(pdfJsDoc.numPages);
|
||||||
|
|
||||||
|
|||||||
@@ -1,309 +1,349 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, formatBytes, hexToRgb, getPDFDocument } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
formatBytes,
|
||||||
|
hexToRgb,
|
||||||
|
getPDFDocument,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { CombineSinglePageState } from '@/types';
|
import { CombineSinglePageState } from '@/types';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
|
import.meta.url
|
||||||
|
).toString();
|
||||||
|
|
||||||
const pageState: CombineSinglePageState = {
|
const pageState: CombineSinglePageState = {
|
||||||
file: null,
|
file: null,
|
||||||
pdfDoc: null,
|
pdfDoc: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
pageState.file = null;
|
pageState.file = null;
|
||||||
pageState.pdfDoc = null;
|
pageState.pdfDoc = null;
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUI() {
|
async function updateUI() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
|
|
||||||
if (!fileDisplayArea) return;
|
if (!fileDisplayArea) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (pageState.file) {
|
if (pageState.file) {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = pageState.file.name;
|
nameSpan.textContent = pageState.file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
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.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.onclick = function () {
|
removeBtn.onclick = function () {
|
||||||
resetState();
|
resetState();
|
||||||
};
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showLoader('Loading PDF...');
|
const result = await loadPdfWithPasswordPrompt(pageState.file);
|
||||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
if (!result) {
|
||||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
resetState();
|
||||||
ignoreEncryption: true,
|
return;
|
||||||
throwOnInvalidObject: false
|
}
|
||||||
});
|
showLoader('Loading PDF...');
|
||||||
hideLoader();
|
result.pdf.destroy();
|
||||||
|
pageState.file = result.file;
|
||||||
|
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||||
|
throwOnInvalidObject: false,
|
||||||
|
});
|
||||||
|
hideLoader();
|
||||||
|
|
||||||
const pageCount = pageState.pdfDoc.getPageCount();
|
const pageCount = pageState.pdfDoc.getPageCount();
|
||||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||||
|
|
||||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading PDF:', error);
|
console.error('Error loading PDF:', error);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', 'Failed to load PDF file.');
|
showAlert('Error', 'Failed to load PDF file.');
|
||||||
resetState();
|
resetState();
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function combineToSinglePage() {
|
async function combineToSinglePage() {
|
||||||
if (!pageState.pdfDoc || !pageState.file) {
|
if (!pageState.pdfDoc || !pageState.file) {
|
||||||
showAlert('Error', 'Please upload a PDF first.');
|
showAlert('Error', 'Please upload a PDF first.');
|
||||||
return;
|
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 newPage = newDoc.addPage([finalWidth, finalHeight]);
|
||||||
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);
|
if (backgroundColorHex.toUpperCase() !== '#FFFFFF') {
|
||||||
const separatorColor = hexToRgb(separatorColorHex);
|
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 {
|
for (let i = 0; i < sourcePages.length; i++) {
|
||||||
const sourceDoc = pageState.pdfDoc;
|
showLoader(`Processing page ${i + 1} of ${sourcePages.length}...`);
|
||||||
const newDoc = await PDFLibDocument.create();
|
const sourcePage = sourcePages[i];
|
||||||
|
const { width, height } = sourcePage.getSize();
|
||||||
|
|
||||||
const pdfBytes = await sourceDoc.save();
|
try {
|
||||||
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
|
const page = await pdfjsDoc.getPage(i + 1);
|
||||||
|
const scale = 2.0;
|
||||||
|
const viewport = page.getViewport({ scale });
|
||||||
|
|
||||||
const sourcePages = sourceDoc.getPages();
|
const canvas = document.createElement('canvas');
|
||||||
let maxWidth = 0;
|
canvas.width = viewport.width;
|
||||||
let maxHeight = 0;
|
canvas.height = viewport.height;
|
||||||
let totalWidth = 0;
|
const context = canvas.getContext('2d')!;
|
||||||
let totalHeight = 0;
|
|
||||||
|
|
||||||
sourcePages.forEach(function (page) {
|
await page.render({
|
||||||
const { width, height } = page.getSize();
|
canvasContext: context,
|
||||||
if (width > maxWidth) maxWidth = width;
|
viewport,
|
||||||
if (height > maxHeight) maxHeight = height;
|
canvas,
|
||||||
totalWidth += width;
|
}).promise;
|
||||||
totalHeight += height;
|
|
||||||
});
|
const pngDataUrl = canvas.toDataURL('image/png');
|
||||||
|
const pngImage = await newDoc.embedPng(pngDataUrl);
|
||||||
|
|
||||||
let finalWidth: number, finalHeight: number;
|
|
||||||
if (orientation === 'horizontal') {
|
if (orientation === 'horizontal') {
|
||||||
finalWidth = totalWidth + Math.max(0, sourcePages.length - 1) * spacing;
|
const y = (finalHeight - height) / 2;
|
||||||
finalHeight = maxHeight;
|
newPage.drawImage(pngImage, { x: currentX, y, width, height });
|
||||||
} else {
|
} else {
|
||||||
finalWidth = maxWidth;
|
currentY -= height;
|
||||||
finalHeight = totalHeight + Math.max(0, sourcePages.length - 1) * spacing;
|
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 (addSeparator && i < sourcePages.length - 1) {
|
||||||
|
if (orientation === 'horizontal') {
|
||||||
if (backgroundColorHex.toUpperCase() !== '#FFFFFF') {
|
const lineX = currentX + width + spacing / 2;
|
||||||
newPage.drawRectangle({
|
newPage.drawLine({
|
||||||
x: 0,
|
start: { x: lineX, y: 0 },
|
||||||
y: 0,
|
end: { x: lineX, y: finalHeight },
|
||||||
width: finalWidth,
|
thickness: separatorThickness,
|
||||||
height: finalHeight,
|
color: rgb(separatorColor.r, separatorColor.g, separatorColor.b),
|
||||||
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.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 {
|
||||||
let currentX = 0;
|
if (orientation === 'horizontal') {
|
||||||
let currentY = finalHeight;
|
currentX += width + spacing;
|
||||||
|
} else {
|
||||||
for (let i = 0; i < sourcePages.length; i++) {
|
currentY -= spacing;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
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) {
|
function handleFileSelect(files: FileList | null) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
if (
|
||||||
pageState.file = file;
|
file.type === 'application/pdf' ||
|
||||||
updateUI();
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
}
|
) {
|
||||||
|
pageState.file = file;
|
||||||
|
updateUI();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
const addSeparatorCheckbox = document.getElementById('add-separator');
|
const addSeparatorCheckbox = document.getElementById('add-separator');
|
||||||
const separatorOptions = document.getElementById('separator-options');
|
const separatorOptions = document.getElementById('separator-options');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', function () {
|
backBtn.addEventListener('click', function () {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
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) {
|
fileInput.addEventListener('click', function () {
|
||||||
addSeparatorCheckbox.addEventListener('change', function () {
|
fileInput.value = '';
|
||||||
if ((addSeparatorCheckbox as HTMLInputElement).checked) {
|
});
|
||||||
separatorOptions.classList.remove('hidden');
|
}
|
||||||
} else {
|
|
||||||
separatorOptions.classList.add('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
if (processBtn) {
|
||||||
fileInput.addEventListener('change', function (e) {
|
processBtn.addEventListener('click', combineToSinglePage);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.ts';
|
import { showLoader, hideLoader, showAlert } from '../ui.ts';
|
||||||
import { getPDFDocument } from '../utils/helpers.ts';
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { CompareState } from '@/types';
|
import { CompareState } from '@/types';
|
||||||
@@ -745,9 +745,11 @@ async function handleFileInput(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showLoader(`Loading ${file.name}...`);
|
hideLoader();
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
pageState[docKey] = await getPDFDocument({ data: arrayBuffer }).promise;
|
if (!result) return;
|
||||||
|
showLoader(`Loading ${result.file.name}...`);
|
||||||
|
pageState[docKey] = result.pdf;
|
||||||
caches.pageModelCache.clear();
|
caches.pageModelCache.clear();
|
||||||
caches.comparisonCache.clear();
|
caches.comparisonCache.clear();
|
||||||
caches.comparisonResultsCache.clear();
|
caches.comparisonResultsCache.clear();
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import {
|
|||||||
formatBytes,
|
formatBytes,
|
||||||
getPDFDocument,
|
getPDFDocument,
|
||||||
} from '../utils/helpers.js';
|
} from '../utils/helpers.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { PDFDocument } from 'pdf-lib';
|
import { PDFDocument } from 'pdf-lib';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -120,15 +122,27 @@ async function performCondenseCompression(
|
|||||||
return { ...result, usedFallback: true };
|
return { ...result, usedFallback: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`PDF compression failed: ${errorMessage}`);
|
throw new Error(`PDF compression failed: ${errorMessage}`, {
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function performPhotonCompression(
|
async function performPhotonCompression(
|
||||||
arrayBuffer: ArrayBuffer,
|
arrayBuffer: ArrayBuffer,
|
||||||
level: string
|
level: string,
|
||||||
|
file?: File
|
||||||
) {
|
) {
|
||||||
const pdfJsDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
let pdfJsDoc: PDFDocumentProxy;
|
||||||
|
if (file) {
|
||||||
|
hideLoader();
|
||||||
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
|
if (!result) return null;
|
||||||
|
showLoader('Running Photon compression...');
|
||||||
|
pdfJsDoc = result.pdf;
|
||||||
|
} else {
|
||||||
|
pdfJsDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||||
|
}
|
||||||
const newPdfDoc = await PDFDocument.create();
|
const newPdfDoc = await PDFDocument.create();
|
||||||
const settings =
|
const settings =
|
||||||
PHOTON_PRESETS[level as keyof typeof PHOTON_PRESETS] ||
|
PHOTON_PRESETS[level as keyof typeof PHOTON_PRESETS] ||
|
||||||
@@ -429,8 +443,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
)) as ArrayBuffer;
|
)) as ArrayBuffer;
|
||||||
const resultBytes = await performPhotonCompression(
|
const resultBytes = await performPhotonCompression(
|
||||||
arrayBuffer,
|
arrayBuffer,
|
||||||
level
|
level,
|
||||||
|
originalFile
|
||||||
);
|
);
|
||||||
|
if (!resultBytes) return;
|
||||||
const buffer = resultBytes.buffer.slice(
|
const buffer = resultBytes.buffer.slice(
|
||||||
resultBytes.byteOffset,
|
resultBytes.byteOffset,
|
||||||
resultBytes.byteOffset + resultBytes.byteLength
|
resultBytes.byteOffset + resultBytes.byteLength
|
||||||
@@ -494,7 +510,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const arrayBuffer = (await readFileAsArrayBuffer(
|
const arrayBuffer = (await readFileAsArrayBuffer(
|
||||||
file
|
file
|
||||||
)) as ArrayBuffer;
|
)) as ArrayBuffer;
|
||||||
resultBytes = await performPhotonCompression(arrayBuffer, level);
|
const photonResult = await performPhotonCompression(
|
||||||
|
arrayBuffer,
|
||||||
|
level,
|
||||||
|
file
|
||||||
|
);
|
||||||
|
if (!photonResult) return;
|
||||||
|
resultBytes = photonResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
totalCompressedSize += resultBytes.length;
|
totalCompressedSize += resultBytes.length;
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import {
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
downloadFile,
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
readFileAsArrayBuffer,
|
|
||||||
formatBytes,
|
|
||||||
getPDFDocument,
|
|
||||||
} from '../utils/helpers.js';
|
|
||||||
import Cropper from 'cropperjs';
|
import Cropper from 'cropperjs';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
@@ -88,16 +84,19 @@ async function handleFile(file: File) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Loading PDF...');
|
|
||||||
cropperState.file = file;
|
cropperState.file = file;
|
||||||
cropperState.pageCrops = {};
|
cropperState.pageCrops = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
cropperState.originalPdfBytes = arrayBuffer as ArrayBuffer;
|
if (!result) {
|
||||||
cropperState.pdfDoc = await getPDFDocument({
|
cropperState.file = null;
|
||||||
data: (arrayBuffer as ArrayBuffer).slice(0),
|
return;
|
||||||
}).promise;
|
}
|
||||||
|
showLoader('Loading PDF...');
|
||||||
|
cropperState.file = result.file;
|
||||||
|
cropperState.originalPdfBytes = result.bytes;
|
||||||
|
cropperState.pdfDoc = result.pdf;
|
||||||
cropperState.currentPageNum = 1;
|
cropperState.currentPageNum = 1;
|
||||||
|
|
||||||
updateFileDisplay();
|
updateFileDisplay();
|
||||||
@@ -308,7 +307,7 @@ async function performMetadataCrop(
|
|||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const pdfToModify = await PDFLibDocument.load(
|
const pdfToModify = await PDFLibDocument.load(
|
||||||
cropperState.originalPdfBytes!,
|
cropperState.originalPdfBytes!,
|
||||||
{ ignoreEncryption: true, throwOnInvalidObject: false }
|
{ throwOnInvalidObject: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const pageNum in cropData) {
|
for (const pageNum in cropData) {
|
||||||
@@ -352,7 +351,7 @@ async function performFlatteningCrop(
|
|||||||
const newPdfDoc = await PDFLibDocument.create();
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
const sourcePdfDocForCopying = await PDFLibDocument.load(
|
const sourcePdfDocForCopying = await PDFLibDocument.load(
|
||||||
cropperState.originalPdfBytes!,
|
cropperState.originalPdfBytes!,
|
||||||
{ ignoreEncryption: true, throwOnInvalidObject: false }
|
{ throwOnInvalidObject: false }
|
||||||
);
|
);
|
||||||
const totalPages = cropperState.pdfDoc.numPages;
|
const totalPages = cropperState.pdfDoc.numPages;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||||
import {
|
import {
|
||||||
readFileAsArrayBuffer,
|
|
||||||
formatBytes,
|
formatBytes,
|
||||||
downloadFile,
|
downloadFile,
|
||||||
getPDFDocument,
|
|
||||||
parsePageRanges,
|
parsePageRanges,
|
||||||
} from '../utils/helpers.js';
|
} from '../utils/helpers.js';
|
||||||
import { PDFDocument } from 'pdf-lib';
|
import { PDFDocument } from 'pdf-lib';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import { deletePdfPages } from '../utils/pdf-operations.js';
|
import { deletePdfPages } from '../utils/pdf-operations.js';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { DeletePagesState } from '@/types';
|
import { DeletePagesState } from '@/types';
|
||||||
@@ -85,18 +84,20 @@ async function handleFile(file: File) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Loading PDF...');
|
|
||||||
deleteState.file = file;
|
deleteState.file = file;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
deleteState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, {
|
if (!result) {
|
||||||
ignoreEncryption: true,
|
deleteState.file = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showLoader('Loading PDF...');
|
||||||
|
deleteState.file = result.file;
|
||||||
|
deleteState.pdfDoc = await PDFDocument.load(result.bytes, {
|
||||||
throwOnInvalidObject: false,
|
throwOnInvalidObject: false,
|
||||||
});
|
});
|
||||||
deleteState.pdfJsDoc = await getPDFDocument({
|
deleteState.pdfJsDoc = result.pdf;
|
||||||
data: (arrayBuffer as ArrayBuffer).slice(0),
|
|
||||||
}).promise;
|
|
||||||
deleteState.totalPages = deleteState.pdfDoc.getPageCount();
|
deleteState.totalPages = deleteState.pdfDoc.getPageCount();
|
||||||
deleteState.pagesToDelete = new Set();
|
deleteState.pagesToDelete = new Set();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { downloadFile } from '../utils/helpers';
|
import { downloadFile } from '../utils/helpers';
|
||||||
|
|
||||||
@@ -151,6 +152,8 @@ async function processDeskew(): Promise<void> {
|
|||||||
const threshold = parseFloat(thresholdSelect?.value || '0.5');
|
const threshold = parseFloat(thresholdSelect?.value || '0.5');
|
||||||
const dpi = parseInt(dpiSelect?.value || '150', 10);
|
const dpi = parseInt(dpiSelect?.value || '150', 10);
|
||||||
|
|
||||||
|
selectedFiles = await batchDecryptIfNeeded(selectedFiles);
|
||||||
|
|
||||||
showLoader('Initializing PyMuPDF...');
|
showLoader('Initializing PyMuPDF...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,235 +1,269 @@
|
|||||||
import { DividePagesState } from '@/types';
|
import { DividePagesState } from '@/types';
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, formatBytes, parsePageRanges } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
formatBytes,
|
||||||
|
parsePageRanges,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
const pageState: DividePagesState = {
|
const pageState: DividePagesState = {
|
||||||
file: null,
|
file: null,
|
||||||
pdfDoc: null,
|
pdfDoc: null,
|
||||||
totalPages: 0,
|
totalPages: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
pageState.file = null;
|
pageState.file = null;
|
||||||
pageState.pdfDoc = null;
|
pageState.pdfDoc = null;
|
||||||
pageState.totalPages = 0;
|
pageState.totalPages = 0;
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
|
|
||||||
const splitTypeSelect = document.getElementById('split-type') as HTMLSelectElement;
|
const splitTypeSelect = document.getElementById(
|
||||||
if (splitTypeSelect) splitTypeSelect.value = 'vertical';
|
'split-type'
|
||||||
|
) as HTMLSelectElement;
|
||||||
|
if (splitTypeSelect) splitTypeSelect.value = 'vertical';
|
||||||
|
|
||||||
const pageRangeInput = document.getElementById('page-range') as HTMLInputElement;
|
const pageRangeInput = document.getElementById(
|
||||||
if (pageRangeInput) pageRangeInput.value = '';
|
'page-range'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
if (pageRangeInput) pageRangeInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUI() {
|
async function updateUI() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
|
|
||||||
if (!fileDisplayArea) return;
|
if (!fileDisplayArea) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (pageState.file) {
|
if (pageState.file) {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = pageState.file.name;
|
nameSpan.textContent = pageState.file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
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.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.onclick = function () {
|
removeBtn.onclick = function () {
|
||||||
resetState();
|
resetState();
|
||||||
};
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showLoader('Loading PDF...');
|
const result = await loadPdfWithPasswordPrompt(pageState.file);
|
||||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
if (!result) {
|
||||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
resetState();
|
||||||
ignoreEncryption: true,
|
return;
|
||||||
throwOnInvalidObject: false
|
}
|
||||||
});
|
result.pdf.destroy();
|
||||||
pageState.totalPages = pageState.pdfDoc.getPageCount();
|
pageState.file = result.file;
|
||||||
hideLoader();
|
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');
|
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.totalPages} pages`;
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading PDF:', error);
|
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||||
hideLoader();
|
} catch (error) {
|
||||||
showAlert('Error', 'Failed to load PDF file.');
|
console.error('Error loading PDF:', error);
|
||||||
resetState();
|
hideLoader();
|
||||||
}
|
showAlert('Error', 'Failed to load PDF file.');
|
||||||
} else {
|
resetState();
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dividePages() {
|
async function dividePages() {
|
||||||
if (!pageState.pdfDoc || !pageState.file) {
|
if (!pageState.pdfDoc || !pageState.file) {
|
||||||
showAlert('Error', 'Please upload a PDF first.');
|
showAlert('Error', 'Please upload a PDF first.');
|
||||||
return;
|
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;
|
showLoader('Splitting PDF pages...');
|
||||||
const pageRangeValue = pageRangeInput?.value.trim().toLowerCase() || '';
|
|
||||||
const splitTypeSelect = document.getElementById('split-type') as HTMLSelectElement;
|
|
||||||
const splitType = splitTypeSelect.value;
|
|
||||||
|
|
||||||
let pagesToDivide: Set<number>;
|
try {
|
||||||
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
|
const pages = pageState.pdfDoc.getPages();
|
||||||
|
|
||||||
if (pageRangeValue === '' || pageRangeValue === 'all') {
|
for (let i = 0; i < pages.length; i++) {
|
||||||
pagesToDivide = new Set(Array.from({ length: pageState.totalPages }, (_, i) => i + 1));
|
const pageNum = i + 1;
|
||||||
} else {
|
const originalPage = pages[i];
|
||||||
const parsedIndices = parsePageRanges(pageRangeValue, pageState.totalPages);
|
const { width, height } = originalPage.getSize();
|
||||||
pagesToDivide = new Set(parsedIndices.map(i => i + 1));
|
|
||||||
|
|
||||||
if (pagesToDivide.size === 0) {
|
showLoader(`Processing page ${pageNum} of ${pages.length}...`);
|
||||||
showAlert('Invalid Range', 'Please enter a valid page range (e.g., 1-5, 8, 11-13).');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
switch (splitType) {
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
case 'vertical':
|
||||||
const pages = pageState.pdfDoc.getPages();
|
page1.setCropBox(0, 0, width / 2, height);
|
||||||
|
page2.setCropBox(width / 2, 0, width / 2, height);
|
||||||
for (let i = 0; i < pages.length; i++) {
|
break;
|
||||||
const pageNum = i + 1;
|
case 'horizontal':
|
||||||
const originalPage = pages[i];
|
page1.setCropBox(0, height / 2, width, height / 2);
|
||||||
const { width, height } = originalPage.getSize();
|
page2.setCropBox(0, 0, width, height / 2);
|
||||||
|
break;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await newPdfDoc.save();
|
newPdfDoc.addPage(page1);
|
||||||
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
|
newPdfDoc.addPage(page2);
|
||||||
|
} else {
|
||||||
downloadFile(
|
const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
|
||||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
newPdfDoc.addPage(copiedPage);
|
||||||
`${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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function handleFileSelect(files: FileList | null) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
if (
|
||||||
pageState.file = file;
|
file.type === 'application/pdf' ||
|
||||||
updateUI();
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
}
|
) {
|
||||||
|
pageState.file = file;
|
||||||
|
updateUI();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', function () {
|
backBtn.addEventListener('click', function () {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
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('click', function () {
|
||||||
fileInput.addEventListener('change', function (e) {
|
fileInput.value = '';
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', function (e) {
|
if (processBtn) {
|
||||||
e.preventDefault();
|
processBtn.addEventListener('click', dividePages);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, getPDFDocument } from '../utils/helpers.js';
|
import { downloadFile } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js';
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
import {
|
||||||
|
renderPagesProgressively,
|
||||||
|
cleanupLazyRendering,
|
||||||
|
} from '../utils/render-utils.js';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
|
import.meta.url
|
||||||
|
).toString();
|
||||||
|
|
||||||
const duplicateOrganizeState = {
|
const duplicateOrganizeState = {
|
||||||
sortableInstances: {},
|
sortableInstances: {},
|
||||||
@@ -91,7 +98,15 @@ export async function renderDuplicateOrganizeThumbnails() {
|
|||||||
|
|
||||||
showLoader('Rendering page previews...');
|
showLoader('Rendering page previews...');
|
||||||
const pdfData = await state.pdfDoc.save();
|
const pdfData = await state.pdfDoc.save();
|
||||||
const pdfjsDoc = await getPDFDocument({ data: pdfData }).promise;
|
hideLoader();
|
||||||
|
const loadResult = await loadPdfWithPasswordPrompt(
|
||||||
|
state.files[0],
|
||||||
|
state.files,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
if (!loadResult) return;
|
||||||
|
showLoader('Rendering page previews...');
|
||||||
|
const pdfjsDoc = loadResult.pdf;
|
||||||
|
|
||||||
grid.textContent = '';
|
grid.textContent = '';
|
||||||
|
|
||||||
@@ -148,22 +163,17 @@ export async function renderDuplicateOrganizeThumbnails() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Render pages progressively with lazy loading
|
// Render pages progressively with lazy loading
|
||||||
await renderPagesProgressively(
|
await renderPagesProgressively(pdfjsDoc, grid, createWrapper, {
|
||||||
pdfjsDoc,
|
batchSize: 8,
|
||||||
grid,
|
useLazyLoading: true,
|
||||||
createWrapper,
|
lazyLoadMargin: '400px',
|
||||||
{
|
onProgress: (current, total) => {
|
||||||
batchSize: 8,
|
showLoader(`Rendering page previews: ${current}/${total}`);
|
||||||
useLazyLoading: true,
|
},
|
||||||
lazyLoadMargin: '400px',
|
onBatchComplete: () => {
|
||||||
onProgress: (current, total) => {
|
createIcons({ icons });
|
||||||
showLoader(`Rendering page previews: ${current}/${total}`);
|
},
|
||||||
},
|
});
|
||||||
onBatchComplete: () => {
|
|
||||||
createIcons({ icons });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
initializePageGridSortable();
|
initializePageGridSortable();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -181,8 +191,10 @@ export async function processAndSave() {
|
|||||||
const finalPageElements = grid.querySelectorAll('.page-thumbnail');
|
const finalPageElements = grid.querySelectorAll('.page-thumbnail');
|
||||||
|
|
||||||
const finalIndices = Array.from(finalPageElements)
|
const finalIndices = Array.from(finalPageElements)
|
||||||
.map((el) => parseInt((el as HTMLElement).dataset.originalPageIndex || '', 10))
|
.map((el) =>
|
||||||
.filter(index => !isNaN(index) && index >= 0);
|
parseInt((el as HTMLElement).dataset.originalPageIndex || '', 10)
|
||||||
|
)
|
||||||
|
.filter((index) => !isNaN(index) && index >= 0);
|
||||||
|
|
||||||
console.log('Saving PDF with indices:', finalIndices);
|
console.log('Saving PDF with indices:', finalIndices);
|
||||||
console.log('Original PDF Page Count:', state.pdfDoc?.getPageCount());
|
console.log('Original PDF Page Count:', state.pdfDoc?.getPageCount());
|
||||||
@@ -195,10 +207,13 @@ export async function processAndSave() {
|
|||||||
const newPdfDoc = await PDFLibDocument.create();
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
|
|
||||||
const totalPages = state.pdfDoc.getPageCount();
|
const totalPages = state.pdfDoc.getPageCount();
|
||||||
const invalidIndices = finalIndices.filter(i => i >= totalPages);
|
const invalidIndices = finalIndices.filter((i) => i >= totalPages);
|
||||||
if (invalidIndices.length > 0) {
|
if (invalidIndices.length > 0) {
|
||||||
console.error('Found invalid indices:', invalidIndices);
|
console.error('Found invalid indices:', invalidIndices);
|
||||||
showAlert('Error', 'Some pages could not be processed. Please try again.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Some pages could not be processed. Please try again.'
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +227,10 @@ export async function processAndSave() {
|
|||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Save error:', e);
|
console.error('Save error:', e);
|
||||||
showAlert('Error', 'Failed to save the new PDF. Check console for details.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to save the new PDF. Check console for details.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
showWasmRequiredDialog,
|
showWasmRequiredDialog,
|
||||||
WasmProvider,
|
WasmProvider,
|
||||||
} from '../utils/wasm-provider.js';
|
} from '../utils/wasm-provider.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
const worker = new Worker(
|
const worker = new Worker(
|
||||||
import.meta.env.BASE_URL + 'workers/edit-attachments.worker.js'
|
import.meta.env.BASE_URL + 'workers/edit-attachments.worker.js'
|
||||||
@@ -327,14 +328,17 @@ async function updateUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileSelect(files: FileList | null) {
|
async function handleFileSelect(files: FileList | null) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (
|
if (
|
||||||
file.type === 'application/pdf' ||
|
file.type === 'application/pdf' ||
|
||||||
file.name.toLowerCase().endsWith('.pdf')
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
) {
|
) {
|
||||||
pageState.file = file;
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
|
if (!result) return;
|
||||||
|
result.pdf.destroy();
|
||||||
|
pageState.file = result.file;
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,367 +3,453 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PDFDocument as PDFLibDocument, PDFName, PDFString } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument, PDFName, PDFString } from 'pdf-lib';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
const pageState: EditMetadataState = {
|
const pageState: EditMetadataState = {
|
||||||
file: null,
|
file: null,
|
||||||
pdfDoc: null,
|
pdfDoc: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
pageState.file = null;
|
pageState.file = null;
|
||||||
pageState.pdfDoc = null;
|
pageState.pdfDoc = null;
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
|
|
||||||
// Clear form fields
|
// Clear form fields
|
||||||
const fields = ['meta-title', 'meta-author', 'meta-subject', 'meta-keywords', 'meta-creator', 'meta-producer', 'meta-creation-date', 'meta-mod-date'];
|
const fields = [
|
||||||
fields.forEach(function (fieldId) {
|
'meta-title',
|
||||||
const field = document.getElementById(fieldId) as HTMLInputElement;
|
'meta-author',
|
||||||
if (field) field.value = '';
|
'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
|
// Clear custom fields
|
||||||
const customFieldsContainer = document.getElementById('custom-fields-container');
|
const customFieldsContainer = document.getElementById(
|
||||||
if (customFieldsContainer) customFieldsContainer.innerHTML = '';
|
'custom-fields-container'
|
||||||
|
);
|
||||||
|
if (customFieldsContainer) customFieldsContainer.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateForInput(date: Date | undefined): string {
|
function formatDateForInput(date: Date | undefined): string {
|
||||||
if (!date) return '';
|
if (!date) return '';
|
||||||
try {
|
try {
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
const hours = String(date.getHours()).padStart(2, '0');
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCustomFieldRow(key: string = '', value: string = '') {
|
function addCustomFieldRow(key: string = '', value: string = '') {
|
||||||
const container = document.getElementById('custom-fields-container');
|
const container = document.getElementById('custom-fields-container');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'flex flex-col gap-2';
|
row.className = 'flex flex-col gap-2';
|
||||||
|
|
||||||
const keyInput = document.createElement('input');
|
const keyInput = document.createElement('input');
|
||||||
keyInput.type = 'text';
|
keyInput.type = 'text';
|
||||||
keyInput.placeholder = 'Key (e.g., Department)';
|
keyInput.placeholder = 'Key (e.g., Department)';
|
||||||
keyInput.value = key;
|
keyInput.value = key;
|
||||||
keyInput.className = 'custom-meta-key w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5';
|
keyInput.className =
|
||||||
|
'custom-meta-key w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5';
|
||||||
|
|
||||||
const valueInput = document.createElement('input');
|
const valueInput = document.createElement('input');
|
||||||
valueInput.type = 'text';
|
valueInput.type = 'text';
|
||||||
valueInput.placeholder = 'Value (e.g., Marketing)';
|
valueInput.placeholder = 'Value (e.g., Marketing)';
|
||||||
valueInput.value = value;
|
valueInput.value = value;
|
||||||
valueInput.className = 'custom-meta-value w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5';
|
valueInput.className =
|
||||||
|
'custom-meta-value w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5';
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.type = 'button';
|
removeBtn.type = 'button';
|
||||||
removeBtn.className = 'text-red-400 hover:text-red-300 p-2 self-center';
|
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.innerHTML = '<i data-lucide="trash-2" class="w-5 h-5"></i>';
|
||||||
removeBtn.onclick = function () {
|
removeBtn.onclick = function () {
|
||||||
row.remove();
|
row.remove();
|
||||||
};
|
};
|
||||||
|
|
||||||
row.append(keyInput, valueInput, removeBtn);
|
row.append(keyInput, valueInput, removeBtn);
|
||||||
container.appendChild(row);
|
container.appendChild(row);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateMetadataFields() {
|
function populateMetadataFields() {
|
||||||
if (!pageState.pdfDoc) return;
|
if (!pageState.pdfDoc) return;
|
||||||
|
|
||||||
const titleInput = document.getElementById('meta-title') as HTMLInputElement;
|
const titleInput = document.getElementById('meta-title') as HTMLInputElement;
|
||||||
const authorInput = document.getElementById('meta-author') as HTMLInputElement;
|
const authorInput = document.getElementById(
|
||||||
const subjectInput = document.getElementById('meta-subject') as HTMLInputElement;
|
'meta-author'
|
||||||
const keywordsInput = document.getElementById('meta-keywords') as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const creatorInput = document.getElementById('meta-creator') as HTMLInputElement;
|
const subjectInput = document.getElementById(
|
||||||
const producerInput = document.getElementById('meta-producer') as HTMLInputElement;
|
'meta-subject'
|
||||||
const creationDateInput = document.getElementById('meta-creation-date') as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const modDateInput = document.getElementById('meta-mod-date') as HTMLInputElement;
|
const keywordsInput = document.getElementById(
|
||||||
|
'meta-keywords'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const creatorInput = document.getElementById(
|
||||||
|
'meta-creator'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const producerInput = document.getElementById(
|
||||||
|
'meta-producer'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const creationDateInput = document.getElementById(
|
||||||
|
'meta-creation-date'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const modDateInput = document.getElementById(
|
||||||
|
'meta-mod-date'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
|
||||||
if (titleInput) titleInput.value = pageState.pdfDoc.getTitle() || '';
|
if (titleInput) titleInput.value = pageState.pdfDoc.getTitle() || '';
|
||||||
if (authorInput) authorInput.value = pageState.pdfDoc.getAuthor() || '';
|
if (authorInput) authorInput.value = pageState.pdfDoc.getAuthor() || '';
|
||||||
if (subjectInput) subjectInput.value = pageState.pdfDoc.getSubject() || '';
|
if (subjectInput) subjectInput.value = pageState.pdfDoc.getSubject() || '';
|
||||||
if (keywordsInput) keywordsInput.value = pageState.pdfDoc.getKeywords() || '';
|
if (keywordsInput) keywordsInput.value = pageState.pdfDoc.getKeywords() || '';
|
||||||
if (creatorInput) creatorInput.value = pageState.pdfDoc.getCreator() || '';
|
if (creatorInput) creatorInput.value = pageState.pdfDoc.getCreator() || '';
|
||||||
if (producerInput) producerInput.value = pageState.pdfDoc.getProducer() || '';
|
if (producerInput) producerInput.value = pageState.pdfDoc.getProducer() || '';
|
||||||
if (creationDateInput) creationDateInput.value = formatDateForInput(pageState.pdfDoc.getCreationDate());
|
if (creationDateInput)
|
||||||
if (modDateInput) modDateInput.value = formatDateForInput(pageState.pdfDoc.getModificationDate());
|
creationDateInput.value = formatDateForInput(
|
||||||
|
pageState.pdfDoc.getCreationDate()
|
||||||
|
);
|
||||||
|
if (modDateInput)
|
||||||
|
modDateInput.value = formatDateForInput(
|
||||||
|
pageState.pdfDoc.getModificationDate()
|
||||||
|
);
|
||||||
|
|
||||||
// Load custom fields
|
// Load custom fields
|
||||||
const customFieldsContainer = document.getElementById('custom-fields-container');
|
const customFieldsContainer = document.getElementById(
|
||||||
if (customFieldsContainer) customFieldsContainer.innerHTML = '';
|
'custom-fields-container'
|
||||||
|
);
|
||||||
|
if (customFieldsContainer) customFieldsContainer.innerHTML = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error getInfoDict is private but accessible at runtime
|
// @ts-expect-error getInfoDict is private but accessible at runtime
|
||||||
const infoDict = pageState.pdfDoc.getInfoDict();
|
const infoDict = pageState.pdfDoc.getInfoDict();
|
||||||
const standardKeys = new Set([
|
const standardKeys = new Set([
|
||||||
'Title', 'Author', 'Subject', 'Keywords', 'Creator',
|
'Title',
|
||||||
'Producer', 'CreationDate', 'ModDate'
|
'Author',
|
||||||
]);
|
'Subject',
|
||||||
|
'Keywords',
|
||||||
|
'Creator',
|
||||||
|
'Producer',
|
||||||
|
'CreationDate',
|
||||||
|
'ModDate',
|
||||||
|
]);
|
||||||
|
|
||||||
const allKeys = infoDict
|
const allKeys = infoDict.keys().map(function (key: {
|
||||||
.keys()
|
asString: () => string;
|
||||||
.map(function (key: { asString: () => string }) {
|
}) {
|
||||||
return key.asString().substring(1);
|
return key.asString().substring(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
allKeys.forEach(function (key: string) {
|
allKeys.forEach(function (key: string) {
|
||||||
if (!standardKeys.has(key)) {
|
if (!standardKeys.has(key)) {
|
||||||
const rawValue = infoDict.lookup(key);
|
const rawValue = infoDict.lookup(key);
|
||||||
let displayValue = '';
|
let displayValue = '';
|
||||||
|
|
||||||
if (rawValue && typeof rawValue.decodeText === 'function') {
|
if (rawValue && typeof rawValue.decodeText === 'function') {
|
||||||
displayValue = rawValue.decodeText();
|
displayValue = rawValue.decodeText();
|
||||||
} else if (rawValue && typeof rawValue.asString === 'function') {
|
} else if (rawValue && typeof rawValue.asString === 'function') {
|
||||||
displayValue = rawValue.asString();
|
displayValue = rawValue.asString();
|
||||||
} else if (rawValue) {
|
} else if (rawValue) {
|
||||||
displayValue = String(rawValue);
|
displayValue = String(rawValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
addCustomFieldRow(key, displayValue);
|
addCustomFieldRow(key, displayValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Could not read custom metadata fields:', e);
|
console.warn('Could not read custom metadata fields:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUI() {
|
async function updateUI() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
|
|
||||||
if (!fileDisplayArea) return;
|
if (!fileDisplayArea) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (pageState.file) {
|
if (pageState.file) {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = pageState.file.name;
|
nameSpan.textContent = pageState.file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
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.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.onclick = function () {
|
removeBtn.onclick = function () {
|
||||||
resetState();
|
resetState();
|
||||||
};
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showLoader('Loading PDF...');
|
const result = await loadPdfWithPasswordPrompt(pageState.file);
|
||||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
if (!result) {
|
||||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
resetState();
|
||||||
ignoreEncryption: true,
|
return;
|
||||||
throwOnInvalidObject: false
|
}
|
||||||
});
|
showLoader('Loading PDF...');
|
||||||
hideLoader();
|
result.pdf.destroy();
|
||||||
|
pageState.file = result.file;
|
||||||
|
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||||
|
throwOnInvalidObject: false,
|
||||||
|
});
|
||||||
|
hideLoader();
|
||||||
|
|
||||||
const pageCount = pageState.pdfDoc.getPageCount();
|
const pageCount = pageState.pdfDoc.getPageCount();
|
||||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||||
|
|
||||||
populateMetadataFields();
|
populateMetadataFields();
|
||||||
|
|
||||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading PDF:', error);
|
console.error('Error loading PDF:', error);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', 'Failed to load PDF file.');
|
showAlert('Error', 'Failed to load PDF file.');
|
||||||
resetState();
|
resetState();
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveMetadata() {
|
async function saveMetadata() {
|
||||||
if (!pageState.pdfDoc || !pageState.file) {
|
if (!pageState.pdfDoc || !pageState.file) {
|
||||||
showAlert('Error', 'Please upload a PDF first.');
|
showAlert('Error', 'Please upload a PDF first.');
|
||||||
return;
|
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...');
|
// Handle modification date
|
||||||
|
if (modDateInput.value) {
|
||||||
try {
|
pageState.pdfDoc.setModificationDate(new Date(modDateInput.value));
|
||||||
const titleInput = document.getElementById('meta-title') as HTMLInputElement;
|
} else {
|
||||||
const authorInput = document.getElementById('meta-author') as HTMLInputElement;
|
pageState.pdfDoc.setModificationDate(new Date());
|
||||||
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 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) {
|
function handleFileSelect(files: FileList | null) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
if (
|
||||||
pageState.file = file;
|
file.type === 'application/pdf' ||
|
||||||
updateUI();
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
}
|
) {
|
||||||
|
pageState.file = file;
|
||||||
|
updateUI();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
const addCustomFieldBtn = document.getElementById('add-custom-field');
|
const addCustomFieldBtn = document.getElementById('add-custom-field');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', function () {
|
backBtn.addEventListener('click', function () {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
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) {
|
fileInput.addEventListener('click', function () {
|
||||||
addCustomFieldBtn.addEventListener('click', function () {
|
fileInput.value = '';
|
||||||
addCustomFieldRow();
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
if (processBtn) {
|
||||||
fileInput.addEventListener('change', function (e) {
|
processBtn.addEventListener('click', saveMetadata);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createIcons, icons } from 'lucide';
|
|||||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||||
import { formatBytes, downloadFile } from '../utils/helpers.js';
|
import { formatBytes, downloadFile } from '../utils/helpers.js';
|
||||||
import { makeUniqueFileKey } from '../utils/deduplicate-filename.js';
|
import { makeUniqueFileKey } from '../utils/deduplicate-filename.js';
|
||||||
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
const embedPdfWasmUrl = new URL(
|
const embedPdfWasmUrl = new URL(
|
||||||
'embedpdf-snippet/dist/pdfium.wasm',
|
'embedpdf-snippet/dist/pdfium.wasm',
|
||||||
@@ -112,8 +113,17 @@ async function handleFiles(files: FileList) {
|
|||||||
|
|
||||||
if (!pdfWrapper || !pdfContainer || !fileDisplayArea) return;
|
if (!pdfWrapper || !pdfContainer || !fileDisplayArea) return;
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
const decryptedFiles = await batchDecryptIfNeeded(pdfFiles);
|
||||||
|
showLoader('Loading PDF Editor...');
|
||||||
|
|
||||||
|
if (decryptedFiles.length === 0) {
|
||||||
|
hideLoader();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isViewerInitialized) {
|
if (!isViewerInitialized) {
|
||||||
const firstFile = pdfFiles[0];
|
const firstFile = decryptedFiles[0];
|
||||||
const firstBuffer = await firstFile.arrayBuffer();
|
const firstBuffer = await firstFile.arrayBuffer();
|
||||||
|
|
||||||
pdfContainer.textContent = '';
|
pdfContainer.textContent = '';
|
||||||
@@ -163,7 +173,7 @@ async function handleFiles(files: FileList) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
addFileEntries(fileDisplayArea, pdfFiles);
|
addFileEntries(fileDisplayArea, decryptedFiles);
|
||||||
|
|
||||||
docManagerPlugin.openDocumentBuffer({
|
docManagerPlugin.openDocumentBuffer({
|
||||||
buffer: firstBuffer,
|
buffer: firstBuffer,
|
||||||
@@ -171,11 +181,11 @@ async function handleFiles(files: FileList) {
|
|||||||
autoActivate: true,
|
autoActivate: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 1; i < pdfFiles.length; i++) {
|
for (let i = 1; i < decryptedFiles.length; i++) {
|
||||||
const buffer = await pdfFiles[i].arrayBuffer();
|
const buffer = await decryptedFiles[i].arrayBuffer();
|
||||||
docManagerPlugin.openDocumentBuffer({
|
docManagerPlugin.openDocumentBuffer({
|
||||||
buffer,
|
buffer,
|
||||||
name: makeUniqueFileKey(i, pdfFiles[i].name),
|
name: makeUniqueFileKey(i, decryptedFiles[i].name),
|
||||||
autoActivate: false,
|
autoActivate: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -214,13 +224,13 @@ async function handleFiles(files: FileList) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
addFileEntries(fileDisplayArea, pdfFiles);
|
addFileEntries(fileDisplayArea, decryptedFiles);
|
||||||
|
|
||||||
for (let i = 0; i < pdfFiles.length; i++) {
|
for (let i = 0; i < decryptedFiles.length; i++) {
|
||||||
const buffer = await pdfFiles[i].arrayBuffer();
|
const buffer = await decryptedFiles[i].arrayBuffer();
|
||||||
docManagerPlugin.openDocumentBuffer({
|
docManagerPlugin.openDocumentBuffer({
|
||||||
buffer,
|
buffer,
|
||||||
name: makeUniqueFileKey(i, pdfFiles[i].name),
|
name: makeUniqueFileKey(i, decryptedFiles[i].name),
|
||||||
autoActivate: true,
|
autoActivate: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { showAlert } from '../ui.js';
|
import { showAlert, showLoader } from '../ui.js';
|
||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
showWasmRequiredDialog,
|
showWasmRequiredDialog,
|
||||||
WasmProvider,
|
WasmProvider,
|
||||||
} from '../utils/wasm-provider.js';
|
} from '../utils/wasm-provider.js';
|
||||||
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
const worker = new Worker(
|
const worker = new Worker(
|
||||||
import.meta.env.BASE_URL + 'workers/extract-attachments.worker.js'
|
import.meta.env.BASE_URL + 'workers/extract-attachments.worker.js'
|
||||||
@@ -198,6 +199,9 @@ async function extractAttachments() {
|
|||||||
showStatus('Reading files...', 'info');
|
showStatus('Reading files...', 'info');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
pageState.files = await batchDecryptIfNeeded(pageState.files);
|
||||||
|
showLoader('Reading files...');
|
||||||
|
|
||||||
const fileBuffers: ArrayBuffer[] = [];
|
const fileBuffers: ArrayBuffer[] = [];
|
||||||
const fileNames: string[] = [];
|
const fileNames: string[] = [];
|
||||||
|
|
||||||
@@ -207,6 +211,14 @@ async function extractAttachments() {
|
|||||||
fileNames.push(file.name);
|
fileNames.push(file.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fileBuffers.length === 0) {
|
||||||
|
if (processBtn) {
|
||||||
|
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||||
|
processBtn.removeAttribute('disabled');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
showStatus(
|
showStatus(
|
||||||
`Extracting attachments from ${pageState.files.length} file(s)...`,
|
`Extracting attachments from ${pageState.files.length} file(s)...`,
|
||||||
'info'
|
'info'
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide';
|
|||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
interface ExtractedImage {
|
interface ExtractedImage {
|
||||||
data: Uint8Array;
|
data: Uint8Array;
|
||||||
@@ -158,7 +159,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const decryptedFiles = await batchDecryptIfNeeded(state.files);
|
||||||
showLoader('Loading PDF processor...');
|
showLoader('Loading PDF processor...');
|
||||||
|
state.files = decryptedFiles;
|
||||||
const pymupdf = await loadPyMuPDF();
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
extractedImages = [];
|
extractedImages = [];
|
||||||
|
|||||||
@@ -1,209 +1,235 @@
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||||
import { readFileAsArrayBuffer, formatBytes, downloadFile, parsePageRanges } from '../utils/helpers.js';
|
import {
|
||||||
|
formatBytes,
|
||||||
|
downloadFile,
|
||||||
|
parsePageRanges,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { PDFDocument } from 'pdf-lib';
|
import { PDFDocument } from 'pdf-lib';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
interface ExtractState {
|
interface ExtractState {
|
||||||
file: File | null;
|
file: File | null;
|
||||||
pdfDoc: any;
|
pdfDoc: any;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractState: ExtractState = {
|
const extractState: ExtractState = {
|
||||||
file: null,
|
file: null,
|
||||||
pdfDoc: null,
|
pdfDoc: null,
|
||||||
totalPages: 0,
|
totalPages: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', initializePage);
|
document.addEventListener('DOMContentLoaded', initializePage);
|
||||||
} else {
|
} else {
|
||||||
initializePage();
|
initializePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializePage() {
|
function initializePage() {
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
|
|
||||||
if (fileInput) {
|
if (fileInput) {
|
||||||
fileInput.addEventListener('change', handleFileUpload);
|
fileInput.addEventListener('change', handleFileUpload);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dropZone) {
|
if (dropZone) {
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.add('bg-gray-700');
|
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;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) {
|
function handleFileUpload(e: Event) {
|
||||||
const input = e.target as HTMLInputElement;
|
const input = e.target as HTMLInputElement;
|
||||||
if (input.files && input.files.length > 0) {
|
if (input.files && input.files.length > 0) {
|
||||||
handleFile(input.files[0]);
|
handleFile(input.files[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFile(file: File) {
|
async function handleFile(file: File) {
|
||||||
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
|
if (
|
||||||
showAlert('Invalid File', 'Please select a PDF file.');
|
file.type !== 'application/pdf' &&
|
||||||
return;
|
!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...');
|
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 {
|
updateFileDisplay();
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
showOptions();
|
||||||
extractState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, {
|
hideLoader();
|
||||||
ignoreEncryption: true,
|
} catch (error) {
|
||||||
throwOnInvalidObject: false,
|
console.error('Error loading PDF:', error);
|
||||||
});
|
hideLoader();
|
||||||
extractState.totalPages = extractState.pdfDoc.getPageCount();
|
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() {
|
function updateFileDisplay() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (!fileDisplayArea || !extractState.file) return;
|
if (!fileDisplayArea || !extractState.file) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = extractState.file.name;
|
nameSpan.textContent = extractState.file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = `${formatBytes(extractState.file.size)} • ${extractState.totalPages} pages`;
|
metaSpan.textContent = `${formatBytes(extractState.file.size)} • ${extractState.totalPages} pages`;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
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.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.onclick = () => resetState();
|
removeBtn.onclick = () => resetState();
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
}
|
}
|
||||||
|
|
||||||
function showOptions() {
|
function showOptions() {
|
||||||
const extractOptions = document.getElementById('extract-options');
|
const extractOptions = document.getElementById('extract-options');
|
||||||
const totalPagesSpan = document.getElementById('total-pages');
|
const totalPagesSpan = document.getElementById('total-pages');
|
||||||
|
|
||||||
if (extractOptions) {
|
if (extractOptions) {
|
||||||
extractOptions.classList.remove('hidden');
|
extractOptions.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
if (totalPagesSpan) {
|
if (totalPagesSpan) {
|
||||||
totalPagesSpan.textContent = extractState.totalPages.toString();
|
totalPagesSpan.textContent = extractState.totalPages.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function extractPages() {
|
async function extractPages() {
|
||||||
const pagesInput = document.getElementById('pages-to-extract') as HTMLInputElement;
|
const pagesInput = document.getElementById(
|
||||||
if (!pagesInput || !pagesInput.value.trim()) {
|
'pages-to-extract'
|
||||||
showAlert('No Pages', 'Please enter page numbers to extract.');
|
) as HTMLInputElement;
|
||||||
return;
|
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);
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
if (pagesToExtract.length === 0) {
|
downloadFile(zipBlob, `${baseName}_extracted_pages.zip`);
|
||||||
showAlert('Invalid Pages', 'No valid page numbers found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoader('Extracting pages...');
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
try {
|
'Success',
|
||||||
const zip = new JSZip();
|
`Extracted ${pagesToExtract.length} page(s) successfully!`,
|
||||||
const baseName = extractState.file?.name.replace('.pdf', '') || 'document';
|
'success',
|
||||||
|
() => {
|
||||||
for (const pageNum of pagesToExtract) {
|
resetState();
|
||||||
const newPdf = await PDFDocument.create();
|
}
|
||||||
const [copiedPage] = await newPdf.copyPages(extractState.pdfDoc, [pageNum - 1]);
|
);
|
||||||
newPdf.addPage(copiedPage);
|
} catch (error) {
|
||||||
const pdfBytes = await newPdf.save();
|
console.error('Error extracting pages:', error);
|
||||||
zip.file(`${baseName}_page_${pageNum}.pdf`, pdfBytes);
|
hideLoader();
|
||||||
}
|
showAlert('Error', 'Failed to extract pages.');
|
||||||
|
}
|
||||||
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.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
extractState.file = null;
|
extractState.file = null;
|
||||||
extractState.pdfDoc = null;
|
extractState.pdfDoc = null;
|
||||||
extractState.totalPages = 0;
|
extractState.totalPages = 0;
|
||||||
|
|
||||||
const extractOptions = document.getElementById('extract-options');
|
const extractOptions = document.getElementById('extract-options');
|
||||||
if (extractOptions) {
|
if (extractOptions) {
|
||||||
extractOptions.classList.add('hidden');
|
extractOptions.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) {
|
if (fileDisplayArea) {
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const pagesInput = document.getElementById('pages-to-extract') as HTMLInputElement;
|
const pagesInput = document.getElementById(
|
||||||
if (pagesInput) {
|
'pages-to-extract'
|
||||||
pagesInput.value = '';
|
) as HTMLInputElement;
|
||||||
}
|
if (pagesInput) {
|
||||||
|
pagesInput.value = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import JSZip from 'jszip';
|
|||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
let file: File | null = null;
|
let file: File | null = null;
|
||||||
|
|
||||||
const updateUI = () => {
|
const updateUI = () => {
|
||||||
@@ -93,6 +94,13 @@ async function extract() {
|
|||||||
try {
|
try {
|
||||||
showLoader('Loading Engine...');
|
showLoader('Loading Engine...');
|
||||||
const pymupdf = await loadPyMuPDF();
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
const pwResult = await loadPdfWithPasswordPrompt(file);
|
||||||
|
if (!pwResult) return;
|
||||||
|
pwResult.pdf.destroy();
|
||||||
|
file = pwResult.file;
|
||||||
|
|
||||||
showLoader('Extracting tables...');
|
showLoader('Extracting tables...');
|
||||||
|
|
||||||
const doc = await pymupdf.open(file);
|
const doc = await pymupdf.open(file);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { showAlert } from '../ui.js';
|
import { showAlert } from '../ui.js';
|
||||||
import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js';
|
import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js';
|
||||||
import { fixPageSize as fixPageSizeCore } from '../utils/pdf-operations';
|
import { fixPageSize as fixPageSizeCore } from '../utils/pdf-operations';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
import { FixPageSizeState } from '@/types';
|
import { FixPageSizeState } from '@/types';
|
||||||
|
|
||||||
@@ -64,14 +65,17 @@ async function updateUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileSelect(files: FileList | null) {
|
async function handleFileSelect(files: FileList | null) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (
|
if (
|
||||||
file.type === 'application/pdf' ||
|
file.type === 'application/pdf' ||
|
||||||
file.name.toLowerCase().endsWith('.pdf')
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
) {
|
) {
|
||||||
pageState.file = file;
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
|
if (!result) return;
|
||||||
|
result.pdf.destroy();
|
||||||
|
pageState.file = result.file;
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { showAlert } from '../ui.js';
|
import { showAlert } from '../ui.js';
|
||||||
import {
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
downloadFile,
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
formatBytes,
|
|
||||||
readFileAsArrayBuffer,
|
|
||||||
} from '../utils/helpers.js';
|
|
||||||
import { PDFDocument } from 'pdf-lib';
|
import { PDFDocument } from 'pdf-lib';
|
||||||
import { flattenAnnotations } from '../utils/flatten-annotations.js';
|
import { flattenAnnotations } from '../utils/flatten-annotations.js';
|
||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
@@ -109,23 +106,22 @@ async function flattenPdf() {
|
|||||||
const loaderModal = document.getElementById('loader-modal');
|
const loaderModal = document.getElementById('loader-modal');
|
||||||
const loaderText = document.getElementById('loader-text');
|
const loaderText = document.getElementById('loader-text');
|
||||||
|
|
||||||
|
pageState.files = await batchDecryptIfNeeded(pageState.files);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (pageState.files.length === 1) {
|
if (pageState.files.length === 1) {
|
||||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||||
if (loaderText) loaderText.textContent = 'Flattening PDF...';
|
if (loaderText) loaderText.textContent = 'Flattening PDF...';
|
||||||
|
|
||||||
const file = pageState.files[0];
|
const file = pageState.files[0];
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, {
|
const pdfDoc = await PDFDocument.load(arrayBuffer);
|
||||||
ignoreEncryption: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
flattenFormsInDoc(pdfDoc);
|
flattenFormsInDoc(pdfDoc);
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
if (e.message.includes('getForm')) {
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
// Ignore if no form found
|
if (!msg.includes('getForm')) {
|
||||||
} else {
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,17 +153,14 @@ async function flattenPdf() {
|
|||||||
loaderText.textContent = `Flattening ${i + 1}/${pageState.files.length}: ${file.name}...`;
|
loaderText.textContent = `Flattening ${i + 1}/${pageState.files.length}: ${file.name}...`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, {
|
const pdfDoc = await PDFDocument.load(arrayBuffer);
|
||||||
ignoreEncryption: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
flattenFormsInDoc(pdfDoc);
|
flattenFormsInDoc(pdfDoc);
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
if (e.message.includes('getForm')) {
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
// Ignore if no form found
|
if (!msg.includes('getForm')) {
|
||||||
} else {
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,10 +200,12 @@ async function flattenPdf() {
|
|||||||
}
|
}
|
||||||
if (loaderModal) loaderModal.classList.add('hidden');
|
if (loaderModal) loaderModal.classList.add('hidden');
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
if (loaderModal) loaderModal.classList.add('hidden');
|
if (loaderModal) loaderModal.classList.add('hidden');
|
||||||
showAlert('Error', e.message || 'An unexpected error occurred.');
|
const errorMessage =
|
||||||
|
e instanceof Error ? e.message : 'An unexpected error occurred.';
|
||||||
|
showAlert('Error', errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
|
|||||||
import { convertFileToOutlines } from '../utils/ghostscript-loader.js';
|
import { convertFileToOutlines } from '../utils/ghostscript-loader.js';
|
||||||
import { isGhostscriptAvailable } from '../utils/ghostscript-dynamic-loader.js';
|
import { isGhostscriptAvailable } from '../utils/ghostscript-dynamic-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||||
@@ -107,6 +108,8 @@ async function processFiles() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pageState.files = await batchDecryptIfNeeded(pageState.files);
|
||||||
|
|
||||||
const loaderModal = document.getElementById('loader-modal');
|
const loaderModal = document.getElementById('loader-modal');
|
||||||
const loaderText = document.getElementById('loader-text');
|
const loaderText = document.getElementById('loader-text');
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type PdfViewerWindow = Window & {
|
|||||||
|
|
||||||
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
||||||
import { downloadFile, hexToRgb, getPDFDocument } from '../utils/helpers.js';
|
import { downloadFile, hexToRgb, getPDFDocument } from '../utils/helpers.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
||||||
@@ -3135,7 +3136,10 @@ function extractExistingFields(pdfDoc: PDFDocument): void {
|
|||||||
|
|
||||||
async function handlePdfUpload(file: File) {
|
async function handlePdfUpload(file: File) {
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
|
if (!result) return;
|
||||||
|
const arrayBuffer = result.bytes;
|
||||||
|
uploadedPdfjsDoc = result.pdf;
|
||||||
uploadedPdfDoc = await PDFDocument.load(arrayBuffer);
|
uploadedPdfDoc = await PDFDocument.load(arrayBuffer);
|
||||||
|
|
||||||
// Check for existing fields and update counter
|
// Check for existing fields and update counter
|
||||||
@@ -3173,8 +3177,6 @@ async function handlePdfUpload(file: File) {
|
|||||||
console.log('No form fields found or error reading fields:', e);
|
console.log('No form fields found or error reading fields:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadedPdfjsDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
|
||||||
|
|
||||||
const pageCount = uploadedPdfDoc.getPageCount();
|
const pageCount = uploadedPdfDoc.getPageCount();
|
||||||
pages = [];
|
pages = [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Self-contained Form Filler logic for standalone page
|
// Self-contained Form Filler logic for standalone page
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { getPDFDocument } from '../utils/helpers.js';
|
import { getPDFDocument } from '../utils/helpers.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
let viewerIframe: HTMLIFrameElement | null = null;
|
let viewerIframe: HTMLIFrameElement | null = null;
|
||||||
let viewerReady = false;
|
let viewerReady = false;
|
||||||
@@ -8,46 +9,52 @@ let currentFile: File | null = null;
|
|||||||
|
|
||||||
// UI helpers
|
// UI helpers
|
||||||
function showLoader(message: string = 'Processing...') {
|
function showLoader(message: string = 'Processing...') {
|
||||||
const loader = document.getElementById('loader-modal');
|
const loader = document.getElementById('loader-modal');
|
||||||
const loaderText = document.getElementById('loader-text');
|
const loaderText = document.getElementById('loader-text');
|
||||||
if (loader) loader.classList.remove('hidden');
|
if (loader) loader.classList.remove('hidden');
|
||||||
if (loaderText) loaderText.textContent = message;
|
if (loaderText) loaderText.textContent = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideLoader() {
|
function hideLoader() {
|
||||||
const loader = document.getElementById('loader-modal');
|
const loader = document.getElementById('loader-modal');
|
||||||
if (loader) loader.classList.add('hidden');
|
if (loader) loader.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAlert(title: string, message: string, type: string = 'error', callback?: () => void) {
|
function showAlert(
|
||||||
const modal = document.getElementById('alert-modal');
|
title: string,
|
||||||
const alertTitle = document.getElementById('alert-title');
|
message: string,
|
||||||
const alertMessage = document.getElementById('alert-message');
|
type: string = 'error',
|
||||||
const okBtn = document.getElementById('alert-ok');
|
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 (alertTitle) alertTitle.textContent = title;
|
||||||
if (alertMessage) alertMessage.textContent = message;
|
if (alertMessage) alertMessage.textContent = message;
|
||||||
if (modal) modal.classList.remove('hidden');
|
if (modal) modal.classList.remove('hidden');
|
||||||
|
|
||||||
if (okBtn) {
|
if (okBtn) {
|
||||||
const newOkBtn = okBtn.cloneNode(true) as HTMLElement;
|
const newOkBtn = okBtn.cloneNode(true) as HTMLElement;
|
||||||
okBtn.replaceWith(newOkBtn);
|
okBtn.replaceWith(newOkBtn);
|
||||||
newOkBtn.addEventListener('click', () => {
|
newOkBtn.addEventListener('click', () => {
|
||||||
modal?.classList.add('hidden');
|
modal?.classList.add('hidden');
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFileDisplay() {
|
function updateFileDisplay() {
|
||||||
const displayArea = document.getElementById('file-display-area');
|
const displayArea = document.getElementById('file-display-area');
|
||||||
if (!displayArea || !currentFile) return;
|
if (!displayArea || !currentFile) return;
|
||||||
|
|
||||||
const fileSize = currentFile.size < 1024 * 1024
|
const fileSize =
|
||||||
? `${(currentFile.size / 1024).toFixed(1)} KB`
|
currentFile.size < 1024 * 1024
|
||||||
: `${(currentFile.size / 1024 / 1024).toFixed(2)} MB`;
|
? `${(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="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 items-center justify-between">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
@@ -61,196 +68,226 @@ function updateFileDisplay() {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
document.getElementById('remove-file')?.addEventListener('click', () => resetState());
|
document
|
||||||
|
.getElementById('remove-file')
|
||||||
|
?.addEventListener('click', () => resetState());
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
viewerIframe = null;
|
viewerIframe = null;
|
||||||
viewerReady = false;
|
viewerReady = false;
|
||||||
currentFile = null;
|
currentFile = null;
|
||||||
const displayArea = document.getElementById('file-display-area');
|
const displayArea = document.getElementById('file-display-area');
|
||||||
if (displayArea) displayArea.innerHTML = '';
|
if (displayArea) displayArea.innerHTML = '';
|
||||||
document.getElementById('form-filler-options')?.classList.add('hidden');
|
document.getElementById('form-filler-options')?.classList.add('hidden');
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
|
|
||||||
// Clear viewer
|
// Clear viewer
|
||||||
const viewerContainer = document.getElementById('pdf-viewer-container');
|
const viewerContainer = document.getElementById('pdf-viewer-container');
|
||||||
if (viewerContainer) {
|
if (viewerContainer) {
|
||||||
viewerContainer.innerHTML = '';
|
viewerContainer.innerHTML = '';
|
||||||
viewerContainer.style.height = '';
|
viewerContainer.style.height = '';
|
||||||
viewerContainer.style.aspectRatio = '';
|
viewerContainer.style.aspectRatio = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolUploader = document.getElementById('tool-uploader');
|
const toolUploader = document.getElementById('tool-uploader');
|
||||||
const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
|
const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
|
||||||
if (toolUploader && !isFullWidth) {
|
if (toolUploader && !isFullWidth) {
|
||||||
toolUploader.classList.remove('max-w-6xl');
|
toolUploader.classList.remove('max-w-6xl');
|
||||||
toolUploader.classList.add('max-w-2xl');
|
toolUploader.classList.add('max-w-2xl');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// File handling
|
// File handling
|
||||||
async function handleFileUpload(file: File) {
|
async function handleFileUpload(file: File) {
|
||||||
if (!file || file.type !== 'application/pdf') {
|
if (!file || file.type !== 'application/pdf') {
|
||||||
showAlert('Error', 'Please upload a valid PDF file.');
|
showAlert('Error', 'Please upload a valid PDF file.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentFile = file;
|
try {
|
||||||
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
|
if (!result) return;
|
||||||
|
result.pdf.destroy();
|
||||||
|
currentFile = result.file;
|
||||||
updateFileDisplay();
|
updateFileDisplay();
|
||||||
await setupFormViewer();
|
await setupFormViewer();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
showAlert('Error', 'Failed to load PDF file.');
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function adjustViewerHeight(file: File) {
|
async function adjustViewerHeight(file: File) {
|
||||||
const viewerContainer = document.getElementById('pdf-viewer-container');
|
const viewerContainer = document.getElementById('pdf-viewer-container');
|
||||||
if (!viewerContainer) return;
|
if (!viewerContainer) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
const loadingTask = getPDFDocument({ data: arrayBuffer });
|
const loadingTask = getPDFDocument({ data: arrayBuffer });
|
||||||
const pdf = await loadingTask.promise;
|
const pdf = await loadingTask.promise;
|
||||||
const page = await pdf.getPage(1);
|
const page = await pdf.getPage(1);
|
||||||
const viewport = page.getViewport({ scale: 1 });
|
const viewport = page.getViewport({ scale: 1 });
|
||||||
|
|
||||||
// Add ~50px for toolbar height
|
// Add ~50px for toolbar height
|
||||||
const aspectRatio = viewport.width / (viewport.height + 50);
|
const aspectRatio = viewport.width / (viewport.height + 50);
|
||||||
|
|
||||||
viewerContainer.style.height = 'auto';
|
viewerContainer.style.height = 'auto';
|
||||||
viewerContainer.style.aspectRatio = `${aspectRatio}`;
|
viewerContainer.style.aspectRatio = `${aspectRatio}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error adjusting viewer height:', e);
|
console.error('Error adjusting viewer height:', e);
|
||||||
viewerContainer.style.height = '80vh';
|
viewerContainer.style.height = '80vh';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupFormViewer() {
|
async function setupFormViewer() {
|
||||||
if (!currentFile) return;
|
if (!currentFile) return;
|
||||||
|
|
||||||
showLoader('Loading PDF form...');
|
showLoader('Loading PDF form...');
|
||||||
const pdfViewerContainer = document.getElementById('pdf-viewer-container');
|
const pdfViewerContainer = document.getElementById('pdf-viewer-container');
|
||||||
|
|
||||||
if (!pdfViewerContainer) {
|
if (!pdfViewerContainer) {
|
||||||
console.error('PDF viewer container not found');
|
console.error('PDF viewer container not found');
|
||||||
hideLoader();
|
hideLoader();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolUploader = document.getElementById('tool-uploader');
|
const toolUploader = document.getElementById('tool-uploader');
|
||||||
// Default to true if not set
|
// Default to true if not set
|
||||||
const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
|
const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
|
||||||
if (toolUploader && !isFullWidth) {
|
if (toolUploader && !isFullWidth) {
|
||||||
toolUploader.classList.remove('max-w-2xl');
|
toolUploader.classList.remove('max-w-2xl');
|
||||||
toolUploader.classList.add('max-w-6xl');
|
toolUploader.classList.add('max-w-6xl');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Apply dynamic height
|
// Apply dynamic height
|
||||||
await adjustViewerHeight(currentFile);
|
await adjustViewerHeight(currentFile);
|
||||||
|
|
||||||
pdfViewerContainer.innerHTML = '';
|
pdfViewerContainer.innerHTML = '';
|
||||||
|
|
||||||
const arrayBuffer = await currentFile.arrayBuffer();
|
const arrayBuffer = await currentFile.arrayBuffer();
|
||||||
const blob = new Blob([arrayBuffer], { type: 'application/pdf' });
|
const blob = new Blob([arrayBuffer], { type: 'application/pdf' });
|
||||||
const blobUrl = URL.createObjectURL(blob);
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
viewerIframe = document.createElement('iframe');
|
viewerIframe = document.createElement('iframe');
|
||||||
viewerIframe.src = `${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html?file=${encodeURIComponent(blobUrl)}`;
|
viewerIframe.src = `${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html?file=${encodeURIComponent(blobUrl)}`;
|
||||||
viewerIframe.style.width = '100%';
|
viewerIframe.style.width = '100%';
|
||||||
viewerIframe.style.height = '100%';
|
viewerIframe.style.height = '100%';
|
||||||
viewerIframe.style.border = 'none';
|
viewerIframe.style.border = 'none';
|
||||||
|
|
||||||
viewerIframe.onload = () => {
|
viewerIframe.onload = () => {
|
||||||
viewerReady = true;
|
viewerReady = true;
|
||||||
hideLoader();
|
hideLoader();
|
||||||
};
|
};
|
||||||
|
|
||||||
pdfViewerContainer.appendChild(viewerIframe);
|
pdfViewerContainer.appendChild(viewerIframe);
|
||||||
|
|
||||||
const formFillerOptions = document.getElementById('form-filler-options');
|
const formFillerOptions = document.getElementById('form-filler-options');
|
||||||
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
|
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Critical error setting up form filler:', e);
|
console.error('Critical error setting up form filler:', e);
|
||||||
showAlert('Error', 'Failed to load PDF form viewer.');
|
showAlert('Error', 'Failed to load PDF form viewer.');
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processAndDownloadForm() {
|
async function processAndDownloadForm() {
|
||||||
if (!viewerIframe || !viewerReady) {
|
if (!viewerIframe || !viewerReady) {
|
||||||
showAlert('Viewer not ready', 'Please wait for the form to finish loading.');
|
showAlert(
|
||||||
return;
|
'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 viewerDoc = viewerWindow.document;
|
||||||
const viewerWindow = viewerIframe.contentWindow;
|
if (!viewerDoc) {
|
||||||
if (!viewerWindow) {
|
console.error('Cannot access iframe document');
|
||||||
console.error('Cannot access iframe window');
|
showAlert(
|
||||||
showAlert('Download', 'Please use the Download button in the PDF viewer toolbar above.');
|
'Download',
|
||||||
return;
|
'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 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
|
// Initialize
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
fileInput?.addEventListener('change', (e) => {
|
fileInput?.addEventListener('change', (e) => {
|
||||||
const file = (e.target as HTMLInputElement).files?.[0];
|
const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
if (file) handleFileUpload(file);
|
if (file) handleFileUpload(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone?.addEventListener('dragover', (e) => {
|
dropZone?.addEventListener('dragover', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.add('border-indigo-500');
|
dropZone.classList.add('border-indigo-500');
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone?.addEventListener('dragleave', () => {
|
dropZone?.addEventListener('dragleave', () => {
|
||||||
dropZone.classList.remove('border-indigo-500');
|
dropZone.classList.remove('border-indigo-500');
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone?.addEventListener('drop', (e) => {
|
dropZone?.addEventListener('drop', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.remove('border-indigo-500');
|
dropZone.classList.remove('border-indigo-500');
|
||||||
const file = e.dataTransfer?.files[0];
|
const file = e.dataTransfer?.files[0];
|
||||||
if (file) handleFileUpload(file);
|
if (file) handleFileUpload(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
processBtn?.addEventListener('click', processAndDownloadForm);
|
processBtn?.addEventListener('click', processAndDownloadForm);
|
||||||
|
|
||||||
backBtn?.addEventListener('click', () => {
|
backBtn?.addEventListener('click', () => {
|
||||||
window.location.href = '../../index.html';
|
window.location.href = '../../index.html';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,132 +1,260 @@
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||||
import { downloadFile, hexToRgb, formatBytes, parsePageRanges } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
hexToRgb,
|
||||||
|
formatBytes,
|
||||||
|
parsePageRanges,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import { HeaderFooterState } from '@/types';
|
import { HeaderFooterState } from '@/types';
|
||||||
|
|
||||||
const pageState: HeaderFooterState = { file: null, pdfDoc: null };
|
const pageState: HeaderFooterState = { file: null, pdfDoc: null };
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', initializePage);
|
document.addEventListener('DOMContentLoaded', initializePage);
|
||||||
} else { initializePage(); }
|
} else {
|
||||||
|
initializePage();
|
||||||
function initializePage() {
|
|
||||||
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); }
|
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) {
|
async function handleFiles(files: FileList) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (!file || file.type !== 'application/pdf') { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; }
|
if (!file || file.type !== 'application/pdf') {
|
||||||
|
showAlert('Invalid File', 'Please upload a valid PDF file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
|
if (!result) return;
|
||||||
showLoader('Loading PDF...');
|
showLoader('Loading PDF...');
|
||||||
try {
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
pageState.pdfDoc = await PDFLibDocument.load(result.bytes);
|
||||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
pageState.file = result.file;
|
||||||
pageState.file = file;
|
result.pdf.destroy();
|
||||||
updateFileDisplay();
|
|
||||||
document.getElementById('options-panel')?.classList.remove('hidden');
|
updateFileDisplay();
|
||||||
const totalPagesSpan = document.getElementById('total-pages');
|
document.getElementById('options-panel')?.classList.remove('hidden');
|
||||||
if (totalPagesSpan) totalPagesSpan.textContent = String(pageState.pdfDoc.getPageCount());
|
const totalPagesSpan = document.getElementById('total-pages');
|
||||||
} catch (error) { console.error(error); showAlert('Error', 'Failed to load PDF file.'); }
|
if (totalPagesSpan)
|
||||||
finally { hideLoader(); }
|
totalPagesSpan.textContent = String(pageState.pdfDoc.getPageCount());
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
showAlert('Error', 'Failed to load PDF file.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFileDisplay() {
|
function updateFileDisplay() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
|
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
fileDiv.className =
|
||||||
const infoContainer = document.createElement('div');
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
||||||
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
const infoContainer = document.createElement('div');
|
||||||
const nameSpan = document.createElement('div');
|
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.textContent = pageState.file.name;
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
const metaSpan = document.createElement('div');
|
nameSpan.textContent = pageState.file.name;
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`;
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`;
|
||||||
const removeBtn = document.createElement('button');
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.onclick = resetState;
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
removeBtn.onclick = resetState;
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
createIcons({ icons });
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
createIcons({ icons });
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
pageState.file = null; pageState.pdfDoc = null;
|
pageState.file = null;
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
pageState.pdfDoc = null;
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
document.getElementById('options-panel')?.classList.add('hidden');
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
document.getElementById('options-panel')?.classList.add('hidden');
|
||||||
if (fileInput) fileInput.value = '';
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
|
if (fileInput) fileInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addHeaderFooter() {
|
async function addHeaderFooter() {
|
||||||
if (!pageState.pdfDoc) { showAlert('Error', 'Please upload a PDF file first.'); return; }
|
if (!pageState.pdfDoc) {
|
||||||
showLoader('Adding header & footer...');
|
showAlert('Error', 'Please upload a PDF file first.');
|
||||||
try {
|
return;
|
||||||
const helveticaFont = await pageState.pdfDoc.embedFont(StandardFonts.Helvetica);
|
}
|
||||||
const allPages = pageState.pdfDoc.getPages();
|
showLoader('Adding header & footer...');
|
||||||
const totalPages = allPages.length;
|
try {
|
||||||
const margin = 40;
|
const helveticaFont = await pageState.pdfDoc.embedFont(
|
||||||
const fontSize = parseInt((document.getElementById('font-size') as HTMLInputElement)?.value || '10') || 10;
|
StandardFonts.Helvetica
|
||||||
const colorHex = (document.getElementById('font-color') as HTMLInputElement)?.value || '#000000';
|
);
|
||||||
const fontColor = hexToRgb(colorHex);
|
const allPages = pageState.pdfDoc.getPages();
|
||||||
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement)?.value || '';
|
const totalPages = allPages.length;
|
||||||
const texts = {
|
const margin = 40;
|
||||||
headerLeft: (document.getElementById('header-left') as HTMLInputElement)?.value || '',
|
const fontSize =
|
||||||
headerCenter: (document.getElementById('header-center') as HTMLInputElement)?.value || '',
|
parseInt(
|
||||||
headerRight: (document.getElementById('header-right') as HTMLInputElement)?.value || '',
|
(document.getElementById('font-size') as HTMLInputElement)?.value ||
|
||||||
footerLeft: (document.getElementById('footer-left') as HTMLInputElement)?.value || '',
|
'10'
|
||||||
footerCenter: (document.getElementById('footer-center') as HTMLInputElement)?.value || '',
|
) || 10;
|
||||||
footerRight: (document.getElementById('footer-right') as HTMLInputElement)?.value || '',
|
const colorHex =
|
||||||
};
|
(document.getElementById('font-color') as HTMLInputElement)?.value ||
|
||||||
const indicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
'#000000';
|
||||||
if (indicesToProcess.length === 0) throw new Error("Invalid page range specified.");
|
const fontColor = hexToRgb(colorHex);
|
||||||
const drawOptions = { font: helveticaFont, size: fontSize, color: rgb(fontColor.r, fontColor.g, fontColor.b) };
|
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) {
|
for (const pageIndex of indicesToProcess) {
|
||||||
const page = allPages[pageIndex];
|
const page = allPages[pageIndex];
|
||||||
const { width, height } = page.getSize();
|
const { width, height } = page.getSize();
|
||||||
const pageNumber = pageIndex + 1;
|
const pageNumber = pageIndex + 1;
|
||||||
const processText = (text: string) => text.replace(/{page}/g, String(pageNumber)).replace(/{total}/g, String(totalPages));
|
const processText = (text: string) =>
|
||||||
const processed = {
|
text
|
||||||
headerLeft: processText(texts.headerLeft), headerCenter: processText(texts.headerCenter), headerRight: processText(texts.headerRight),
|
.replace(/{page}/g, String(pageNumber))
|
||||||
footerLeft: processText(texts.footerLeft), footerCenter: processText(texts.footerCenter), footerRight: processText(texts.footerRight),
|
.replace(/{total}/g, String(totalPages));
|
||||||
};
|
const processed = {
|
||||||
if (processed.headerLeft) page.drawText(processed.headerLeft, { ...drawOptions, x: margin, y: height - margin });
|
headerLeft: processText(texts.headerLeft),
|
||||||
if (processed.headerCenter) page.drawText(processed.headerCenter, { ...drawOptions, x: width / 2 - helveticaFont.widthOfTextAtSize(processed.headerCenter, fontSize) / 2, y: height - margin });
|
headerCenter: processText(texts.headerCenter),
|
||||||
if (processed.headerRight) page.drawText(processed.headerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processed.headerRight, fontSize), y: height - margin });
|
headerRight: processText(texts.headerRight),
|
||||||
if (processed.footerLeft) page.drawText(processed.footerLeft, { ...drawOptions, x: margin, y: margin });
|
footerLeft: processText(texts.footerLeft),
|
||||||
if (processed.footerCenter) page.drawText(processed.footerCenter, { ...drawOptions, x: width / 2 - helveticaFont.widthOfTextAtSize(processed.footerCenter, fontSize) / 2, y: margin });
|
footerCenter: processText(texts.footerCenter),
|
||||||
if (processed.footerRight) page.drawText(processed.footerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processed.footerRight, fontSize), y: margin });
|
footerRight: processText(texts.footerRight),
|
||||||
}
|
};
|
||||||
const newPdfBytes = await pageState.pdfDoc.save();
|
if (processed.headerLeft)
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'header-footer-added.pdf');
|
page.drawText(processed.headerLeft, {
|
||||||
showAlert('Success', 'Header & Footer added successfully!', 'success', () => { resetState(); });
|
...drawOptions,
|
||||||
} catch (e: any) { console.error(e); showAlert('Error', e.message || 'Could not add header or footer.'); }
|
x: margin,
|
||||||
finally { hideLoader(); }
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
|||||||
import { applyInvertColors } from '../utils/image-effects.js';
|
import { applyInvertColors } from '../utils/image-effects.js';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { InvertColorsState } from '@/types';
|
import { InvertColorsState } from '@/types';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -64,11 +65,13 @@ async function handleFiles(files: FileList) {
|
|||||||
showAlert('Invalid File', 'Please upload a valid PDF file.');
|
showAlert('Invalid File', 'Please upload a valid PDF file.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader('Loading PDF...');
|
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
if (!result) return;
|
||||||
pageState.file = file;
|
showLoader('Loading PDF...');
|
||||||
|
result.pdf.destroy();
|
||||||
|
pageState.pdfDoc = await PDFLibDocument.load(result.bytes);
|
||||||
|
pageState.file = result.file;
|
||||||
updateFileDisplay();
|
updateFileDisplay();
|
||||||
document.getElementById('options-panel')?.classList.remove('hidden');
|
document.getElementById('options-panel')?.classList.remove('hidden');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import {
|
import { downloadFile } from '../utils/helpers.js';
|
||||||
downloadFile,
|
|
||||||
readFileAsArrayBuffer,
|
|
||||||
getPDFDocument,
|
|
||||||
} from '../utils/helpers.js';
|
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
import {
|
import {
|
||||||
renderPagesProgressively,
|
renderPagesProgressively,
|
||||||
cleanupLazyRendering,
|
cleanupLazyRendering,
|
||||||
@@ -453,15 +450,23 @@ export async function refreshMergeUI() {
|
|||||||
mergeState.pdfDocs = {};
|
mergeState.pdfDocs = {};
|
||||||
mergeState.pdfBytes = {};
|
mergeState.pdfBytes = {};
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
state.files = await batchDecryptIfNeeded(state.files);
|
||||||
|
showLoader('Loading PDF documents...');
|
||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
const file = state.files[i];
|
const file = state.files[i];
|
||||||
const fileKey = `${i}_${file.name}`;
|
const fileKey = `${i}_${file.name}`;
|
||||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
|
||||||
mergeState.pdfBytes[fileKey] = pdfBytes as ArrayBuffer;
|
|
||||||
|
|
||||||
const bytesForPdfJs = (pdfBytes as ArrayBuffer).slice(0);
|
const bytes = await file.arrayBuffer();
|
||||||
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
|
const pdf = await pdfjsLib.getDocument({ data: bytes.slice(0) }).promise;
|
||||||
mergeState.pdfDocs[fileKey] = pdfjsDoc;
|
mergeState.pdfBytes[fileKey] = bytes;
|
||||||
|
mergeState.pdfDocs[fileKey] = pdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.files.length === 0) {
|
||||||
|
hideLoader();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading PDFs:', error);
|
console.error('Error loading PDFs:', error);
|
||||||
|
|||||||
@@ -2,265 +2,304 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js';
|
import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
interface NUpState {
|
interface NUpState {
|
||||||
file: File | null;
|
file: File | null;
|
||||||
pdfDoc: PDFLibDocument | null;
|
pdfDoc: PDFLibDocument | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageState: NUpState = {
|
const pageState: NUpState = {
|
||||||
file: null,
|
file: null,
|
||||||
pdfDoc: null,
|
pdfDoc: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
pageState.file = null;
|
pageState.file = null;
|
||||||
pageState.pdfDoc = null;
|
pageState.pdfDoc = null;
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUI() {
|
async function updateUI() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
|
|
||||||
if (!fileDisplayArea) return;
|
if (!fileDisplayArea) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (pageState.file) {
|
if (pageState.file) {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = pageState.file.name;
|
nameSpan.textContent = pageState.file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
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.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.onclick = function () {
|
removeBtn.onclick = function () {
|
||||||
resetState();
|
resetState();
|
||||||
};
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showLoader('Loading PDF...');
|
const result = await loadPdfWithPasswordPrompt(pageState.file);
|
||||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
if (!result) {
|
||||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
resetState();
|
||||||
ignoreEncryption: true,
|
return;
|
||||||
throwOnInvalidObject: false
|
}
|
||||||
});
|
showLoader('Loading PDF...');
|
||||||
hideLoader();
|
result.pdf.destroy();
|
||||||
|
pageState.file = result.file;
|
||||||
|
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||||
|
throwOnInvalidObject: false,
|
||||||
|
});
|
||||||
|
hideLoader();
|
||||||
|
|
||||||
const pageCount = pageState.pdfDoc.getPageCount();
|
const pageCount = pageState.pdfDoc.getPageCount();
|
||||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||||
|
|
||||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading PDF:', error);
|
console.error('Error loading PDF:', error);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', 'Failed to load PDF file.');
|
showAlert('Error', 'Failed to load PDF file.');
|
||||||
resetState();
|
resetState();
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function nUpTool() {
|
async function nUpTool() {
|
||||||
if (!pageState.pdfDoc || !pageState.file) {
|
if (!pageState.pdfDoc || !pageState.file) {
|
||||||
showAlert('Error', 'Please upload a PDF first.');
|
showAlert('Error', 'Please upload a PDF first.');
|
||||||
return;
|
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);
|
if (orientation === 'landscape' && pageWidth < pageHeight) {
|
||||||
const pageSizeKey = (document.getElementById('output-page-size') as HTMLSelectElement).value as keyof typeof PageSizes;
|
[pageWidth, pageHeight] = [pageHeight, pageWidth];
|
||||||
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...');
|
const margin = useMargins ? 36 : 0;
|
||||||
|
const gutter = useMargins ? 10 : 0;
|
||||||
|
|
||||||
try {
|
const usableWidth = pageWidth - margin * 2;
|
||||||
const sourceDoc = pageState.pdfDoc;
|
const usableHeight = pageHeight - margin * 2;
|
||||||
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] };
|
for (let i = 0; i < sourcePages.length; i += n) {
|
||||||
const dims = gridDims[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') {
|
for (let j = 0; j < chunk.length; j++) {
|
||||||
const firstPage = sourcePages[0];
|
const sourcePage = chunk[j];
|
||||||
const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight();
|
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||||
orientation = isSourceLandscape && dims[0] > dims[1] ? 'landscape' : 'portrait';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orientation === 'landscape' && pageWidth < pageHeight) {
|
const scale = Math.min(
|
||||||
[pageWidth, pageHeight] = [pageHeight, pageWidth];
|
cellWidth / embeddedPage.width,
|
||||||
}
|
cellHeight / embeddedPage.height
|
||||||
|
|
||||||
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 scaledWidth = embeddedPage.width * scale;
|
||||||
|
const scaledHeight = embeddedPage.height * scale;
|
||||||
|
|
||||||
showAlert('Success', 'N-Up PDF created successfully!', 'success', function () {
|
const row = Math.floor(j / dims[0]);
|
||||||
resetState();
|
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);
|
if (addBorder) {
|
||||||
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
|
outputPage.drawRectangle({
|
||||||
} finally {
|
x,
|
||||||
hideLoader();
|
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) {
|
function handleFileSelect(files: FileList | null) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
if (
|
||||||
pageState.file = file;
|
file.type === 'application/pdf' ||
|
||||||
updateUI();
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
}
|
) {
|
||||||
|
pageState.file = file;
|
||||||
|
updateUI();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
const addBorderCheckbox = document.getElementById('add-border');
|
const addBorderCheckbox = document.getElementById('add-border');
|
||||||
const borderColorWrapper = document.getElementById('border-color-wrapper');
|
const borderColorWrapper = document.getElementById('border-color-wrapper');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', function () {
|
backBtn.addEventListener('click', function () {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
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) {
|
fileInput.addEventListener('click', function () {
|
||||||
addBorderCheckbox.addEventListener('change', function () {
|
fileInput.value = '';
|
||||||
borderColorWrapper.classList.toggle('hidden', !(addBorderCheckbox as HTMLInputElement).checked);
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
if (processBtn) {
|
||||||
fileInput.addEventListener('change', function (e) {
|
processBtn.addEventListener('click', nUpTool);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { tesseractLanguages } from '../config/tesseract-languages.js';
|
import { tesseractLanguages } from '../config/tesseract-languages.js';
|
||||||
import { showAlert } from '../ui.js';
|
import { showAlert } from '../ui.js';
|
||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
import { OcrState } from '@/types';
|
import { OcrState } from '@/types';
|
||||||
import { performOcr } from '../utils/ocr.js';
|
import { performOcr } from '../utils/ocr.js';
|
||||||
@@ -231,14 +232,17 @@ async function updateUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileSelect(files: FileList | null) {
|
async function handleFileSelect(files: FileList | null) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (
|
if (
|
||||||
file.type === 'application/pdf' ||
|
file.type === 'application/pdf' ||
|
||||||
file.name.toLowerCase().endsWith('.pdf')
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
) {
|
) {
|
||||||
pageState.file = file;
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
|
if (!result) return;
|
||||||
|
result.pdf.destroy();
|
||||||
|
pageState.file = result.file;
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||||
import {
|
import { formatBytes, downloadFile } from '../utils/helpers.js';
|
||||||
readFileAsArrayBuffer,
|
|
||||||
formatBytes,
|
|
||||||
downloadFile,
|
|
||||||
getPDFDocument,
|
|
||||||
} from '../utils/helpers.js';
|
|
||||||
import { initPagePreview } from '../utils/page-preview.js';
|
import { initPagePreview } from '../utils/page-preview.js';
|
||||||
import { PDFDocument } from 'pdf-lib';
|
import { PDFDocument } from 'pdf-lib';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
|
|
||||||
@@ -173,18 +169,18 @@ async function handleFile(file: File) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Loading PDF...');
|
|
||||||
organizeState.file = file;
|
organizeState.file = file;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
organizeState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, {
|
if (!result) return;
|
||||||
ignoreEncryption: true,
|
showLoader('Loading PDF...');
|
||||||
|
|
||||||
|
organizeState.pdfDoc = await PDFDocument.load(result.bytes, {
|
||||||
throwOnInvalidObject: false,
|
throwOnInvalidObject: false,
|
||||||
});
|
});
|
||||||
organizeState.pdfJsDoc = await getPDFDocument({
|
organizeState.pdfJsDoc = result.pdf;
|
||||||
data: (arrayBuffer as ArrayBuffer).slice(0),
|
organizeState.file = result.file;
|
||||||
}).promise;
|
|
||||||
organizeState.totalPages = organizeState.pdfDoc.getPageCount();
|
organizeState.totalPages = organizeState.pdfDoc.getPageCount();
|
||||||
|
|
||||||
updateFileDisplay();
|
updateFileDisplay();
|
||||||
|
|||||||
@@ -1,81 +1,86 @@
|
|||||||
import { showAlert } from '../ui.js';
|
import { showAlert } from '../ui.js';
|
||||||
import { formatBytes, getStandardPageName, convertPoints } from '../utils/helpers.js';
|
import {
|
||||||
|
formatBytes,
|
||||||
|
getStandardPageName,
|
||||||
|
convertPoints,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import { PDFDocument } from 'pdf-lib';
|
import { PDFDocument } from 'pdf-lib';
|
||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
import { PageDimensionsState } from '@/types';
|
import { PageDimensionsState } from '@/types';
|
||||||
|
|
||||||
const pageState: PageDimensionsState = {
|
const pageState: PageDimensionsState = {
|
||||||
file: null,
|
file: null,
|
||||||
pdfDoc: null,
|
pdfDoc: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
let analyzedPagesData: any[] = [];
|
let analyzedPagesData: any[] = [];
|
||||||
|
|
||||||
function calculateAspectRatio(width: number, height: number): string {
|
function calculateAspectRatio(width: number, height: number): string {
|
||||||
const ratio = width / height;
|
const ratio = width / height;
|
||||||
return ratio.toFixed(3);
|
return ratio.toFixed(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateArea(width: number, height: number, unit: string): string {
|
function calculateArea(width: number, height: number, unit: string): string {
|
||||||
const areaInPoints = width * height;
|
const areaInPoints = width * height;
|
||||||
let convertedArea = 0;
|
let convertedArea: number;
|
||||||
let unitSuffix = '';
|
let unitSuffix: string;
|
||||||
|
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
case 'in':
|
case 'in':
|
||||||
convertedArea = areaInPoints / (72 * 72);
|
convertedArea = areaInPoints / (72 * 72);
|
||||||
unitSuffix = 'in²';
|
unitSuffix = 'in²';
|
||||||
break;
|
break;
|
||||||
case 'mm':
|
case 'mm':
|
||||||
convertedArea = areaInPoints / (72 * 72) * (25.4 * 25.4);
|
convertedArea = (areaInPoints / (72 * 72)) * (25.4 * 25.4);
|
||||||
unitSuffix = 'mm²';
|
unitSuffix = 'mm²';
|
||||||
break;
|
break;
|
||||||
case 'px':
|
case 'px':
|
||||||
const pxPerPoint = 96 / 72;
|
const pxPerPoint = 96 / 72;
|
||||||
convertedArea = areaInPoints * (pxPerPoint * pxPerPoint);
|
convertedArea = areaInPoints * (pxPerPoint * pxPerPoint);
|
||||||
unitSuffix = 'px²';
|
unitSuffix = 'px²';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
convertedArea = areaInPoints;
|
convertedArea = areaInPoints;
|
||||||
unitSuffix = 'pt²';
|
unitSuffix = 'pt²';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${convertedArea.toFixed(2)} ${unitSuffix}`;
|
return `${convertedArea.toFixed(2)} ${unitSuffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSummaryStats() {
|
function getSummaryStats() {
|
||||||
const totalPages = analyzedPagesData.length;
|
const totalPages = analyzedPagesData.length;
|
||||||
|
|
||||||
const uniqueSizes = new Map();
|
const uniqueSizes = new Map();
|
||||||
analyzedPagesData.forEach((pageData: any) => {
|
analyzedPagesData.forEach((pageData: any) => {
|
||||||
const key = `${pageData.width.toFixed(2)}x${pageData.height.toFixed(2)}`;
|
const key = `${pageData.width.toFixed(2)}x${pageData.height.toFixed(2)}`;
|
||||||
const label = `${pageData.standardSize} (${pageData.orientation})`;
|
const label = `${pageData.standardSize} (${pageData.orientation})`;
|
||||||
uniqueSizes.set(key, {
|
uniqueSizes.set(key, {
|
||||||
count: (uniqueSizes.get(key)?.count || 0) + 1,
|
count: (uniqueSizes.get(key)?.count || 0) + 1,
|
||||||
label: label,
|
label: label,
|
||||||
width: pageData.width,
|
width: pageData.width,
|
||||||
height: pageData.height
|
height: pageData.height,
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const hasMixedSizes = uniqueSizes.size > 1;
|
const hasMixedSizes = uniqueSizes.size > 1;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalPages,
|
totalPages,
|
||||||
uniqueSizesCount: uniqueSizes.size,
|
uniqueSizesCount: uniqueSizes.size,
|
||||||
uniqueSizes: Array.from(uniqueSizes.values()),
|
uniqueSizes: Array.from(uniqueSizes.values()),
|
||||||
hasMixedSizes
|
hasMixedSizes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSummary() {
|
function renderSummary() {
|
||||||
const summaryContainer = document.getElementById('dimensions-summary');
|
const summaryContainer = document.getElementById('dimensions-summary');
|
||||||
if (!summaryContainer) return;
|
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="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">
|
<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>
|
<p class="text-sm text-gray-400 mb-1">Total Pages</p>
|
||||||
@@ -94,8 +99,8 @@ function renderSummary() {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (stats.hasMixedSizes) {
|
if (stats.hasMixedSizes) {
|
||||||
summaryHTML += `
|
summaryHTML += `
|
||||||
<div class="bg-yellow-900/20 border border-yellow-500/30 rounded-lg p-4 mb-4">
|
<div class="bg-yellow-900/20 border border-yellow-500/30 rounded-lg p-4 mb-4">
|
||||||
<div class="flex items-start gap-3">
|
<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>
|
<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>
|
<h4 class="text-yellow-200 font-semibold mb-2">Mixed Page Sizes Detected</h4>
|
||||||
<p class="text-sm text-gray-300 mb-3">This document contains pages with different dimensions:</p>
|
<p class="text-sm text-gray-300 mb-3">This document contains pages with different dimensions:</p>
|
||||||
<ul class="space-y-1 text-sm text-gray-300">
|
<ul class="space-y-1 text-sm text-gray-300">
|
||||||
${stats.uniqueSizes.map((size: any) => `
|
${stats.uniqueSizes
|
||||||
|
.map(
|
||||||
|
(size: any) => `
|
||||||
<li>• ${size.label}: ${size.count} page${size.count > 1 ? 's' : ''}</li>
|
<li>• ${size.label}: ${size.count} page${size.count > 1 ? 's' : ''}</li>
|
||||||
`).join('')}
|
`
|
||||||
|
)
|
||||||
|
.join('')}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
summaryContainer.innerHTML = summaryHTML;
|
summaryContainer.innerHTML = summaryHTML;
|
||||||
|
|
||||||
if (stats.hasMixedSizes) {
|
if (stats.hasMixedSizes) {
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTable(unit: string) {
|
function renderTable(unit: string) {
|
||||||
const tableBody = document.getElementById('dimensions-table-body');
|
const tableBody = document.getElementById('dimensions-table-body');
|
||||||
if (!tableBody) return;
|
if (!tableBody) return;
|
||||||
|
|
||||||
tableBody.textContent = '';
|
tableBody.textContent = '';
|
||||||
|
|
||||||
analyzedPagesData.forEach((pageData) => {
|
analyzedPagesData.forEach((pageData) => {
|
||||||
const width = convertPoints(pageData.width, unit);
|
const width = convertPoints(pageData.width, unit);
|
||||||
const height = convertPoints(pageData.height, unit);
|
const height = convertPoints(pageData.height, unit);
|
||||||
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
|
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
|
||||||
const area = calculateArea(pageData.width, pageData.height, unit);
|
const area = calculateArea(pageData.width, pageData.height, unit);
|
||||||
|
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
const pageNumCell = document.createElement('td');
|
const pageNumCell = document.createElement('td');
|
||||||
pageNumCell.className = 'px-4 py-3 text-white';
|
pageNumCell.className = 'px-4 py-3 text-white';
|
||||||
pageNumCell.textContent = pageData.pageNum;
|
pageNumCell.textContent = pageData.pageNum;
|
||||||
|
|
||||||
const dimensionsCell = document.createElement('td');
|
const dimensionsCell = document.createElement('td');
|
||||||
dimensionsCell.className = 'px-4 py-3 text-gray-300';
|
dimensionsCell.className = 'px-4 py-3 text-gray-300';
|
||||||
dimensionsCell.textContent = `${width} x ${height} ${unit}`;
|
dimensionsCell.textContent = `${width} x ${height} ${unit}`;
|
||||||
|
|
||||||
const sizeCell = document.createElement('td');
|
const sizeCell = document.createElement('td');
|
||||||
sizeCell.className = 'px-4 py-3 text-gray-300';
|
sizeCell.className = 'px-4 py-3 text-gray-300';
|
||||||
sizeCell.textContent = pageData.standardSize;
|
sizeCell.textContent = pageData.standardSize;
|
||||||
|
|
||||||
const orientationCell = document.createElement('td');
|
const orientationCell = document.createElement('td');
|
||||||
orientationCell.className = 'px-4 py-3 text-gray-300';
|
orientationCell.className = 'px-4 py-3 text-gray-300';
|
||||||
orientationCell.textContent = pageData.orientation;
|
orientationCell.textContent = pageData.orientation;
|
||||||
|
|
||||||
const aspectRatioCell = document.createElement('td');
|
const aspectRatioCell = document.createElement('td');
|
||||||
aspectRatioCell.className = 'px-4 py-3 text-gray-300';
|
aspectRatioCell.className = 'px-4 py-3 text-gray-300';
|
||||||
aspectRatioCell.textContent = aspectRatio;
|
aspectRatioCell.textContent = aspectRatio;
|
||||||
|
|
||||||
const areaCell = document.createElement('td');
|
const areaCell = document.createElement('td');
|
||||||
areaCell.className = 'px-4 py-3 text-gray-300';
|
areaCell.className = 'px-4 py-3 text-gray-300';
|
||||||
areaCell.textContent = area;
|
areaCell.textContent = area;
|
||||||
|
|
||||||
const rotationCell = document.createElement('td');
|
const rotationCell = document.createElement('td');
|
||||||
rotationCell.className = 'px-4 py-3 text-gray-300';
|
rotationCell.className = 'px-4 py-3 text-gray-300';
|
||||||
rotationCell.textContent = `${pageData.rotation}°`;
|
rotationCell.textContent = `${pageData.rotation}°`;
|
||||||
|
|
||||||
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell, aspectRatioCell, areaCell, rotationCell);
|
row.append(
|
||||||
tableBody.appendChild(row);
|
pageNumCell,
|
||||||
});
|
dimensionsCell,
|
||||||
|
sizeCell,
|
||||||
|
orientationCell,
|
||||||
|
aspectRatioCell,
|
||||||
|
areaCell,
|
||||||
|
rotationCell
|
||||||
|
);
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportToCSV() {
|
function exportToCSV() {
|
||||||
const unitsSelect = document.getElementById('units-select') as HTMLSelectElement;
|
const unitsSelect = document.getElementById(
|
||||||
const unit = unitsSelect?.value || 'pt';
|
'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 headers = [
|
||||||
const csvRows = [headers.join(',')];
|
'Page #',
|
||||||
|
`Width (${unit})`,
|
||||||
|
`Height (${unit})`,
|
||||||
|
'Standard Size',
|
||||||
|
'Orientation',
|
||||||
|
'Aspect Ratio',
|
||||||
|
`Area (${unit}²)`,
|
||||||
|
'Rotation',
|
||||||
|
];
|
||||||
|
const csvRows = [headers.join(',')];
|
||||||
|
|
||||||
analyzedPagesData.forEach((pageData: any) => {
|
analyzedPagesData.forEach((pageData: any) => {
|
||||||
const width = convertPoints(pageData.width, unit);
|
const width = convertPoints(pageData.width, unit);
|
||||||
const height = convertPoints(pageData.height, unit);
|
const height = convertPoints(pageData.height, unit);
|
||||||
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
|
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
|
||||||
const area = calculateArea(pageData.width, pageData.height, unit);
|
const area = calculateArea(pageData.width, pageData.height, unit);
|
||||||
|
|
||||||
const row = [
|
const row = [
|
||||||
pageData.pageNum,
|
pageData.pageNum,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
pageData.standardSize,
|
pageData.standardSize,
|
||||||
pageData.orientation,
|
pageData.orientation,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
area,
|
area,
|
||||||
`${pageData.rotation}°`
|
`${pageData.rotation}°`,
|
||||||
];
|
];
|
||||||
csvRows.push(row.join(','));
|
csvRows.push(row.join(','));
|
||||||
});
|
});
|
||||||
|
|
||||||
const csvContent = csvRows.join('\n');
|
const csvContent = csvRows.join('\n');
|
||||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = 'page-dimensions.csv';
|
link.download = 'page-dimensions.csv';
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
function analyzeAndDisplayDimensions() {
|
function analyzeAndDisplayDimensions() {
|
||||||
if (!pageState.pdfDoc) return;
|
if (!pageState.pdfDoc) return;
|
||||||
|
|
||||||
analyzedPagesData = [];
|
analyzedPagesData = [];
|
||||||
const pages = pageState.pdfDoc.getPages();
|
const pages = pageState.pdfDoc.getPages();
|
||||||
|
|
||||||
pages.forEach((page: any, index: number) => {
|
pages.forEach((page: any, index: number) => {
|
||||||
const { width, height } = page.getSize();
|
const { width, height } = page.getSize();
|
||||||
const rotation = page.getRotation().angle || 0;
|
const rotation = page.getRotation().angle || 0;
|
||||||
|
|
||||||
analyzedPagesData.push({
|
analyzedPagesData.push({
|
||||||
pageNum: index + 1,
|
pageNum: index + 1,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
orientation: width > height ? 'Landscape' : 'Portrait',
|
orientation: width > height ? 'Landscape' : 'Portrait',
|
||||||
standardSize: getStandardPageName(width, height),
|
standardSize: getStandardPageName(width, height),
|
||||||
rotation: rotation
|
rotation: rotation,
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const resultsContainer = document.getElementById('dimensions-results');
|
const resultsContainer = document.getElementById('dimensions-results');
|
||||||
const unitsSelect = document.getElementById('units-select') as HTMLSelectElement;
|
const unitsSelect = document.getElementById(
|
||||||
|
'units-select'
|
||||||
|
) as HTMLSelectElement;
|
||||||
|
|
||||||
renderSummary();
|
renderSummary();
|
||||||
renderTable(unitsSelect.value);
|
renderTable(unitsSelect.value);
|
||||||
|
|
||||||
if (resultsContainer) resultsContainer.classList.remove('hidden');
|
if (resultsContainer) resultsContainer.classList.remove('hidden');
|
||||||
|
|
||||||
unitsSelect.addEventListener('change', (e) => {
|
unitsSelect.addEventListener('change', (e) => {
|
||||||
renderTable((e.target as HTMLSelectElement).value);
|
renderTable((e.target as HTMLSelectElement).value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const exportButton = document.getElementById('export-csv-btn');
|
const exportButton = document.getElementById('export-csv-btn');
|
||||||
if (exportButton) {
|
if (exportButton) {
|
||||||
exportButton.addEventListener('click', exportToCSV);
|
exportButton.addEventListener('click', exportToCSV);
|
||||||
}
|
}
|
||||||
|
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
pageState.file = null;
|
pageState.file = null;
|
||||||
pageState.pdfDoc = null;
|
pageState.pdfDoc = null;
|
||||||
analyzedPagesData = [];
|
analyzedPagesData = [];
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
const resultsContainer = document.getElementById('dimensions-results');
|
const resultsContainer = document.getElementById('dimensions-results');
|
||||||
if (resultsContainer) resultsContainer.classList.add('hidden');
|
if (resultsContainer) resultsContainer.classList.add('hidden');
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUI() {
|
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) {
|
if (pageState.file) {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = pageState.file.name;
|
nameSpan.textContent = pageState.file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
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.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.onclick = function () {
|
removeBtn.onclick = function () {
|
||||||
resetState();
|
resetState();
|
||||||
};
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFileSelect(files: FileList | null) {
|
async function handleFileSelect(files: FileList | null) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
if (
|
||||||
pageState.file = file;
|
file.type === 'application/pdf' ||
|
||||||
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
|
if (!result) return;
|
||||||
|
result.pdf.destroy();
|
||||||
|
|
||||||
try {
|
pageState.file = result.file;
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
pageState.pdfDoc = await PDFDocument.load(result.bytes);
|
||||||
pageState.pdfDoc = await PDFDocument.load(arrayBuffer);
|
updateUI();
|
||||||
updateUI();
|
analyzeAndDisplayDimensions();
|
||||||
analyzeAndDisplayDimensions();
|
} catch (e) {
|
||||||
} catch (e) {
|
console.error('Error loading PDF:', e);
|
||||||
console.error('Error loading PDF:', e);
|
showAlert('Error', 'Failed to load PDF file.');
|
||||||
showAlert('Error', 'Failed to load PDF file.');
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', function () {
|
backBtn.addEventListener('click', function () {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
if (fileInput && dropZone) {
|
||||||
fileInput.addEventListener('change', function (e) {
|
fileInput.addEventListener('change', function (e) {
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', function (e) {
|
dropZone.addEventListener('dragover', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.add('bg-gray-700');
|
dropZone.classList.add('bg-gray-700');
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', function (e) {
|
dropZone.addEventListener('dragleave', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.remove('bg-gray-700');
|
dropZone.classList.remove('bg-gray-700');
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone.addEventListener('drop', function (e) {
|
dropZone.addEventListener('drop', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.remove('bg-gray-700');
|
dropZone.classList.remove('bg-gray-700');
|
||||||
handleFileSelect(e.dataTransfer?.files);
|
handleFileSelect(e.dataTransfer?.files);
|
||||||
});
|
});
|
||||||
|
|
||||||
fileInput.addEventListener('click', function () {
|
fileInput.addEventListener('click', function () {
|
||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createIcons, icons } from 'lucide';
|
|||||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||||
import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import {
|
import {
|
||||||
addPageNumbers as addPageNumbersToPdf,
|
addPageNumbers as addPageNumbersToPdf,
|
||||||
type PageNumberPosition,
|
type PageNumberPosition,
|
||||||
@@ -83,11 +84,14 @@ async function handleFiles(files: FileList) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Loading PDF...');
|
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
if (!result) return;
|
||||||
pageState.file = file;
|
showLoader('Loading PDF...');
|
||||||
|
|
||||||
|
pageState.pdfDoc = await PDFLibDocument.load(result.bytes);
|
||||||
|
pageState.file = result.file;
|
||||||
|
result.pdf.destroy();
|
||||||
|
|
||||||
updateFileDisplay();
|
updateFileDisplay();
|
||||||
document.getElementById('options-panel')?.classList.remove('hidden');
|
document.getElementById('options-panel')?.classList.remove('hidden');
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PDFDocument as PDFLibDocument, degrees, PageSizes } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument, degrees, PageSizes } from 'pdf-lib';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.mjs',
|
'pdfjs-dist/build/pdf.worker.mjs',
|
||||||
@@ -87,19 +88,20 @@ async function updateUI() {
|
|||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const result = await loadPdfWithPasswordPrompt(pageState.file);
|
||||||
|
if (!result) {
|
||||||
|
resetState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
showLoader('Loading PDF...');
|
showLoader('Loading PDF...');
|
||||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
pageState.file = result.file;
|
||||||
pageState.pdfBytes = new Uint8Array(arrayBuffer);
|
pageState.pdfBytes = new Uint8Array(result.bytes);
|
||||||
|
pageState.pdfjsDoc = result.pdf;
|
||||||
|
|
||||||
pageState.pdfDoc = await PDFLibDocument.load(pageState.pdfBytes, {
|
pageState.pdfDoc = await PDFLibDocument.load(pageState.pdfBytes, {
|
||||||
ignoreEncryption: true,
|
|
||||||
throwOnInvalidObject: false,
|
throwOnInvalidObject: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
pageState.pdfjsDoc = await pdfjsLib.getDocument({
|
|
||||||
data: pageState.pdfBytes.slice(),
|
|
||||||
}).promise;
|
|
||||||
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
const pageCount = pageState.pdfDoc.getPageCount();
|
const pageCount = pageState.pdfDoc.getPageCount();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { createIcons, icons } from 'lucide';
|
|||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
interface LayerData {
|
interface LayerData {
|
||||||
number: number;
|
number: number;
|
||||||
@@ -416,14 +417,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
const handleFileSelect = async (files: FileList | null) => {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (
|
if (
|
||||||
file.type === 'application/pdf' ||
|
file.type === 'application/pdf' ||
|
||||||
file.name.toLowerCase().endsWith('.pdf')
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
) {
|
) {
|
||||||
currentFile = file;
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
|
if (!result) return;
|
||||||
|
result.pdf.destroy();
|
||||||
|
currentFile = result.file;
|
||||||
updateUI();
|
updateUI();
|
||||||
} else {
|
} else {
|
||||||
showAlert('Invalid File', 'Please select a PDF file.');
|
showAlert('Invalid File', 'Please select a PDF file.');
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import * as pdfjsLib from 'pdfjs-dist';
|
|||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
import { downloadFile, getPDFDocument } from '../utils/helpers';
|
import { downloadFile, getPDFDocument } from '../utils/helpers';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import {
|
import {
|
||||||
renderPagesProgressively,
|
renderPagesProgressively,
|
||||||
cleanupLazyRendering,
|
cleanupLazyRendering,
|
||||||
@@ -428,6 +429,12 @@ async function loadPdfs(files: File[]) {
|
|||||||
arrayBuffer = await file.arrayBuffer();
|
arrayBuffer = await file.arrayBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hideLoading();
|
||||||
|
const pwResult = await loadPdfWithPasswordPrompt(file);
|
||||||
|
if (!pwResult) continue;
|
||||||
|
pwResult.pdf.destroy();
|
||||||
|
arrayBuffer = pwResult.bytes as ArrayBuffer;
|
||||||
|
|
||||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
const pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
||||||
ignoreEncryption: true,
|
ignoreEncryption: true,
|
||||||
throwOnInvalidObject: false,
|
throwOnInvalidObject: false,
|
||||||
@@ -848,15 +855,17 @@ async function handleInsertPdf(e: Event) {
|
|||||||
if (insertAfterIndex === undefined) return;
|
if (insertAfterIndex === undefined) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const pwResult = await loadPdfWithPasswordPrompt(file);
|
||||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
if (!pwResult) return;
|
||||||
|
pwResult.pdf.destroy();
|
||||||
|
|
||||||
|
const pdfDoc = await PDFLibDocument.load(pwResult.bytes, {
|
||||||
ignoreEncryption: true,
|
ignoreEncryption: true,
|
||||||
throwOnInvalidObject: false,
|
throwOnInvalidObject: false,
|
||||||
});
|
});
|
||||||
currentPdfDocs.push(pdfDoc);
|
currentPdfDocs.push(pdfDoc);
|
||||||
const pdfIndex = currentPdfDocs.length - 1;
|
const pdfIndex = currentPdfDocs.length - 1;
|
||||||
|
|
||||||
// Load PDF.js document for rendering
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
const pdfjsDoc = await getPDFDocument({ data: new Uint8Array(pdfBytes) })
|
const pdfjsDoc = await getPDFDocument({ data: new Uint8Array(pdfBytes) })
|
||||||
.promise;
|
.promise;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import JSZip from 'jszip';
|
|||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { PDFPageProxy } from 'pdfjs-dist';
|
import { PDFPageProxy } from 'pdfjs-dist';
|
||||||
import { t } from '../i18n/i18n';
|
import { t } from '../i18n/i18n';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -98,10 +99,11 @@ async function convert() {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader(t('tools:pdfToBmp.loader.converting'));
|
|
||||||
try {
|
try {
|
||||||
const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0]))
|
const result = await loadPdfWithPasswordPrompt(files[0], files, 0);
|
||||||
.promise;
|
if (!result) return;
|
||||||
|
showLoader(t('tools:pdfToBmp.loader.converting'));
|
||||||
|
const { pdf } = result;
|
||||||
|
|
||||||
if (pdf.numPages === 1) {
|
if (pdf.numPages === 1) {
|
||||||
const page = await pdf.getPage(1);
|
const page = await pdf.getPage(1);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
generateComicBookInfoJson,
|
generateComicBookInfoJson,
|
||||||
} from '../utils/comic-info.js';
|
} from '../utils/comic-info.js';
|
||||||
import type { CbzOptions, ComicMetadata } from '@/types';
|
import type { CbzOptions, ComicMetadata } from '@/types';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -229,8 +230,11 @@ async function convert() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const options = getOptions();
|
const options = getOptions();
|
||||||
const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0]))
|
hideLoader();
|
||||||
.promise;
|
const result = await loadPdfWithPasswordPrompt(files[0], files, 0);
|
||||||
|
if (!result) return;
|
||||||
|
showLoader(t('tools:pdfToCbz.converting'));
|
||||||
|
const { pdf } = result;
|
||||||
|
|
||||||
if (pdf.numPages === 0) {
|
if (pdf.numPages === 0) {
|
||||||
throw new Error('PDF has no pages');
|
throw new Error('PDF has no pages');
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import JSZip from 'jszip';
|
|||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
let file: File | null = null;
|
let file: File | null = null;
|
||||||
|
|
||||||
const updateUI = () => {
|
const updateUI = () => {
|
||||||
@@ -86,6 +87,13 @@ async function convert() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const pymupdf = await loadPyMuPDF();
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
const pwResult = await loadPdfWithPasswordPrompt(file);
|
||||||
|
if (!pwResult) return;
|
||||||
|
pwResult.pdf.destroy();
|
||||||
|
file = pwResult.file;
|
||||||
|
|
||||||
showLoader('Extracting tables...');
|
showLoader('Extracting tables...');
|
||||||
|
|
||||||
const doc = await pymupdf.open(file);
|
const doc = await pymupdf.open(file);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide';
|
|||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -105,6 +106,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
showLoader('Loading PDF converter...');
|
showLoader('Loading PDF converter...');
|
||||||
const pymupdf = await loadPyMuPDF();
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
state.files = await batchDecryptIfNeeded(state.files);
|
||||||
|
showLoader('Converting...');
|
||||||
|
|
||||||
if (state.files.length === 1) {
|
if (state.files.length === 1) {
|
||||||
const file = state.files[0];
|
const file = state.files[0];
|
||||||
showLoader(`Converting ${file.name}...`);
|
showLoader(`Converting ${file.name}...`);
|
||||||
@@ -122,7 +127,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
showLoader('Converting multiple PDFs...');
|
|
||||||
const JSZip = (await import('jszip')).default;
|
const JSZip = (await import('jszip')).default;
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
const usedNames = new Set<string>();
|
const usedNames = new Set<string>();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createIcons, icons } from 'lucide';
|
|||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
let file: File | null = null;
|
let file: File | null = null;
|
||||||
|
|
||||||
@@ -66,6 +67,13 @@ async function convert() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const pymupdf = await loadPyMuPDF();
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
const pwResult = await loadPdfWithPasswordPrompt(file);
|
||||||
|
if (!pwResult) return;
|
||||||
|
pwResult.pdf.destroy();
|
||||||
|
file = pwResult.file;
|
||||||
|
|
||||||
showLoader('Extracting tables...');
|
showLoader('Extracting tables...');
|
||||||
|
|
||||||
const doc = await pymupdf.open(file);
|
const doc = await pymupdf.open(file);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { PDFDocument } from 'pdf-lib';
|
|||||||
import { applyGreyscale } from '../utils/image-effects.js';
|
import { applyGreyscale } from '../utils/image-effects.js';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { t } from '../i18n/i18n';
|
import { t } from '../i18n/i18n';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -94,13 +95,11 @@ async function convert() {
|
|||||||
showAlert('No File', 'Please upload a PDF file first.');
|
showAlert('No File', 'Please upload a PDF file first.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader('Converting to greyscale...');
|
|
||||||
try {
|
try {
|
||||||
const pdfBytes = (await readFileAsArrayBuffer(files[0])) as ArrayBuffer;
|
const result = await loadPdfWithPasswordPrompt(files[0], files, 0);
|
||||||
const pdfDoc = await PDFDocument.load(pdfBytes);
|
if (!result) return;
|
||||||
const pages = pdfDoc.getPages();
|
showLoader('Converting to greyscale...');
|
||||||
|
const { pdf: pdfjsDoc } = result;
|
||||||
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
|
|
||||||
const newPdfDoc = await PDFDocument.create();
|
const newPdfDoc = await PDFDocument.create();
|
||||||
|
|
||||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import JSZip from 'jszip';
|
|||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { PDFPageProxy } from 'pdfjs-dist';
|
import { PDFPageProxy } from 'pdfjs-dist';
|
||||||
import { t } from '../i18n/i18n';
|
import { t } from '../i18n/i18n';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -103,10 +104,11 @@ async function convert() {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader(t('tools:pdfToJpg.loader.converting'));
|
|
||||||
try {
|
try {
|
||||||
const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0]))
|
const result = await loadPdfWithPasswordPrompt(files[0], files, 0);
|
||||||
.promise;
|
if (!result) return;
|
||||||
|
showLoader(t('tools:pdfToJpg.loader.converting'));
|
||||||
|
const { pdf } = result;
|
||||||
|
|
||||||
const qualityInput = document.getElementById(
|
const qualityInput = document.getElementById(
|
||||||
'jpg-quality'
|
'jpg-quality'
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
showWasmRequiredDialog,
|
showWasmRequiredDialog,
|
||||||
WasmProvider,
|
WasmProvider,
|
||||||
} from '../utils/wasm-provider.js';
|
} from '../utils/wasm-provider.js';
|
||||||
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
const worker = new Worker(
|
const worker = new Worker(
|
||||||
import.meta.env.BASE_URL + 'workers/pdf-to-json.worker.js'
|
import.meta.env.BASE_URL + 'workers/pdf-to-json.worker.js'
|
||||||
@@ -105,6 +106,10 @@ async function convertPDFsToJSON() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
convertBtn.disabled = true;
|
convertBtn.disabled = true;
|
||||||
|
showStatus('Checking for encrypted PDFs...', 'info');
|
||||||
|
|
||||||
|
selectedFiles = await batchDecryptIfNeeded(selectedFiles);
|
||||||
|
|
||||||
showStatus('Reading files (Main Thread)...', 'info');
|
showStatus('Reading files (Main Thread)...', 'info');
|
||||||
|
|
||||||
const fileBuffers = await Promise.all(
|
const fileBuffers = await Promise.all(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide';
|
|||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -110,6 +111,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
const includeImages = includeImagesCheckbox?.checked ?? false;
|
const includeImages = includeImagesCheckbox?.checked ?? false;
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
state.files = await batchDecryptIfNeeded(state.files);
|
||||||
|
showLoader('Converting...');
|
||||||
|
|
||||||
if (state.files.length === 1) {
|
if (state.files.length === 1) {
|
||||||
const file = state.files[0];
|
const file = state.files[0];
|
||||||
showLoader(`Converting ${file.name}...`);
|
showLoader(`Converting ${file.name}...`);
|
||||||
@@ -128,7 +133,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
showLoader('Converting multiple PDFs...');
|
|
||||||
const JSZip = (await import('jszip')).default;
|
const JSZip = (await import('jszip')).default;
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
const usedNames = new Set<string>();
|
const usedNames = new Set<string>();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide';
|
|||||||
import { convertFileToPdfA, type PdfALevel } from '../utils/ghostscript-loader';
|
import { convertFileToPdfA, type PdfALevel } from '../utils/ghostscript-loader';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -108,10 +109,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
try {
|
try {
|
||||||
if (state.files.length === 0) {
|
if (state.files.length === 0) {
|
||||||
showAlert('No Files', 'Please select at least one PDF file.');
|
showAlert('No Files', 'Please select at least one PDF file.');
|
||||||
hideLoader();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.files = await batchDecryptIfNeeded(state.files);
|
||||||
|
|
||||||
if (state.files.length === 1) {
|
if (state.files.length === 1) {
|
||||||
const originalFile = state.files[0];
|
const originalFile = state.files[0];
|
||||||
const preFlattenCheckbox = document.getElementById(
|
const preFlattenCheckbox = document.getElementById(
|
||||||
@@ -125,7 +127,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (shouldPreFlatten) {
|
if (shouldPreFlatten) {
|
||||||
if (!isPyMuPDFAvailable()) {
|
if (!isPyMuPDFAvailable()) {
|
||||||
showWasmRequiredDialog('pymupdf');
|
showWasmRequiredDialog('pymupdf');
|
||||||
hideLoader();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import JSZip from 'jszip';
|
|||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { PDFPageProxy } from 'pdfjs-dist';
|
import { PDFPageProxy } from 'pdfjs-dist';
|
||||||
import { t } from '../i18n/i18n';
|
import { t } from '../i18n/i18n';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -101,10 +102,11 @@ async function convert() {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader(t('tools:pdfToPng.loader.converting'));
|
|
||||||
try {
|
try {
|
||||||
const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0]))
|
const result = await loadPdfWithPasswordPrompt(files[0], files, 0);
|
||||||
.promise;
|
if (!result) return;
|
||||||
|
showLoader(t('tools:pdfToPng.loader.converting'));
|
||||||
|
const { pdf } = result;
|
||||||
|
|
||||||
const scaleInput = document.getElementById('png-scale') as HTMLInputElement;
|
const scaleInput = document.getElementById('png-scale') as HTMLInputElement;
|
||||||
const scale = scaleInput ? parseFloat(scaleInput.value) : 2.0;
|
const scale = scaleInput ? parseFloat(scaleInput.value) : 2.0;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import JSZip from 'jszip';
|
|||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
let pymupdf: any = null;
|
let pymupdf: any = null;
|
||||||
let files: File[] = [];
|
let files: File[] = [];
|
||||||
@@ -87,6 +88,10 @@ async function convert() {
|
|||||||
pymupdf = await loadPyMuPDF();
|
pymupdf = await loadPyMuPDF();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
files = await batchDecryptIfNeeded(files);
|
||||||
|
showLoader('Converting to SVG...');
|
||||||
|
|
||||||
const isSingleFile = files.length === 1;
|
const isSingleFile = files.length === 1;
|
||||||
|
|
||||||
if (isSingleFile) {
|
if (isSingleFile) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
|
|||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||||
|
|
||||||
let files: File[] = [];
|
let files: File[] = [];
|
||||||
@@ -176,6 +177,10 @@ async function extractText() {
|
|||||||
try {
|
try {
|
||||||
const mupdf = await ensurePyMuPDF();
|
const mupdf = await ensurePyMuPDF();
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
files = await batchDecryptIfNeeded(files);
|
||||||
|
showLoader('Extracting text...');
|
||||||
|
|
||||||
if (files.length === 1) {
|
if (files.length === 1) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
showLoader(`Extracting text from ${file.name}...`);
|
showLoader(`Extracting text from ${file.name}...`);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { t } from '../i18n/i18n';
|
|||||||
import type Vips from 'wasm-vips';
|
import type Vips from 'wasm-vips';
|
||||||
import wasmUrl from 'wasm-vips/vips.wasm?url';
|
import wasmUrl from 'wasm-vips/vips.wasm?url';
|
||||||
import type { TiffOptions } from '@/types';
|
import type { TiffOptions } from '@/types';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -229,8 +230,11 @@ async function convert() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const options = getOptions();
|
const options = getOptions();
|
||||||
const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0]))
|
hideLoader();
|
||||||
.promise;
|
const result = await loadPdfWithPasswordPrompt(files[0], files, 0);
|
||||||
|
if (!result) return;
|
||||||
|
showLoader(t('tools:pdfToTiff.converting'));
|
||||||
|
const { pdf } = result;
|
||||||
|
|
||||||
if (options.multiPage && pdf.numPages > 1) {
|
if (options.multiPage && pdf.numPages > 1) {
|
||||||
const pages: Vips.Image[] = [];
|
const pages: Vips.Image[] = [];
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import JSZip from 'jszip';
|
|||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { PDFPageProxy } from 'pdfjs-dist';
|
import { PDFPageProxy } from 'pdfjs-dist';
|
||||||
import { t } from '../i18n/i18n';
|
import { t } from '../i18n/i18n';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -103,10 +104,11 @@ async function convert() {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader(t('tools:pdfToWebp.loader.converting'));
|
|
||||||
try {
|
try {
|
||||||
const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0]))
|
const result = await loadPdfWithPasswordPrompt(files[0], files, 0);
|
||||||
.promise;
|
if (!result) return;
|
||||||
|
showLoader(t('tools:pdfToWebp.loader.converting'));
|
||||||
|
const { pdf } = result;
|
||||||
|
|
||||||
const qualityInput = document.getElementById(
|
const qualityInput = document.getElementById(
|
||||||
'webp-quality'
|
'webp-quality'
|
||||||
|
|||||||
@@ -1,393 +1,497 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, parsePageRanges, getPDFDocument, formatBytes } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
parsePageRanges,
|
||||||
|
formatBytes,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import { PDFDocument, PageSizes } from 'pdf-lib';
|
import { PDFDocument, PageSizes } from 'pdf-lib';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PosterizeState } from '@/types';
|
import { PosterizeState } from '@/types';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
|
import.meta.url
|
||||||
|
).toString();
|
||||||
|
|
||||||
const pageState: PosterizeState = {
|
const pageState: PosterizeState = {
|
||||||
file: null,
|
file: null,
|
||||||
pdfJsDoc: null,
|
pdfJsDoc: null,
|
||||||
pdfBytes: null,
|
pdfBytes: null,
|
||||||
pageSnapshots: {},
|
pageSnapshots: {},
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
pageState.file = null;
|
pageState.file = null;
|
||||||
pageState.pdfJsDoc = null;
|
pageState.pdfJsDoc = null;
|
||||||
pageState.pdfBytes = null;
|
pageState.pdfBytes = null;
|
||||||
pageState.pageSnapshots = {};
|
pageState.pageSnapshots = {};
|
||||||
pageState.currentPage = 1;
|
pageState.currentPage = 1;
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
|
|
||||||
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
|
const processBtn = document.getElementById(
|
||||||
if (processBtn) processBtn.disabled = true;
|
'process-btn'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
if (processBtn) processBtn.disabled = true;
|
||||||
|
|
||||||
const totalPages = document.getElementById('total-pages');
|
const totalPages = document.getElementById('total-pages');
|
||||||
if (totalPages) totalPages.textContent = '0';
|
if (totalPages) totalPages.textContent = '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderPosterizePreview(pageNum: number) {
|
async function renderPosterizePreview(pageNum: number) {
|
||||||
if (!pageState.pdfJsDoc) return;
|
if (!pageState.pdfJsDoc) return;
|
||||||
|
|
||||||
pageState.currentPage = pageNum;
|
pageState.currentPage = pageNum;
|
||||||
showLoader(`Rendering preview for page ${pageNum}...`);
|
showLoader(`Rendering preview for page ${pageNum}...`);
|
||||||
|
|
||||||
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
|
const canvas = document.getElementById(
|
||||||
const context = canvas.getContext('2d');
|
'posterize-preview-canvas'
|
||||||
|
) as HTMLCanvasElement;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
if (!context) {
|
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();
|
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() {
|
function drawGridOverlay() {
|
||||||
if (!pageState.pageSnapshots[pageState.currentPage] || !pageState.pdfJsDoc) return;
|
if (!pageState.pageSnapshots[pageState.currentPage] || !pageState.pdfJsDoc)
|
||||||
|
return;
|
||||||
|
|
||||||
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
|
const canvas = document.getElementById(
|
||||||
const context = canvas.getContext('2d');
|
'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 pageRangeInput = (
|
||||||
const pagesToProcess = parsePageRanges(pageRangeInput, pageState.pdfJsDoc.numPages);
|
document.getElementById('page-range') as HTMLInputElement
|
||||||
|
).value;
|
||||||
|
const pagesToProcess = parsePageRanges(
|
||||||
|
pageRangeInput,
|
||||||
|
pageState.pdfJsDoc.numPages
|
||||||
|
);
|
||||||
|
|
||||||
if (pagesToProcess.includes(pageState.currentPage - 1)) {
|
if (pagesToProcess.includes(pageState.currentPage - 1)) {
|
||||||
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
|
const rows =
|
||||||
const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1;
|
parseInt(
|
||||||
|
(document.getElementById('posterize-rows') as HTMLInputElement).value
|
||||||
|
) || 1;
|
||||||
|
const cols =
|
||||||
|
parseInt(
|
||||||
|
(document.getElementById('posterize-cols') as HTMLInputElement).value
|
||||||
|
) || 1;
|
||||||
|
|
||||||
context.strokeStyle = 'rgba(239, 68, 68, 0.9)';
|
context.strokeStyle = 'rgba(239, 68, 68, 0.9)';
|
||||||
context.lineWidth = 2;
|
context.lineWidth = 2;
|
||||||
context.setLineDash([10, 5]);
|
context.setLineDash([10, 5]);
|
||||||
|
|
||||||
const cellWidth = canvas.width / cols;
|
const cellWidth = canvas.width / cols;
|
||||||
const cellHeight = canvas.height / rows;
|
const cellHeight = canvas.height / rows;
|
||||||
|
|
||||||
for (let i = 1; i < cols; i++) {
|
for (let i = 1; i < cols; i++) {
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
context.moveTo(i * cellWidth, 0);
|
context.moveTo(i * cellWidth, 0);
|
||||||
context.lineTo(i * cellWidth, canvas.height);
|
context.lineTo(i * cellWidth, canvas.height);
|
||||||
context.stroke();
|
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 < rows; i++) {
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(0, i * cellHeight);
|
||||||
|
context.lineTo(canvas.width, i * cellHeight);
|
||||||
|
context.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.setLineDash([]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePreviewNav() {
|
function updatePreviewNav() {
|
||||||
if (!pageState.pdfJsDoc) return;
|
if (!pageState.pdfJsDoc) return;
|
||||||
|
|
||||||
const currentPageSpan = document.getElementById('current-preview-page');
|
const currentPageSpan = document.getElementById('current-preview-page');
|
||||||
const prevBtn = document.getElementById('prev-preview-page') as HTMLButtonElement;
|
const prevBtn = document.getElementById(
|
||||||
const nextBtn = document.getElementById('next-preview-page') as HTMLButtonElement;
|
'prev-preview-page'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
const nextBtn = document.getElementById(
|
||||||
|
'next-preview-page'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
if (currentPageSpan) currentPageSpan.textContent = pageState.currentPage.toString();
|
if (currentPageSpan)
|
||||||
if (prevBtn) prevBtn.disabled = pageState.currentPage <= 1;
|
currentPageSpan.textContent = pageState.currentPage.toString();
|
||||||
if (nextBtn) nextBtn.disabled = pageState.currentPage >= pageState.pdfJsDoc.numPages;
|
if (prevBtn) prevBtn.disabled = pageState.currentPage <= 1;
|
||||||
|
if (nextBtn)
|
||||||
|
nextBtn.disabled = pageState.currentPage >= pageState.pdfJsDoc.numPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function posterize() {
|
async function posterize() {
|
||||||
if (!pageState.pdfJsDoc || !pageState.pdfBytes) {
|
if (!pageState.pdfJsDoc || !pageState.pdfBytes) {
|
||||||
showAlert('No File', 'Please upload a PDF file first.');
|
showAlert('No File', 'Please upload a PDF file first.');
|
||||||
return;
|
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 {
|
if (!tempCtx) {
|
||||||
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
|
throw new Error('Could not create canvas context.');
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
async function updateUI() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
|
const processBtn = document.getElementById(
|
||||||
|
'process-btn'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
if (!fileDisplayArea) return;
|
if (!fileDisplayArea) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (pageState.file) {
|
if (pageState.file) {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = pageState.file.name;
|
nameSpan.textContent = pageState.file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
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.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.onclick = function () {
|
removeBtn.onclick = function () {
|
||||||
resetState();
|
resetState();
|
||||||
};
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||||
if (processBtn) processBtn.disabled = false;
|
if (processBtn) processBtn.disabled = false;
|
||||||
} else {
|
} else {
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFileSelect(files: FileList | null) {
|
async function handleFileSelect(files: FileList | null) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
if (
|
||||||
pageState.file = file;
|
file.type === 'application/pdf' ||
|
||||||
pageState.pdfBytes = new Uint8Array(await file.arrayBuffer());
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
pageState.pdfJsDoc = await getPDFDocument({ data: pageState.pdfBytes }).promise;
|
) {
|
||||||
pageState.pageSnapshots = {};
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
pageState.currentPage = 1;
|
if (!result) return;
|
||||||
|
|
||||||
const totalPagesSpan = document.getElementById('total-pages');
|
pageState.file = result.file;
|
||||||
const totalPreviewPages = document.getElementById('total-preview-pages');
|
pageState.pdfBytes = new Uint8Array(result.bytes);
|
||||||
|
pageState.pdfJsDoc = result.pdf;
|
||||||
|
pageState.pageSnapshots = {};
|
||||||
|
pageState.currentPage = 1;
|
||||||
|
|
||||||
if (totalPagesSpan && pageState.pdfJsDoc) {
|
const totalPagesSpan = document.getElementById('total-pages');
|
||||||
totalPagesSpan.textContent = pageState.pdfJsDoc.numPages.toString();
|
const totalPreviewPages = document.getElementById('total-preview-pages');
|
||||||
}
|
|
||||||
if (totalPreviewPages && pageState.pdfJsDoc) {
|
|
||||||
totalPreviewPages.textContent = pageState.pdfJsDoc.numPages.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateUI();
|
if (totalPagesSpan && pageState.pdfJsDoc) {
|
||||||
await renderPosterizePreview(1);
|
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 () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
|
const processBtn = document.getElementById(
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
'process-btn'
|
||||||
const prevBtn = document.getElementById('prev-preview-page');
|
) as HTMLButtonElement;
|
||||||
const nextBtn = document.getElementById('next-preview-page');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
const rowsInput = document.getElementById('posterize-rows');
|
const prevBtn = document.getElementById('prev-preview-page');
|
||||||
const colsInput = document.getElementById('posterize-cols');
|
const nextBtn = document.getElementById('next-preview-page');
|
||||||
const pageRangeInput = document.getElementById('page-range');
|
const rowsInput = document.getElementById('posterize-rows');
|
||||||
|
const colsInput = document.getElementById('posterize-cols');
|
||||||
|
const pageRangeInput = document.getElementById('page-range');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', function () {
|
backBtn.addEventListener('click', function () {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
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('click', function () {
|
||||||
fileInput.addEventListener('change', function (e) {
|
fileInput.value = '';
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', function (e) {
|
// Preview navigation
|
||||||
e.preventDefault();
|
if (prevBtn) {
|
||||||
dropZone.classList.add('bg-gray-700');
|
prevBtn.addEventListener('click', function () {
|
||||||
});
|
if (pageState.currentPage > 1) {
|
||||||
|
renderPosterizePreview(pageState.currentPage - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', function (e) {
|
if (nextBtn) {
|
||||||
e.preventDefault();
|
nextBtn.addEventListener('click', function () {
|
||||||
dropZone.classList.remove('bg-gray-700');
|
if (
|
||||||
});
|
pageState.pdfJsDoc &&
|
||||||
|
pageState.currentPage < pageState.pdfJsDoc.numPages
|
||||||
|
) {
|
||||||
|
renderPosterizePreview(pageState.currentPage + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('drop', function (e) {
|
// Grid input changes trigger overlay redraw
|
||||||
e.preventDefault();
|
if (rowsInput) {
|
||||||
dropZone.classList.remove('bg-gray-700');
|
rowsInput.addEventListener('input', drawGridOverlay);
|
||||||
const files = e.dataTransfer?.files;
|
}
|
||||||
if (files && files.length > 0) {
|
if (colsInput) {
|
||||||
const pdfFiles = Array.from(files).filter(function (f) {
|
colsInput.addEventListener('input', drawGridOverlay);
|
||||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
}
|
||||||
});
|
if (pageRangeInput) {
|
||||||
if (pdfFiles.length > 0) {
|
pageRangeInput.addEventListener('input', drawGridOverlay);
|
||||||
const dataTransfer = new DataTransfer();
|
}
|
||||||
dataTransfer.items.add(pdfFiles[0]);
|
|
||||||
handleFileSelect(dataTransfer.files);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput.addEventListener('click', function () {
|
// Process button
|
||||||
fileInput.value = '';
|
if (processBtn) {
|
||||||
});
|
processBtn.addEventListener('click', posterize);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||||
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -104,6 +105,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
showLoader('Loading engine...');
|
showLoader('Loading engine...');
|
||||||
const pymupdf = await loadPyMuPDF();
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
state.files = await batchDecryptIfNeeded(state.files);
|
||||||
|
showLoader('Extracting...');
|
||||||
|
|
||||||
const total = state.files.length;
|
const total = state.files.length;
|
||||||
let completed = 0;
|
let completed = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
@@ -128,13 +133,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Multiple files - create ZIP
|
|
||||||
const JSZip = (await import('jszip')).default;
|
const JSZip = (await import('jszip')).default;
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
const usedNames = new Set<string>();
|
const usedNames = new Set<string>();
|
||||||
|
|
||||||
for (const file of state.files) {
|
for (let fi = 0; fi < state.files.length; fi++) {
|
||||||
try {
|
try {
|
||||||
|
const file = state.files[fi];
|
||||||
showLoader(
|
showLoader(
|
||||||
`Extracting ${file.name} for AI (${completed + 1}/${total})...`
|
`Extracting ${file.name} for AI (${completed + 1}/${total})...`
|
||||||
);
|
);
|
||||||
@@ -147,7 +152,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
completed++;
|
completed++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to extract ${file.name}:`, error);
|
console.error(`Failed to extract ${state.files[fi].name}:`, error);
|
||||||
failed++;
|
failed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide';
|
|||||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -123,6 +124,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
document.getElementById('rasterize-grayscale') as HTMLInputElement
|
document.getElementById('rasterize-grayscale') as HTMLInputElement
|
||||||
).checked;
|
).checked;
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
state.files = await batchDecryptIfNeeded(state.files);
|
||||||
|
showLoader('Rasterizing...');
|
||||||
|
|
||||||
const total = state.files.length;
|
const total = state.files.length;
|
||||||
let completed = 0;
|
let completed = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
@@ -149,13 +154,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
() => resetState()
|
() => resetState()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Multiple files - create ZIP
|
|
||||||
const JSZip = (await import('jszip')).default;
|
const JSZip = (await import('jszip')).default;
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
const usedNames = new Set<string>();
|
const usedNames = new Set<string>();
|
||||||
|
|
||||||
for (const file of state.files) {
|
for (let fi = 0; fi < state.files.length; fi++) {
|
||||||
try {
|
try {
|
||||||
|
const file = state.files[fi];
|
||||||
showLoader(
|
showLoader(
|
||||||
`Rasterizing ${file.name} (${completed + 1}/${total})...`
|
`Rasterizing ${file.name} (${completed + 1}/${total})...`
|
||||||
);
|
);
|
||||||
@@ -174,7 +179,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
completed++;
|
completed++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to rasterize ${file.name}:`, error);
|
console.error(
|
||||||
|
`Failed to rasterize ${state.files[fi].name}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
failed++;
|
failed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,71 @@
|
|||||||
import { PDFDocument, PDFName } from 'pdf-lib';
|
import { PDFDocument, PDFName } from 'pdf-lib';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
// State management
|
// State management
|
||||||
const pageState: { pdfDoc: PDFDocument | null; file: File | null } = {
|
const pageState: { pdfDoc: PDFDocument | null; file: File | null } = {
|
||||||
pdfDoc: null,
|
pdfDoc: null,
|
||||||
file: null,
|
file: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// UI helpers
|
// UI helpers
|
||||||
function showLoader(message: string = 'Processing...') {
|
function showLoader(message: string = 'Processing...') {
|
||||||
const loader = document.getElementById('loader-modal');
|
const loader = document.getElementById('loader-modal');
|
||||||
const loaderText = document.getElementById('loader-text');
|
const loaderText = document.getElementById('loader-text');
|
||||||
if (loader) loader.classList.remove('hidden');
|
if (loader) loader.classList.remove('hidden');
|
||||||
if (loaderText) loaderText.textContent = message;
|
if (loaderText) loaderText.textContent = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideLoader() {
|
function hideLoader() {
|
||||||
const loader = document.getElementById('loader-modal');
|
const loader = document.getElementById('loader-modal');
|
||||||
if (loader) loader.classList.add('hidden');
|
if (loader) loader.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAlert(title: string, message: string, type: string = 'error', callback?: () => void) {
|
function showAlert(
|
||||||
const modal = document.getElementById('alert-modal');
|
title: string,
|
||||||
const alertTitle = document.getElementById('alert-title');
|
message: string,
|
||||||
const alertMessage = document.getElementById('alert-message');
|
type: string = 'error',
|
||||||
const okBtn = document.getElementById('alert-ok');
|
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 (alertTitle) alertTitle.textContent = title;
|
||||||
if (alertMessage) alertMessage.textContent = message;
|
if (alertMessage) alertMessage.textContent = message;
|
||||||
if (modal) modal.classList.remove('hidden');
|
if (modal) modal.classList.remove('hidden');
|
||||||
|
|
||||||
if (okBtn) {
|
if (okBtn) {
|
||||||
const newOkBtn = okBtn.cloneNode(true) as HTMLElement;
|
const newOkBtn = okBtn.cloneNode(true) as HTMLElement;
|
||||||
okBtn.replaceWith(newOkBtn);
|
okBtn.replaceWith(newOkBtn);
|
||||||
newOkBtn.addEventListener('click', () => {
|
newOkBtn.addEventListener('click', () => {
|
||||||
modal?.classList.add('hidden');
|
modal?.classList.add('hidden');
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadFile(blob: Blob, filename: string) {
|
function downloadFile(blob: Blob, filename: string) {
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = filename;
|
a.download = filename;
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFileDisplay() {
|
function updateFileDisplay() {
|
||||||
const displayArea = document.getElementById('file-display-area');
|
const displayArea = document.getElementById('file-display-area');
|
||||||
if (!displayArea || !pageState.file || !pageState.pdfDoc) return;
|
if (!displayArea || !pageState.file || !pageState.pdfDoc) return;
|
||||||
|
|
||||||
const fileSize = pageState.file.size < 1024 * 1024
|
const fileSize =
|
||||||
? `${(pageState.file.size / 1024).toFixed(1)} KB`
|
pageState.file.size < 1024 * 1024
|
||||||
: `${(pageState.file.size / 1024 / 1024).toFixed(2)} MB`;
|
? `${(pageState.file.size / 1024).toFixed(1)} KB`
|
||||||
const pageCount = pageState.pdfDoc.getPageCount();
|
: `${(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="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 items-center justify-between">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
@@ -72,105 +79,114 @@ function updateFileDisplay() {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
document.getElementById('remove-file')?.addEventListener('click', () => resetState());
|
document
|
||||||
|
.getElementById('remove-file')
|
||||||
|
?.addEventListener('click', () => resetState());
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
pageState.pdfDoc = null;
|
pageState.pdfDoc = null;
|
||||||
pageState.file = null;
|
pageState.file = null;
|
||||||
const displayArea = document.getElementById('file-display-area');
|
const displayArea = document.getElementById('file-display-area');
|
||||||
if (displayArea) displayArea.innerHTML = '';
|
if (displayArea) displayArea.innerHTML = '';
|
||||||
document.getElementById('options-panel')?.classList.add('hidden');
|
document.getElementById('options-panel')?.classList.add('hidden');
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// File handling
|
// File handling
|
||||||
async function handleFileUpload(file: File) {
|
async function handleFileUpload(file: File) {
|
||||||
if (!file || file.type !== 'application/pdf') {
|
if (!file || file.type !== 'application/pdf') {
|
||||||
showAlert('Error', 'Please upload a valid PDF file.');
|
showAlert('Error', 'Please upload a valid PDF file.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
|
if (!result) return;
|
||||||
showLoader('Loading PDF...');
|
showLoader('Loading PDF...');
|
||||||
try {
|
result.pdf.destroy();
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
pageState.pdfDoc = await PDFDocument.load(result.bytes);
|
||||||
pageState.pdfDoc = await PDFDocument.load(arrayBuffer);
|
pageState.file = result.file;
|
||||||
pageState.file = file;
|
updateFileDisplay();
|
||||||
updateFileDisplay();
|
document.getElementById('options-panel')?.classList.remove('hidden');
|
||||||
document.getElementById('options-panel')?.classList.remove('hidden');
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error(error);
|
||||||
console.error(error);
|
showAlert('Error', 'Failed to load PDF file.');
|
||||||
showAlert('Error', 'Failed to load PDF file.');
|
} finally {
|
||||||
} finally {
|
hideLoader();
|
||||||
hideLoader();
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process function
|
// Process function
|
||||||
async function processRemoveAnnotations() {
|
async function processRemoveAnnotations() {
|
||||||
if (!pageState.pdfDoc) {
|
if (!pageState.pdfDoc) {
|
||||||
showAlert('Error', 'Please upload a PDF file first.');
|
showAlert('Error', 'Please upload a PDF file first.');
|
||||||
return;
|
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...');
|
const newPdfBytes = await pageState.pdfDoc.save();
|
||||||
try {
|
downloadFile(
|
||||||
const pages = pageState.pdfDoc.getPages();
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'annotations-removed.pdf'
|
||||||
// Remove all annotations from all pages
|
);
|
||||||
for (let i = 0; i < pages.length; i++) {
|
showAlert('Success', 'Annotations removed successfully!', 'success', () => {
|
||||||
const page = pages[i];
|
resetState();
|
||||||
const annotRefs = page.node.Annots()?.asArray() || [];
|
});
|
||||||
if (annotRefs.length > 0) {
|
} catch (e) {
|
||||||
page.node.delete(PDFName.of('Annots'));
|
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
|
// Initialize
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
fileInput?.addEventListener('change', (e) => {
|
fileInput?.addEventListener('change', (e) => {
|
||||||
const file = (e.target as HTMLInputElement).files?.[0];
|
const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
if (file) handleFileUpload(file);
|
if (file) handleFileUpload(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone?.addEventListener('dragover', (e) => {
|
dropZone?.addEventListener('dragover', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.add('border-indigo-500');
|
dropZone.classList.add('border-indigo-500');
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone?.addEventListener('dragleave', () => {
|
dropZone?.addEventListener('dragleave', () => {
|
||||||
dropZone.classList.remove('border-indigo-500');
|
dropZone.classList.remove('border-indigo-500');
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone?.addEventListener('drop', (e) => {
|
dropZone?.addEventListener('drop', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.remove('border-indigo-500');
|
dropZone.classList.remove('border-indigo-500');
|
||||||
const file = e.dataTransfer?.files[0];
|
const file = e.dataTransfer?.files[0];
|
||||||
if (file) handleFileUpload(file);
|
if (file) handleFileUpload(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
processBtn?.addEventListener('click', processRemoveAnnotations);
|
processBtn?.addEventListener('click', processRemoveAnnotations);
|
||||||
|
|
||||||
backBtn?.addEventListener('click', () => {
|
backBtn?.addEventListener('click', () => {
|
||||||
window.location.href = '../../index.html';
|
window.location.href = '../../index.html';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { PDFDocument } from 'pdf-lib';
|
|||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { initPagePreview } from '../utils/page-preview.js';
|
import { initPagePreview } from '../utils/page-preview.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -116,11 +117,13 @@ async function handleFileUpload(file: File) {
|
|||||||
showAlert('Error', 'Please upload a valid PDF file.');
|
showAlert('Error', 'Please upload a valid PDF file.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader('Loading PDF...');
|
|
||||||
try {
|
try {
|
||||||
const buf = await file.arrayBuffer();
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
pageState.pdfDoc = await PDFDocument.load(buf);
|
if (!result) return;
|
||||||
pageState.file = file;
|
showLoader('Loading PDF...');
|
||||||
|
result.pdf.destroy();
|
||||||
|
pageState.pdfDoc = await PDFDocument.load(result.bytes);
|
||||||
|
pageState.file = result.file;
|
||||||
pageState.detectedBlankPages = [];
|
pageState.detectedBlankPages = [];
|
||||||
updateFileDisplay();
|
updateFileDisplay();
|
||||||
document.getElementById('options-panel')?.classList.remove('hidden');
|
document.getElementById('options-panel')?.classList.remove('hidden');
|
||||||
|
|||||||
@@ -2,202 +2,226 @@ import { showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { PDFDocument, PDFName } from 'pdf-lib';
|
import { PDFDocument, PDFName } from 'pdf-lib';
|
||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
interface PageState {
|
interface PageState {
|
||||||
file: File | null;
|
file: File | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageState: PageState = {
|
const pageState: PageState = {
|
||||||
file: null,
|
file: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function removeMetadataFromDoc(pdfDoc: PDFDocument) {
|
function removeMetadataFromDoc(pdfDoc: PDFDocument) {
|
||||||
const infoDict = (pdfDoc as any).getInfoDict();
|
// @ts-expect-error getInfoDict is private but accessible at runtime
|
||||||
const allKeys = infoDict.keys();
|
const infoDict = pdfDoc.getInfoDict();
|
||||||
allKeys.forEach((key: any) => {
|
const allKeys = infoDict.keys();
|
||||||
infoDict.delete(key);
|
allKeys.forEach((key: { asString: () => string }) => {
|
||||||
});
|
infoDict.delete(key);
|
||||||
|
});
|
||||||
|
|
||||||
pdfDoc.setTitle('');
|
pdfDoc.setTitle('');
|
||||||
pdfDoc.setAuthor('');
|
pdfDoc.setAuthor('');
|
||||||
pdfDoc.setSubject('');
|
pdfDoc.setSubject('');
|
||||||
pdfDoc.setKeywords([]);
|
pdfDoc.setKeywords([]);
|
||||||
pdfDoc.setCreator('');
|
pdfDoc.setCreator('');
|
||||||
pdfDoc.setProducer('');
|
pdfDoc.setProducer('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const catalogDict = (pdfDoc.catalog as any).dict;
|
// @ts-expect-error catalog.dict is private but accessible at runtime
|
||||||
if (catalogDict.has(PDFName.of('Metadata'))) {
|
const catalogDict = pdfDoc.catalog.dict;
|
||||||
catalogDict.delete(PDFName.of('Metadata'));
|
if (catalogDict.has(PDFName.of('Metadata'))) {
|
||||||
}
|
catalogDict.delete(PDFName.of('Metadata'));
|
||||||
} catch (e: any) {
|
|
||||||
console.warn('Could not remove XMP metadata:', e.message);
|
|
||||||
}
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
console.warn('Could not remove XMP metadata:', msg);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const context = pdfDoc.context;
|
const context = pdfDoc.context;
|
||||||
if ((context as any).trailerInfo) {
|
if (context.trailerInfo) {
|
||||||
delete (context as any).trailerInfo.ID;
|
delete context.trailerInfo.ID;
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
console.warn('Could not remove document IDs:', e.message);
|
|
||||||
}
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
console.warn('Could not remove document IDs:', msg);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const catalogDict = (pdfDoc.catalog as any).dict;
|
// @ts-expect-error catalog.dict is private but accessible at runtime
|
||||||
if (catalogDict.has(PDFName.of('PieceInfo'))) {
|
const catalogDict = pdfDoc.catalog.dict;
|
||||||
catalogDict.delete(PDFName.of('PieceInfo'));
|
if (catalogDict.has(PDFName.of('PieceInfo'))) {
|
||||||
}
|
catalogDict.delete(PDFName.of('PieceInfo'));
|
||||||
} catch (e: any) {
|
|
||||||
console.warn('Could not remove PieceInfo:', e.message);
|
|
||||||
}
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
console.warn('Could not remove PieceInfo:', msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
pageState.file = null;
|
pageState.file = null;
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUI() {
|
async function updateUI() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
|
|
||||||
if (!fileDisplayArea) return;
|
if (!fileDisplayArea) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (pageState.file) {
|
if (pageState.file) {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = pageState.file.name;
|
nameSpan.textContent = pageState.file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
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.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.onclick = function () {
|
removeBtn.onclick = function () {
|
||||||
resetState();
|
resetState();
|
||||||
};
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||||
} else {
|
} else {
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileSelect(files: FileList | null) {
|
function handleFileSelect(files: FileList | null) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
if (
|
||||||
pageState.file = file;
|
file.type === 'application/pdf' ||
|
||||||
updateUI();
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
}
|
) {
|
||||||
|
pageState.file = file;
|
||||||
|
updateUI();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeMetadata() {
|
async function removeMetadata() {
|
||||||
if (!pageState.file) {
|
if (!pageState.file) {
|
||||||
showAlert('No File', 'Please upload a PDF file first.');
|
showAlert('No File', 'Please upload a PDF file first.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loaderModal = document.getElementById('loader-modal');
|
const loaderModal = document.getElementById('loader-modal');
|
||||||
const loaderText = document.getElementById('loader-text');
|
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 (loaderModal) loaderModal.classList.remove('hidden');
|
||||||
if (loaderText) loaderText.textContent = 'Removing all metadata...';
|
if (loaderText) loaderText.textContent = 'Removing all metadata...';
|
||||||
|
result.pdf.destroy();
|
||||||
|
const pdfDoc = await PDFDocument.load(result.bytes);
|
||||||
|
|
||||||
try {
|
removeMetadataFromDoc(pdfDoc);
|
||||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
|
||||||
const pdfDoc = await PDFDocument.load(arrayBuffer);
|
|
||||||
|
|
||||||
removeMetadataFromDoc(pdfDoc);
|
const newPdfBytes = await pdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
const newPdfBytes = await pdfDoc.save();
|
new Blob([newPdfBytes as BlobPart], { type: 'application/pdf' }),
|
||||||
downloadFile(
|
'metadata-removed.pdf'
|
||||||
new Blob([newPdfBytes as BlobPart], { type: 'application/pdf' }),
|
);
|
||||||
'metadata-removed.pdf'
|
showAlert('Success', 'Metadata removed successfully!', 'success', () => {
|
||||||
);
|
resetState();
|
||||||
showAlert('Success', 'Metadata removed successfully!', 'success', () => { resetState(); });
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'An error occurred while trying to remove metadata.');
|
showAlert('Error', 'An error occurred while trying to remove metadata.');
|
||||||
} finally {
|
} finally {
|
||||||
if (loaderModal) loaderModal.classList.add('hidden');
|
if (loaderModal) loaderModal.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', function () {
|
backBtn.addEventListener('click', function () {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
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('click', function () {
|
||||||
fileInput.addEventListener('change', function (e) {
|
fileInput.value = '';
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', function (e) {
|
if (processBtn) {
|
||||||
e.preventDefault();
|
processBtn.addEventListener('click', removeMetadata);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||||
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
export async function repairPdfFile(file: File): Promise<Uint8Array | null> {
|
export async function repairPdfFile(file: File): Promise<Uint8Array | null> {
|
||||||
const inputPath = '/input.pdf';
|
const inputPath = '/input.pdf';
|
||||||
@@ -67,7 +68,9 @@ export async function repairPdf() {
|
|||||||
const failedRepairs: string[] = [];
|
const failedRepairs: string[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const decryptedFiles = await batchDecryptIfNeeded(state.files);
|
||||||
showLoader('Initializing repair engine...');
|
showLoader('Initializing repair engine...');
|
||||||
|
state.files = decryptedFiles;
|
||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
const file = state.files[i];
|
const file = state.files[i];
|
||||||
@@ -105,7 +108,9 @@ export async function repairPdf() {
|
|||||||
|
|
||||||
if (successfulRepairs.length === 1) {
|
if (successfulRepairs.length === 1) {
|
||||||
const file = successfulRepairs[0];
|
const file = successfulRepairs[0];
|
||||||
const blob = new Blob([file.data as any], { type: 'application/pdf' });
|
const blob = new Blob([new Uint8Array(file.data)], {
|
||||||
|
type: 'application/pdf',
|
||||||
|
});
|
||||||
downloadFile(blob, file.name);
|
downloadFile(blob, file.name);
|
||||||
} else {
|
} else {
|
||||||
showLoader('Creating ZIP archive...');
|
showLoader('Creating ZIP archive...');
|
||||||
@@ -124,7 +129,7 @@ export async function repairPdf() {
|
|||||||
if (failedRepairs.length === 0) {
|
if (failedRepairs.length === 0) {
|
||||||
showAlert('Success', 'All files repaired successfully!');
|
showAlert('Success', 'All files repaired successfully!');
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error('Critical error during repair:', error);
|
console.error('Critical error during repair:', error);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert(
|
showAlert(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createIcons, icons } from 'lucide';
|
|||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||||
|
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
interface ReverseState {
|
interface ReverseState {
|
||||||
files: File[];
|
files: File[];
|
||||||
@@ -76,75 +77,61 @@ function updateUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function reverseSingleFile(file: File): Promise<Uint8Array> {
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
||||||
|
|
||||||
|
const newPdf = await PDFLibDocument.create();
|
||||||
|
const pageCount = pdfDoc.getPageCount();
|
||||||
|
const reversedIndices = Array.from({ length: pageCount }, function (_, i) {
|
||||||
|
return pageCount - 1 - i;
|
||||||
|
});
|
||||||
|
|
||||||
|
const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices);
|
||||||
|
copiedPages.forEach(function (page) {
|
||||||
|
newPdf.addPage(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
return newPdf.save();
|
||||||
|
}
|
||||||
|
|
||||||
async function reversePages() {
|
async function reversePages() {
|
||||||
if (reverseState.files.length === 0) {
|
if (reverseState.files.length === 0) {
|
||||||
showAlert('No Files', 'Please select one or more PDF files.');
|
showAlert('No Files', 'Please select one or more PDF files.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Reversing page order...');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const decryptedFiles = await batchDecryptIfNeeded(reverseState.files);
|
||||||
|
showLoader('Reversing page order...');
|
||||||
|
reverseState.files = decryptedFiles;
|
||||||
|
|
||||||
|
const validFiles = reverseState.files.filter(function (f) {
|
||||||
|
return f !== null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validFiles.length === 0) {
|
||||||
|
hideLoader();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
const usedNames = new Set<string>();
|
const usedNames = new Set<string>();
|
||||||
|
|
||||||
for (let j = 0; j < reverseState.files.length; j++) {
|
for (let j = 0; j < validFiles.length; j++) {
|
||||||
const file = reverseState.files[j];
|
const file = validFiles[j];
|
||||||
showLoader(
|
showLoader(`Reversing ${file.name} (${j + 1}/${validFiles.length})...`);
|
||||||
`Processing ${file.name} (${j + 1}/${reverseState.files.length})...`
|
|
||||||
);
|
|
||||||
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const newPdfBytes = await reverseSingleFile(file);
|
||||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
|
||||||
ignoreEncryption: true,
|
|
||||||
throwOnInvalidObject: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const newPdf = await PDFLibDocument.create();
|
|
||||||
const pageCount = pdfDoc.getPageCount();
|
|
||||||
const reversedIndices = Array.from(
|
|
||||||
{ length: pageCount },
|
|
||||||
function (_, i) {
|
|
||||||
return pageCount - 1 - i;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices);
|
|
||||||
copiedPages.forEach(function (page) {
|
|
||||||
newPdf.addPage(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
const newPdfBytes = await newPdf.save();
|
|
||||||
const originalName = file.name.replace(/\.pdf$/i, '');
|
const originalName = file.name.replace(/\.pdf$/i, '');
|
||||||
const fileName = `${originalName}_reversed.pdf`;
|
const fileName = `${originalName}_reversed.pdf`;
|
||||||
const zipEntryName = deduplicateFileName(fileName, usedNames);
|
const zipEntryName = deduplicateFileName(fileName, usedNames);
|
||||||
zip.file(zipEntryName, newPdfBytes);
|
zip.file(zipEntryName, newPdfBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reverseState.files.length === 1) {
|
if (validFiles.length === 1) {
|
||||||
// Single file: download directly
|
const file = validFiles[0];
|
||||||
const file = reverseState.files[0];
|
const newPdfBytes = await reverseSingleFile(file);
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
|
||||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
|
||||||
ignoreEncryption: true,
|
|
||||||
throwOnInvalidObject: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const newPdf = await PDFLibDocument.create();
|
|
||||||
const pageCount = pdfDoc.getPageCount();
|
|
||||||
const reversedIndices = Array.from(
|
|
||||||
{ length: pageCount },
|
|
||||||
function (_, i) {
|
|
||||||
return pageCount - 1 - i;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices);
|
|
||||||
copiedPages.forEach(function (page) {
|
|
||||||
newPdf.addPage(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
const newPdfBytes = await newPdf.save();
|
|
||||||
const originalName = file.name.replace(/\.pdf$/i, '');
|
const originalName = file.name.replace(/\.pdf$/i, '');
|
||||||
|
|
||||||
downloadFile(
|
downloadFile(
|
||||||
@@ -152,7 +139,6 @@ async function reversePages() {
|
|||||||
`${originalName}_reversed.pdf`
|
`${originalName}_reversed.pdf`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Multiple files: download as ZIP
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, 'reversed_pdfs.zip');
|
downloadFile(zipBlob, 'reversed_pdfs.zip');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,386 +1,443 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PDFDocument as PDFLibDocument, degrees } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument, degrees } from 'pdf-lib';
|
||||||
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js';
|
import {
|
||||||
|
renderPagesProgressively,
|
||||||
|
cleanupLazyRendering,
|
||||||
|
} from '../utils/render-utils.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
|
import.meta.url
|
||||||
|
).toString();
|
||||||
|
|
||||||
interface RotateState {
|
interface RotateState {
|
||||||
file: File | null;
|
file: File | null;
|
||||||
pdfDoc: PDFLibDocument | null;
|
pdfDoc: PDFLibDocument | null;
|
||||||
pdfJsDoc: pdfjsLib.PDFDocumentProxy | null;
|
pdfJsDoc: pdfjsLib.PDFDocumentProxy | null;
|
||||||
rotations: number[];
|
rotations: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageState: RotateState = {
|
const pageState: RotateState = {
|
||||||
file: null,
|
file: null,
|
||||||
pdfDoc: null,
|
pdfDoc: null,
|
||||||
pdfJsDoc: null,
|
pdfJsDoc: null,
|
||||||
rotations: [],
|
rotations: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
cleanupLazyRendering();
|
cleanupLazyRendering();
|
||||||
pageState.file = null;
|
pageState.file = null;
|
||||||
pageState.pdfDoc = null;
|
pageState.pdfDoc = null;
|
||||||
pageState.pdfJsDoc = null;
|
pageState.pdfJsDoc = null;
|
||||||
pageState.rotations = [];
|
pageState.rotations = [];
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
|
||||||
const pageThumbnails = document.getElementById('page-thumbnails');
|
const pageThumbnails = document.getElementById('page-thumbnails');
|
||||||
if (pageThumbnails) pageThumbnails.innerHTML = '';
|
if (pageThumbnails) pageThumbnails.innerHTML = '';
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
|
|
||||||
const batchAngle = document.getElementById('batch-custom-angle') as HTMLInputElement;
|
const batchAngle = document.getElementById(
|
||||||
if (batchAngle) batchAngle.value = '0';
|
'batch-custom-angle'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
if (batchAngle) batchAngle.value = '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAllRotationDisplays() {
|
function updateAllRotationDisplays() {
|
||||||
for (let i = 0; i < pageState.rotations.length; i++) {
|
for (let i = 0; i < pageState.rotations.length; i++) {
|
||||||
const input = document.getElementById(`page-angle-${i}`) as HTMLInputElement;
|
const input = document.getElementById(
|
||||||
if (input) input.value = pageState.rotations[i].toString();
|
`page-angle-${i}`
|
||||||
const container = document.querySelector(`[data-page-index="${i}"]`);
|
) as HTMLInputElement;
|
||||||
if (container) {
|
if (input) input.value = pageState.rotations[i].toString();
|
||||||
const wrapper = container.querySelector('.thumbnail-wrapper') as HTMLElement;
|
const container = document.querySelector(`[data-page-index="${i}"]`);
|
||||||
if (wrapper) wrapper.style.transform = `rotate(${-pageState.rotations[i]}deg)`;
|
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 {
|
function createPageWrapper(
|
||||||
const pageIndex = pageNumber - 1;
|
canvas: HTMLCanvasElement,
|
||||||
|
pageNumber: number
|
||||||
|
): HTMLElement {
|
||||||
|
const pageIndex = pageNumber - 1;
|
||||||
|
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.className = 'page-thumbnail relative bg-gray-700 rounded-lg overflow-hidden';
|
container.className =
|
||||||
container.dataset.pageIndex = pageIndex.toString();
|
'page-thumbnail relative bg-gray-700 rounded-lg overflow-hidden';
|
||||||
container.dataset.pageNumber = pageNumber.toString();
|
container.dataset.pageIndex = pageIndex.toString();
|
||||||
|
container.dataset.pageNumber = pageNumber.toString();
|
||||||
|
|
||||||
const canvasWrapper = document.createElement('div');
|
const canvasWrapper = document.createElement('div');
|
||||||
canvasWrapper.className = 'thumbnail-wrapper flex items-center justify-center p-2 h-36';
|
canvasWrapper.className =
|
||||||
canvasWrapper.style.transition = 'transform 0.3s ease';
|
'thumbnail-wrapper flex items-center justify-center p-2 h-36';
|
||||||
// Apply initial rotation if it exists (negated for canvas display)
|
canvasWrapper.style.transition = 'transform 0.3s ease';
|
||||||
const initialRotation = pageState.rotations[pageIndex] || 0;
|
// Apply initial rotation if it exists (negated for canvas display)
|
||||||
canvasWrapper.style.transform = `rotate(${-initialRotation}deg)`;
|
const initialRotation = pageState.rotations[pageIndex] || 0;
|
||||||
|
canvasWrapper.style.transform = `rotate(${-initialRotation}deg)`;
|
||||||
|
|
||||||
canvas.className = 'max-w-full max-h-full object-contain';
|
canvas.className = 'max-w-full max-h-full object-contain';
|
||||||
canvasWrapper.appendChild(canvas);
|
canvasWrapper.appendChild(canvas);
|
||||||
|
|
||||||
const pageLabel = document.createElement('div');
|
const pageLabel = document.createElement('div');
|
||||||
pageLabel.className = 'absolute top-1 left-1 bg-black bg-opacity-60 text-white text-xs px-2 py-1 rounded';
|
pageLabel.className =
|
||||||
pageLabel.textContent = `${pageNumber}`;
|
'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(canvasWrapper);
|
||||||
container.appendChild(pageLabel);
|
container.appendChild(pageLabel);
|
||||||
|
|
||||||
// Per-page rotation controls - Custom angle input
|
// Per-page rotation controls - Custom angle input
|
||||||
const controls = document.createElement('div');
|
const controls = document.createElement('div');
|
||||||
controls.className = 'flex items-center justify-center gap-1 p-2 bg-gray-800';
|
controls.className = 'flex items-center justify-center gap-1 p-2 bg-gray-800';
|
||||||
|
|
||||||
const decrementBtn = document.createElement('button');
|
const decrementBtn = document.createElement('button');
|
||||||
decrementBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-sm';
|
decrementBtn.className =
|
||||||
decrementBtn.textContent = '-';
|
'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.onclick = function (e) {
|
decrementBtn.textContent = '-';
|
||||||
e.stopPropagation();
|
decrementBtn.onclick = function (e) {
|
||||||
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement;
|
e.stopPropagation();
|
||||||
const current = parseInt(input.value) || 0;
|
const input = document.getElementById(
|
||||||
input.value = (current - 1).toString();
|
`page-angle-${pageIndex}`
|
||||||
};
|
) as HTMLInputElement;
|
||||||
|
const current = parseInt(input.value) || 0;
|
||||||
|
input.value = (current - 1).toString();
|
||||||
|
};
|
||||||
|
|
||||||
const angleInput = document.createElement('input');
|
const angleInput = document.createElement('input');
|
||||||
angleInput.type = 'number';
|
angleInput.type = 'number';
|
||||||
angleInput.id = `page-angle-${pageIndex}`;
|
angleInput.id = `page-angle-${pageIndex}`;
|
||||||
angleInput.value = pageState.rotations[pageIndex]?.toString() || '0';
|
angleInput.value = pageState.rotations[pageIndex]?.toString() || '0';
|
||||||
angleInput.className = 'w-12 h-8 text-center bg-gray-700 border border-gray-600 text-white rounded text-xs';
|
angleInput.className =
|
||||||
|
'w-12 h-8 text-center bg-gray-700 border border-gray-600 text-white rounded text-xs';
|
||||||
|
|
||||||
const incrementBtn = document.createElement('button');
|
const incrementBtn = document.createElement('button');
|
||||||
incrementBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-sm';
|
incrementBtn.className =
|
||||||
incrementBtn.textContent = '+';
|
'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.onclick = function (e) {
|
incrementBtn.textContent = '+';
|
||||||
e.stopPropagation();
|
incrementBtn.onclick = function (e) {
|
||||||
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement;
|
e.stopPropagation();
|
||||||
const current = parseInt(input.value) || 0;
|
const input = document.getElementById(
|
||||||
input.value = (current + 1).toString();
|
`page-angle-${pageIndex}`
|
||||||
};
|
) as HTMLInputElement;
|
||||||
|
const current = parseInt(input.value) || 0;
|
||||||
|
input.value = (current + 1).toString();
|
||||||
|
};
|
||||||
|
|
||||||
const applyBtn = document.createElement('button');
|
const applyBtn = document.createElement('button');
|
||||||
applyBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600';
|
applyBtn.className =
|
||||||
applyBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-4 h-4"></i>';
|
'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600';
|
||||||
applyBtn.onclick = function (e) {
|
applyBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-4 h-4"></i>';
|
||||||
e.stopPropagation();
|
applyBtn.onclick = function (e) {
|
||||||
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement;
|
e.stopPropagation();
|
||||||
const angle = parseInt(input.value) || 0;
|
const input = document.getElementById(
|
||||||
pageState.rotations[pageIndex] = angle;
|
`page-angle-${pageIndex}`
|
||||||
const wrapper = container.querySelector('.thumbnail-wrapper') as HTMLElement;
|
) as HTMLInputElement;
|
||||||
if (wrapper) wrapper.style.transform = `rotate(${-angle}deg)`;
|
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);
|
controls.append(decrementBtn, angleInput, incrementBtn, applyBtn);
|
||||||
container.appendChild(controls);
|
container.appendChild(controls);
|
||||||
|
|
||||||
// Re-create icons for the new element
|
// Re-create icons for the new element
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderThumbnails() {
|
async function renderThumbnails() {
|
||||||
const pageThumbnails = document.getElementById('page-thumbnails');
|
const pageThumbnails = document.getElementById('page-thumbnails');
|
||||||
if (!pageThumbnails || !pageState.pdfJsDoc) return;
|
if (!pageThumbnails || !pageState.pdfJsDoc) return;
|
||||||
|
|
||||||
pageThumbnails.innerHTML = '';
|
pageThumbnails.innerHTML = '';
|
||||||
|
|
||||||
await renderPagesProgressively(
|
await renderPagesProgressively(
|
||||||
pageState.pdfJsDoc,
|
pageState.pdfJsDoc,
|
||||||
pageThumbnails,
|
pageThumbnails,
|
||||||
createPageWrapper,
|
createPageWrapper,
|
||||||
{
|
{
|
||||||
batchSize: 8,
|
batchSize: 8,
|
||||||
useLazyLoading: true,
|
useLazyLoading: true,
|
||||||
lazyLoadMargin: '200px',
|
lazyLoadMargin: '200px',
|
||||||
eagerLoadBatches: 2,
|
eagerLoadBatches: 2,
|
||||||
onBatchComplete: function () {
|
onBatchComplete: function () {
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUI() {
|
async function updateUI() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
|
|
||||||
if (!fileDisplayArea) return;
|
if (!fileDisplayArea) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (pageState.file) {
|
if (pageState.file) {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = pageState.file.name;
|
nameSpan.textContent = pageState.file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
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.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.onclick = function () {
|
removeBtn.onclick = function () {
|
||||||
resetState();
|
resetState();
|
||||||
};
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showLoader('Loading PDF...');
|
const result = await loadPdfWithPasswordPrompt(pageState.file);
|
||||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
if (!result) {
|
||||||
|
resetState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showLoader('Loading PDF...');
|
||||||
|
|
||||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer.slice(0), {
|
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||||
ignoreEncryption: true,
|
throwOnInvalidObject: false,
|
||||||
throwOnInvalidObject: false
|
});
|
||||||
});
|
|
||||||
|
|
||||||
pageState.pdfJsDoc = await getPDFDocument({ data: arrayBuffer.slice(0) }).promise;
|
pageState.pdfJsDoc = result.pdf;
|
||||||
|
|
||||||
const pageCount = pageState.pdfDoc.getPageCount();
|
const pageCount = pageState.pdfDoc.getPageCount();
|
||||||
pageState.rotations = new Array(pageCount).fill(0);
|
pageState.rotations = new Array(pageCount).fill(0);
|
||||||
|
|
||||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||||
|
|
||||||
await renderThumbnails();
|
await renderThumbnails();
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading PDF:', error);
|
console.error('Error loading PDF:', error);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', 'Failed to load PDF file.');
|
showAlert('Error', 'Failed to load PDF file.');
|
||||||
resetState();
|
resetState();
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyRotations() {
|
async function applyRotations() {
|
||||||
if (!pageState.pdfDoc || !pageState.file) {
|
if (!pageState.pdfDoc || !pageState.file) {
|
||||||
showAlert('Error', 'Please upload a PDF first.');
|
showAlert('Error', 'Please upload a PDF first.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Applying rotations...');
|
showLoader('Applying rotations...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pageCount = pageState.pdfDoc.getPageCount();
|
const pageCount = pageState.pdfDoc.getPageCount();
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
|
|
||||||
for (let i = 0; i < pageCount; i++) {
|
for (let i = 0; i < pageCount; i++) {
|
||||||
const rotation = pageState.rotations[i] || 0;
|
const rotation = pageState.rotations[i] || 0;
|
||||||
const originalPage = pageState.pdfDoc.getPage(i);
|
const originalPage = pageState.pdfDoc.getPage(i);
|
||||||
const currentRotation = originalPage.getRotation().angle;
|
const currentRotation = originalPage.getRotation().angle;
|
||||||
const totalRotation = currentRotation + rotation;
|
const totalRotation = currentRotation + rotation;
|
||||||
|
|
||||||
console.log(`Page ${i}: rotation=${rotation}, currentRotation=${currentRotation}, totalRotation=${totalRotation}, applying=${-totalRotation}`);
|
console.log(
|
||||||
|
`Page ${i}: rotation=${rotation}, currentRotation=${currentRotation}, totalRotation=${totalRotation}, applying=${-totalRotation}`
|
||||||
|
);
|
||||||
|
|
||||||
if (totalRotation % 90 === 0) {
|
if (totalRotation % 90 === 0) {
|
||||||
const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
|
const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
|
||||||
copiedPage.setRotation(degrees(totalRotation));
|
copiedPage.setRotation(degrees(totalRotation));
|
||||||
newPdfDoc.addPage(copiedPage);
|
newPdfDoc.addPage(copiedPage);
|
||||||
} else {
|
} else {
|
||||||
const embeddedPage = await newPdfDoc.embedPage(originalPage);
|
const embeddedPage = await newPdfDoc.embedPage(originalPage);
|
||||||
const { width, height } = embeddedPage.scale(1);
|
const { width, height } = embeddedPage.scale(1);
|
||||||
|
|
||||||
const angleRad = (totalRotation * Math.PI) / 180;
|
const angleRad = (totalRotation * Math.PI) / 180;
|
||||||
const absCos = Math.abs(Math.cos(angleRad));
|
const absCos = Math.abs(Math.cos(angleRad));
|
||||||
const absSin = Math.abs(Math.sin(angleRad));
|
const absSin = Math.abs(Math.sin(angleRad));
|
||||||
|
|
||||||
const newWidth = width * absCos + height * absSin;
|
const newWidth = width * absCos + height * absSin;
|
||||||
const newHeight = width * absSin + height * absCos;
|
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 x =
|
||||||
const y = newHeight / 2 - (width / 2 * Math.sin(angleRad) + height / 2 * Math.cos(angleRad));
|
newWidth / 2 -
|
||||||
|
((width / 2) * Math.cos(angleRad) -
|
||||||
|
(height / 2) * Math.sin(angleRad));
|
||||||
|
const y =
|
||||||
|
newHeight / 2 -
|
||||||
|
((width / 2) * Math.sin(angleRad) +
|
||||||
|
(height / 2) * Math.cos(angleRad));
|
||||||
|
|
||||||
newPage.drawPage(embeddedPage, {
|
newPage.drawPage(embeddedPage, {
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
rotate: degrees(totalRotation),
|
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();
|
|
||||||
});
|
});
|
||||||
} 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) {
|
function handleFileSelect(files: FileList | null) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
if (
|
||||||
pageState.file = file;
|
file.type === 'application/pdf' ||
|
||||||
updateUI();
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
}
|
) {
|
||||||
|
pageState.file = file;
|
||||||
|
updateUI();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
const batchDecrement = document.getElementById('batch-decrement');
|
const batchDecrement = document.getElementById('batch-decrement');
|
||||||
const batchIncrement = document.getElementById('batch-increment');
|
const batchIncrement = document.getElementById('batch-increment');
|
||||||
const batchApply = document.getElementById('batch-apply');
|
const batchApply = document.getElementById('batch-apply');
|
||||||
const batchAngleInput = document.getElementById('batch-custom-angle') as HTMLInputElement;
|
const batchAngleInput = document.getElementById(
|
||||||
|
'batch-custom-angle'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', function () {
|
backBtn.addEventListener('click', function () {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
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) {
|
fileInput.addEventListener('click', function () {
|
||||||
batchDecrement.addEventListener('click', function () {
|
fileInput.value = '';
|
||||||
const current = parseInt(batchAngleInput.value) || 0;
|
});
|
||||||
batchAngleInput.value = (current - 1).toString();
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (batchIncrement && batchAngleInput) {
|
if (processBtn) {
|
||||||
batchIncrement.addEventListener('click', function () {
|
processBtn.addEventListener('click', applyRotations);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
import {
|
import {
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
cleanupLazyRendering,
|
cleanupLazyRendering,
|
||||||
} from '../utils/render-utils.js';
|
} from '../utils/render-utils.js';
|
||||||
import { rotatePdfPages } from '../utils/pdf-operations.js';
|
import { rotatePdfPages } from '../utils/pdf-operations.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
@@ -199,16 +200,18 @@ async function updateUI() {
|
|||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const result = await loadPdfWithPasswordPrompt(pageState.file);
|
||||||
|
if (!result) {
|
||||||
|
resetState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
showLoader('Loading PDF...');
|
showLoader('Loading PDF...');
|
||||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
|
||||||
|
|
||||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer.slice(0), {
|
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||||
ignoreEncryption: true,
|
|
||||||
throwOnInvalidObject: false,
|
throwOnInvalidObject: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
pageState.pdfJsDoc = await getPDFDocument({ data: arrayBuffer.slice(0) })
|
pageState.pdfJsDoc = result.pdf;
|
||||||
.promise;
|
|
||||||
|
|
||||||
const pageCount = pageState.pdfDoc.getPageCount();
|
const pageCount = pageState.pdfDoc.getPageCount();
|
||||||
pageState.rotations = new Array(pageCount).fill(0);
|
pageState.rotations = new Array(pageCount).fill(0);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
|
|||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
import { SanitizePdfState } from '@/types';
|
import { SanitizePdfState } from '@/types';
|
||||||
import { sanitizePdf } from '../utils/sanitize.js';
|
import { sanitizePdf } from '../utils/sanitize.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
const pageState: SanitizePdfState = {
|
const pageState: SanitizePdfState = {
|
||||||
file: null,
|
file: null,
|
||||||
@@ -132,8 +133,17 @@ async function runSanitize() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
if (loaderModal) loaderModal.classList.add('hidden');
|
||||||
const result = await sanitizePdf(new Uint8Array(arrayBuffer), options);
|
const loaded = await loadPdfWithPasswordPrompt(pageState.file);
|
||||||
|
if (!loaded) {
|
||||||
|
if (loaderModal) loaderModal.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||||
|
if (loaderText) loaderText.textContent = 'Sanitizing PDF...';
|
||||||
|
loaded.pdf.destroy();
|
||||||
|
pageState.file = loaded.file;
|
||||||
|
const result = await sanitizePdf(new Uint8Array(loaded.bytes), options);
|
||||||
|
|
||||||
downloadFile(
|
downloadFile(
|
||||||
new Blob([new Uint8Array(result.bytes)], { type: 'application/pdf' }),
|
new Blob([new Uint8Array(result.bytes)], { type: 'application/pdf' }),
|
||||||
@@ -147,9 +157,10 @@ async function runSanitize() {
|
|||||||
resetState();
|
resetState();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error('Sanitization Error:', e);
|
console.error('Sanitization Error:', e);
|
||||||
showAlert('Error', `An error occurred during sanitization: ${e.message}`);
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
showAlert('Error', `An error occurred during sanitization: ${msg}`);
|
||||||
} finally {
|
} finally {
|
||||||
if (loaderModal) loaderModal.classList.add('hidden');
|
if (loaderModal) loaderModal.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { applyScannerEffect } from '../utils/image-effects.js';
|
|||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import type { ScanSettings } from '../types/scanner-effect-type.js';
|
import type { ScanSettings } from '../types/scanner-effect-type.js';
|
||||||
import { t } from '../i18n/i18n';
|
import { t } from '../i18n/i18n';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -402,13 +403,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
files = [validFiles[0]];
|
|
||||||
updateUI();
|
|
||||||
|
|
||||||
showLoader('Loading preview...');
|
|
||||||
try {
|
try {
|
||||||
const buffer = await readFileAsArrayBuffer(validFiles[0]);
|
const result = await loadPdfWithPasswordPrompt(validFiles[0]);
|
||||||
pdfjsDoc = await getPDFDocument({ data: buffer }).promise;
|
if (!result) return;
|
||||||
|
showLoader('Loading preview...');
|
||||||
|
files = [result.file];
|
||||||
|
updateUI();
|
||||||
|
pdfjsDoc = result.pdf;
|
||||||
await renderPreview();
|
await renderPreview();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
@@ -1,313 +1,358 @@
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||||
import { readFileAsArrayBuffer, formatBytes, downloadFile, getPDFDocument } from '../utils/helpers.js';
|
import {
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
formatBytes,
|
||||||
|
downloadFile,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import { PDFDocument } from 'pdf-lib';
|
import { PDFDocument } from 'pdf-lib';
|
||||||
import { t } from '../i18n/i18n';
|
import { t } from '../i18n/i18n';
|
||||||
|
|
||||||
interface SignState {
|
interface SignState {
|
||||||
file: File | null;
|
file: File | null;
|
||||||
pdfDoc: any;
|
pdfDoc: any;
|
||||||
viewerIframe: HTMLIFrameElement | null;
|
viewerIframe: HTMLIFrameElement | null;
|
||||||
viewerReady: boolean;
|
viewerReady: boolean;
|
||||||
blobUrl: string | null;
|
blobUrl: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const signState: SignState = {
|
const signState: SignState = {
|
||||||
file: null,
|
file: null,
|
||||||
pdfDoc: null,
|
pdfDoc: null,
|
||||||
viewerIframe: null,
|
viewerIframe: null,
|
||||||
viewerReady: false,
|
viewerReady: false,
|
||||||
blobUrl: null,
|
blobUrl: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', initializePage);
|
document.addEventListener('DOMContentLoaded', initializePage);
|
||||||
} else {
|
} else {
|
||||||
initializePage();
|
initializePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializePage() {
|
function initializePage() {
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
|
|
||||||
if (fileInput) {
|
if (fileInput) {
|
||||||
fileInput.addEventListener('change', handleFileUpload);
|
fileInput.addEventListener('change', handleFileUpload);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dropZone) {
|
if (dropZone) {
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.add('bg-gray-700');
|
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;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) {
|
function handleFileUpload(e: Event) {
|
||||||
const input = e.target as HTMLInputElement;
|
const input = e.target as HTMLInputElement;
|
||||||
if (input.files && input.files.length > 0) {
|
if (input.files && input.files.length > 0) {
|
||||||
handleFile(input.files[0]);
|
handleFile(input.files[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFile(file: File) {
|
function handleFile(file: File) {
|
||||||
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
|
if (
|
||||||
showAlert('Invalid File', 'Please select a PDF file.');
|
file.type !== 'application/pdf' &&
|
||||||
return;
|
!file.name.toLowerCase().endsWith('.pdf')
|
||||||
}
|
) {
|
||||||
|
showAlert('Invalid File', 'Please select a PDF file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
signState.file = file;
|
signState.file = file;
|
||||||
updateFileDisplay();
|
updateFileDisplay();
|
||||||
setupSignTool();
|
setupSignTool();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateFileDisplay() {
|
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 = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
document.getElementById('signature-editor')?.classList.add('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
const fileDiv = document.createElement('div');
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
createIcons({ icons });
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const result = await loadPdfWithPasswordPrompt(signState.file);
|
||||||
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
if (!result) {
|
||||||
|
signState.file = null;
|
||||||
const nameSpan = document.createElement('div');
|
signState.pdfDoc = null;
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
fileDisplayArea.innerHTML = '';
|
||||||
nameSpan.textContent = signState.file.name;
|
document.getElementById('signature-editor')?.classList.add('hidden');
|
||||||
|
return;
|
||||||
const metaSpan = document.createElement('div');
|
}
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
signState.file = result.file;
|
||||||
metaSpan.textContent = `${formatBytes(signState.file.size)} • ${t('common.loadingPageCount')}`;
|
nameSpan.textContent = result.file.name;
|
||||||
|
metaSpan.textContent = `${formatBytes(result.file.size)} • ${result.pdf.numPages} pages`;
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
result.pdf.destroy();
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupSignTool() {
|
async function setupSignTool() {
|
||||||
const signatureEditor = document.getElementById('signature-editor');
|
const signatureEditor = document.getElementById('signature-editor');
|
||||||
if (signatureEditor) {
|
if (signatureEditor) {
|
||||||
signatureEditor.classList.remove('hidden');
|
signatureEditor.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Loading PDF viewer...');
|
showLoader('Loading PDF viewer...');
|
||||||
|
|
||||||
const container = document.getElementById('canvas-container-sign');
|
const container = document.getElementById('canvas-container-sign');
|
||||||
if (!container) {
|
if (!container) {
|
||||||
console.error('Sign tool canvas container not found');
|
console.error('Sign tool canvas container not found');
|
||||||
hideLoader();
|
hideLoader();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!signState.file) {
|
if (!signState.file) {
|
||||||
console.error('No file loaded for signing');
|
console.error('No file loaded for signing');
|
||||||
hideLoader();
|
hideLoader();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.textContent = '';
|
container.textContent = '';
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
iframe.style.width = '100%';
|
iframe.style.width = '100%';
|
||||||
iframe.style.height = '100%';
|
iframe.style.height = '100%';
|
||||||
iframe.style.border = 'none';
|
iframe.style.border = 'none';
|
||||||
container.appendChild(iframe);
|
container.appendChild(iframe);
|
||||||
signState.viewerIframe = iframe;
|
signState.viewerIframe = iframe;
|
||||||
|
|
||||||
const pdfBytes = await readFileAsArrayBuffer(signState.file);
|
const pdfBytes = await readFileAsArrayBuffer(signState.file);
|
||||||
const blob = new Blob([pdfBytes as BlobPart], { type: 'application/pdf' });
|
const blob = new Blob([pdfBytes as BlobPart], { type: 'application/pdf' });
|
||||||
signState.blobUrl = URL.createObjectURL(blob);
|
signState.blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const existingPrefsRaw = localStorage.getItem('pdfjs.preferences');
|
const existingPrefsRaw = localStorage.getItem('pdfjs.preferences');
|
||||||
const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {};
|
const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {};
|
||||||
delete (existingPrefs as any).annotationEditorMode;
|
delete (existingPrefs as any).annotationEditorMode;
|
||||||
const newPrefs = {
|
const newPrefs = {
|
||||||
...existingPrefs,
|
...existingPrefs,
|
||||||
enableSignatureEditor: true,
|
enableSignatureEditor: true,
|
||||||
enablePermissions: false,
|
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 = '';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
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() {
|
async function applyAndSaveSignatures() {
|
||||||
if (!signState.viewerReady || !signState.viewerIframe) {
|
if (!signState.viewerReady || !signState.viewerIframe) {
|
||||||
showAlert('Viewer not ready', 'Please wait for the PDF viewer to load.');
|
showAlert('Viewer not ready', 'Please wait for the PDF viewer to load.');
|
||||||
return;
|
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 app = viewerWindow.PDFViewerApplication;
|
||||||
const viewerWindow: any = signState.viewerIframe.contentWindow;
|
const flattenCheckbox = document.getElementById(
|
||||||
if (!viewerWindow || !viewerWindow.PDFViewerApplication) {
|
'flatten-signature-toggle'
|
||||||
showAlert('Viewer not ready', 'The PDF viewer is still initializing.');
|
) as HTMLInputElement | null;
|
||||||
return;
|
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() {
|
function resetState() {
|
||||||
cleanup();
|
cleanup();
|
||||||
signState.file = null;
|
signState.file = null;
|
||||||
signState.viewerIframe = null;
|
signState.viewerIframe = null;
|
||||||
signState.viewerReady = false;
|
signState.viewerReady = false;
|
||||||
|
|
||||||
const signatureEditor = document.getElementById('signature-editor');
|
const signatureEditor = document.getElementById('signature-editor');
|
||||||
if (signatureEditor) {
|
if (signatureEditor) {
|
||||||
signatureEditor.classList.add('hidden');
|
signatureEditor.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = document.getElementById('canvas-container-sign');
|
const container = document.getElementById('canvas-container-sign');
|
||||||
if (container) {
|
if (container) {
|
||||||
container.textContent = '';
|
container.textContent = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) {
|
if (fileDisplayArea) {
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const processBtn = document.getElementById('process-btn') as HTMLButtonElement | null;
|
const processBtn = document.getElementById(
|
||||||
if (processBtn) {
|
'process-btn'
|
||||||
processBtn.style.display = 'none';
|
) as HTMLButtonElement | null;
|
||||||
}
|
if (processBtn) {
|
||||||
|
processBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
const flattenCheckbox = document.getElementById('flatten-signature-toggle') as HTMLInputElement | null;
|
const flattenCheckbox = document.getElementById(
|
||||||
if (flattenCheckbox) {
|
'flatten-signature-toggle'
|
||||||
flattenCheckbox.checked = false;
|
) as HTMLInputElement | null;
|
||||||
}
|
if (flattenCheckbox) {
|
||||||
|
flattenCheckbox.checked = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanup() {
|
function cleanup() {
|
||||||
if (signState.blobUrl) {
|
if (signState.blobUrl) {
|
||||||
URL.revokeObjectURL(signState.blobUrl);
|
URL.revokeObjectURL(signState.blobUrl);
|
||||||
signState.blobUrl = null;
|
signState.blobUrl = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,8 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { t } from '../i18n/i18n';
|
import { t } from '../i18n/i18n';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import {
|
import { downloadFile, getPDFDocument, formatBytes } from '../utils/helpers.js';
|
||||||
downloadFile,
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
getPDFDocument,
|
|
||||||
readFileAsArrayBuffer,
|
|
||||||
formatBytes,
|
|
||||||
} from '../utils/helpers.js';
|
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import {
|
import {
|
||||||
renderPagesProgressively,
|
renderPagesProgressively,
|
||||||
@@ -94,12 +90,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Load PDF Document
|
// Load PDF Document
|
||||||
try {
|
try {
|
||||||
if (!state.pdfDoc) {
|
if (!state.pdfDoc) {
|
||||||
showLoader('Loading PDF...');
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
const arrayBuffer = (await readFileAsArrayBuffer(
|
if (!result) {
|
||||||
file
|
state.files = [];
|
||||||
)) as ArrayBuffer;
|
updateUI();
|
||||||
state.pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
return;
|
||||||
hideLoader();
|
}
|
||||||
|
result.pdf.destroy();
|
||||||
|
state.files[0] = result.file;
|
||||||
|
state.pdfDoc = await PDFLibDocument.load(result.bytes);
|
||||||
}
|
}
|
||||||
// Update page count
|
// Update page count
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • ${state.pdfDoc.getPageCount()} pages`;
|
metaSpan.textContent = `${formatBytes(file.size)} • ${state.pdfDoc.getPageCount()} pages`;
|
||||||
@@ -139,10 +138,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// If pdfDoc is not loaded yet (e.g. page refresh), try to load it from the first file
|
// If pdfDoc is not loaded yet (e.g. page refresh), try to load it from the first file
|
||||||
if (state.files.length > 0) {
|
if (state.files.length > 0) {
|
||||||
const file = state.files[0];
|
const file = state.files[0];
|
||||||
const arrayBuffer = (await readFileAsArrayBuffer(
|
hideLoader();
|
||||||
file
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
)) as ArrayBuffer;
|
if (!result) {
|
||||||
state.pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
showLoader('Rendering page previews...');
|
||||||
|
throw new Error('No PDF document loaded');
|
||||||
|
}
|
||||||
|
result.pdf.destroy();
|
||||||
|
state.files[0] = result.file;
|
||||||
|
state.pdfDoc = await PDFLibDocument.load(result.bytes);
|
||||||
|
showLoader('Rendering page previews...');
|
||||||
} else {
|
} else {
|
||||||
throw new Error('No PDF document loaded');
|
throw new Error('No PDF document loaded');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
showWasmRequiredDialog,
|
showWasmRequiredDialog,
|
||||||
WasmProvider,
|
WasmProvider,
|
||||||
} from '../utils/wasm-provider.js';
|
} from '../utils/wasm-provider.js';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
const worker = new Worker(
|
const worker = new Worker(
|
||||||
import.meta.env.BASE_URL + 'workers/table-of-contents.worker.js'
|
import.meta.env.BASE_URL + 'workers/table-of-contents.worker.js'
|
||||||
@@ -95,15 +96,18 @@ function renderFileDisplay(file: File) {
|
|||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileSelect(file: File) {
|
async function handleFileSelect(file: File) {
|
||||||
if (file.type !== 'application/pdf') {
|
if (file.type !== 'application/pdf') {
|
||||||
showStatus('Please select a PDF file.', 'error');
|
showStatus('Please select a PDF file.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pdfFile = file;
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
|
if (!result) return;
|
||||||
|
result.pdf.destroy();
|
||||||
|
pdfFile = result.file;
|
||||||
generateBtn.disabled = false;
|
generateBtn.disabled = false;
|
||||||
renderFileDisplay(file);
|
renderFileDisplay(pdfFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
|
|||||||
@@ -1,135 +1,199 @@
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||||
import { downloadFile, hexToRgb, formatBytes, getPDFDocument, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
hexToRgb,
|
||||||
|
formatBytes,
|
||||||
|
getPDFDocument,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { TextColorState } from '@/types';
|
import { TextColorState } from '@/types';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
|
import.meta.url
|
||||||
|
).toString();
|
||||||
|
|
||||||
const pageState: TextColorState = { file: null, pdfDoc: null };
|
const pageState: TextColorState = { file: null, pdfDoc: null };
|
||||||
|
|
||||||
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializePage); }
|
if (document.readyState === 'loading') {
|
||||||
else { initializePage(); }
|
document.addEventListener('DOMContentLoaded', initializePage);
|
||||||
|
} else {
|
||||||
function initializePage() {
|
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); }
|
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) {
|
async function handleFiles(files: FileList) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (!file || file.type !== 'application/pdf') { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; }
|
if (!file || file.type !== 'application/pdf') {
|
||||||
|
showAlert('Invalid File', 'Please upload a valid PDF file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await loadPdfWithPasswordPrompt(file);
|
||||||
|
if (!result) return;
|
||||||
showLoader('Loading PDF...');
|
showLoader('Loading PDF...');
|
||||||
try {
|
result.pdf.destroy();
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
pageState.pdfDoc = await PDFLibDocument.load(result.bytes);
|
||||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
pageState.file = result.file;
|
||||||
pageState.file = file;
|
updateFileDisplay();
|
||||||
updateFileDisplay();
|
document.getElementById('options-panel')?.classList.remove('hidden');
|
||||||
document.getElementById('options-panel')?.classList.remove('hidden');
|
} catch (error) {
|
||||||
} catch (error) { console.error(error); showAlert('Error', 'Failed to load PDF file.'); }
|
console.error(error);
|
||||||
finally { hideLoader(); }
|
showAlert('Error', 'Failed to load PDF file.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFileDisplay() {
|
function updateFileDisplay() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
|
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
fileDiv.className =
|
||||||
const infoContainer = document.createElement('div');
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
||||||
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
const infoContainer = document.createElement('div');
|
||||||
const nameSpan = document.createElement('div');
|
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.textContent = pageState.file.name;
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
const metaSpan = document.createElement('div');
|
nameSpan.textContent = pageState.file.name;
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`;
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`;
|
||||||
const removeBtn = document.createElement('button');
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.onclick = resetState;
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
removeBtn.onclick = resetState;
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
createIcons({ icons });
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
createIcons({ icons });
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
pageState.file = null; pageState.pdfDoc = null;
|
pageState.file = null;
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
pageState.pdfDoc = null;
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
document.getElementById('options-panel')?.classList.add('hidden');
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
document.getElementById('options-panel')?.classList.add('hidden');
|
||||||
if (fileInput) fileInput.value = '';
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
|
if (fileInput) fileInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeTextColor() {
|
async function changeTextColor() {
|
||||||
if (!pageState.pdfDoc || !pageState.file) { showAlert('Error', 'Please upload a PDF file first.'); return; }
|
if (!pageState.pdfDoc || !pageState.file) {
|
||||||
const colorHex = (document.getElementById('text-color-input') as HTMLInputElement).value;
|
showAlert('Error', 'Please upload a PDF file first.');
|
||||||
const { r, g, b } = hexToRgb(colorHex);
|
return;
|
||||||
const darknessThreshold = 120;
|
}
|
||||||
showLoader('Changing text color...');
|
const colorHex = (
|
||||||
try {
|
document.getElementById('text-color-input') as HTMLInputElement
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
).value;
|
||||||
const pdf = await getPDFDocument(await readFileAsArrayBuffer(pageState.file)).promise;
|
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++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
||||||
const page = await pdf.getPage(i);
|
const page = await pdf.getPage(i);
|
||||||
const viewport = page.getViewport({ scale: 2.0 });
|
const viewport = page.getViewport({ scale: 2.0 });
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
const context = canvas.getContext('2d')!;
|
const context = canvas.getContext('2d')!;
|
||||||
await page.render({ canvasContext: context, viewport, canvas }).promise;
|
await page.render({ canvasContext: context, viewport, canvas }).promise;
|
||||||
|
|
||||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
const data = imageData.data;
|
const data = imageData.data;
|
||||||
for (let j = 0; j < data.length; j += 4) {
|
for (let j = 0; j < data.length; j += 4) {
|
||||||
if (data[j] < darknessThreshold && data[j + 1] < darknessThreshold && data[j + 2] < darknessThreshold) {
|
if (
|
||||||
data[j] = r * 255;
|
data[j] < darknessThreshold &&
|
||||||
data[j + 1] = g * 255;
|
data[j + 1] < darknessThreshold &&
|
||||||
data[j + 2] = b * 255;
|
data[j + 2] < darknessThreshold
|
||||||
}
|
) {
|
||||||
}
|
data[j] = r * 255;
|
||||||
context.putImageData(imageData, 0, 0);
|
data[j + 1] = g * 255;
|
||||||
|
data[j + 2] = b * 255;
|
||||||
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');
|
context.putImageData(imageData, 0, 0);
|
||||||
showAlert('Success', 'Text color changed successfully!', 'success', () => { resetState(); });
|
|
||||||
} catch (e) { console.error(e); showAlert('Error', 'Could not change text color.'); }
|
const pngImageBytes = await new Promise<Uint8Array>((resolve) =>
|
||||||
finally { hideLoader(); }
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,360 +1,404 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { formatBytes, formatIsoDate, getPDFDocument } from '../utils/helpers.js';
|
import { formatBytes, formatIsoDate } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { ViewMetadataState } from '@/types';
|
import { ViewMetadataState } from '@/types';
|
||||||
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
|
|
||||||
const pageState: ViewMetadataState = {
|
const pageState: ViewMetadataState = {
|
||||||
file: null,
|
file: null,
|
||||||
metadata: {},
|
metadata: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
pageState.file = null;
|
pageState.file = null;
|
||||||
pageState.metadata = {};
|
pageState.metadata = {};
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
|
||||||
const metadataDisplay = document.getElementById('metadata-display');
|
const metadataDisplay = document.getElementById('metadata-display');
|
||||||
if (metadataDisplay) metadataDisplay.innerHTML = '';
|
if (metadataDisplay) metadataDisplay.innerHTML = '';
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSection(title: string): { wrapper: HTMLDivElement; ul: HTMLUListElement } {
|
function createSection(title: string): {
|
||||||
const wrapper = document.createElement('div');
|
wrapper: HTMLDivElement;
|
||||||
wrapper.className = 'mb-6';
|
ul: HTMLUListElement;
|
||||||
const h3 = document.createElement('h3');
|
} {
|
||||||
h3.className = 'text-lg font-semibold text-white mb-2';
|
const wrapper = document.createElement('div');
|
||||||
h3.textContent = title;
|
wrapper.className = 'mb-6';
|
||||||
const ul = document.createElement('ul');
|
const h3 = document.createElement('h3');
|
||||||
ul.className = 'space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700';
|
h3.className = 'text-lg font-semibold text-white mb-2';
|
||||||
wrapper.append(h3, ul);
|
h3.textContent = title;
|
||||||
return { wrapper, ul };
|
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 {
|
function createListItem(key: string, value: string): HTMLLIElement {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = 'flex flex-col sm:flex-row';
|
li.className = 'flex flex-col sm:flex-row';
|
||||||
const strong = document.createElement('strong');
|
const strong = document.createElement('strong');
|
||||||
strong.className = 'w-40 flex-shrink-0 text-gray-400';
|
strong.className = 'w-40 flex-shrink-0 text-gray-400';
|
||||||
strong.textContent = key;
|
strong.textContent = key;
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'flex-grow text-white break-all';
|
div.className = 'flex-grow text-white break-all';
|
||||||
div.textContent = value;
|
div.textContent = value;
|
||||||
li.append(strong, div);
|
li.append(strong, div);
|
||||||
return li;
|
return li;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePdfDate(pdfDate: string | unknown): string {
|
function parsePdfDate(pdfDate: string | unknown): string {
|
||||||
if (!pdfDate || typeof pdfDate !== 'string' || !pdfDate.startsWith('D:')) {
|
if (!pdfDate || typeof pdfDate !== 'string' || !pdfDate.startsWith('D:')) {
|
||||||
return String(pdfDate || '');
|
return String(pdfDate || '');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const year = pdfDate.substring(2, 6);
|
const year = pdfDate.substring(2, 6);
|
||||||
const month = pdfDate.substring(6, 8);
|
const month = pdfDate.substring(6, 8);
|
||||||
const day = pdfDate.substring(8, 10);
|
const day = pdfDate.substring(8, 10);
|
||||||
const hour = pdfDate.substring(10, 12);
|
const hour = pdfDate.substring(10, 12);
|
||||||
const minute = pdfDate.substring(12, 14);
|
const minute = pdfDate.substring(12, 14);
|
||||||
const second = pdfDate.substring(14, 16);
|
const second = pdfDate.substring(14, 16);
|
||||||
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`).toLocaleString();
|
return new Date(
|
||||||
} catch {
|
`${year}-${month}-${day}T${hour}:${minute}:${second}Z`
|
||||||
return pdfDate;
|
).toLocaleString();
|
||||||
}
|
} catch {
|
||||||
|
return pdfDate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createXmpListItem(key: string, value: string, indent: number = 0): HTMLLIElement {
|
function createXmpListItem(
|
||||||
const li = document.createElement('li');
|
key: string,
|
||||||
li.className = 'flex flex-col sm:flex-row';
|
value: string,
|
||||||
|
indent: number = 0
|
||||||
|
): HTMLLIElement {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'flex flex-col sm:flex-row';
|
||||||
|
|
||||||
const strong = document.createElement('strong');
|
const strong = document.createElement('strong');
|
||||||
strong.className = 'w-56 flex-shrink-0 text-gray-400';
|
strong.className = 'w-56 flex-shrink-0 text-gray-400';
|
||||||
strong.textContent = key;
|
strong.textContent = key;
|
||||||
strong.style.paddingLeft = `${indent * 1.2}rem`;
|
strong.style.paddingLeft = `${indent * 1.2}rem`;
|
||||||
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'flex-grow text-white break-all';
|
div.className = 'flex-grow text-white break-all';
|
||||||
div.textContent = value;
|
div.textContent = value;
|
||||||
|
|
||||||
li.append(strong, div);
|
li.append(strong, div);
|
||||||
return li;
|
return li;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createXmpHeaderItem(key: string, indent: number = 0): HTMLLIElement {
|
function createXmpHeaderItem(key: string, indent: number = 0): HTMLLIElement {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = 'flex pt-2';
|
li.className = 'flex pt-2';
|
||||||
const strong = document.createElement('strong');
|
const strong = document.createElement('strong');
|
||||||
strong.className = 'w-full flex-shrink-0 text-gray-300 font-medium';
|
strong.className = 'w-full flex-shrink-0 text-gray-300 font-medium';
|
||||||
strong.textContent = key;
|
strong.textContent = key;
|
||||||
strong.style.paddingLeft = `${indent * 1.2}rem`;
|
strong.style.paddingLeft = `${indent * 1.2}rem`;
|
||||||
li.append(strong);
|
li.append(strong);
|
||||||
return li;
|
return li;
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendXmpNodes(xmlNode: Element, ulElement: HTMLUListElement, indentLevel: number) {
|
function appendXmpNodes(
|
||||||
const xmpDateKeys = ['xap:CreateDate', 'xap:ModifyDate', 'xap:MetadataDate'];
|
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) {
|
for (const child of childNodes) {
|
||||||
if (child.nodeType !== 1) continue;
|
if (child.nodeType !== 1) continue;
|
||||||
|
|
||||||
let key = child.tagName;
|
let key = child.tagName;
|
||||||
const elementChildren = Array.from(child.children).filter(function (c) {
|
const elementChildren = Array.from(child.children).filter(function (c) {
|
||||||
return c.nodeType === 1;
|
return c.nodeType === 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (key === 'rdf:li') {
|
if (key === 'rdf:li') {
|
||||||
appendXmpNodes(child, ulElement, indentLevel);
|
appendXmpNodes(child, ulElement, indentLevel);
|
||||||
continue;
|
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: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() {
|
async function displayMetadata() {
|
||||||
const metadataDisplay = document.getElementById('metadata-display');
|
const metadataDisplay = document.getElementById('metadata-display');
|
||||||
if (!metadataDisplay || !pageState.file) return;
|
if (!metadataDisplay || !pageState.file) return;
|
||||||
|
|
||||||
metadataDisplay.innerHTML = '';
|
metadataDisplay.innerHTML = '';
|
||||||
pageState.metadata = {};
|
pageState.metadata = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await loadPdfWithPasswordPrompt(pageState.file);
|
||||||
|
if (!result) return;
|
||||||
showLoader('Analyzing full PDF metadata...');
|
showLoader('Analyzing full PDF metadata...');
|
||||||
|
const { pdf: pdfjsDoc, file: currentFile } = result;
|
||||||
|
pageState.file = currentFile;
|
||||||
|
|
||||||
try {
|
const [metadataResult, fieldObjects] = await Promise.all([
|
||||||
const pdfBytes = await pageState.file.arrayBuffer();
|
pdfjsDoc.getMetadata(),
|
||||||
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
|
pdfjsDoc.getFieldObjects(),
|
||||||
|
]);
|
||||||
|
|
||||||
const [metadataResult, fieldObjects] = await Promise.all([
|
const { info, metadata } = metadataResult;
|
||||||
pdfjsDoc.getMetadata(),
|
const rawXmpString = metadata ? metadata.getRaw() : null;
|
||||||
pdfjsDoc.getFieldObjects(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { info, metadata } = metadataResult;
|
// Info Dictionary Section
|
||||||
const rawXmpString = metadata ? metadata.getRaw() : null;
|
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
|
if (value === null || typeof value === 'undefined') {
|
||||||
const infoSection = createSection('Info Dictionary');
|
displayValue = '- Not Set -';
|
||||||
if (info && Object.keys(info).length > 0) {
|
} else if (
|
||||||
for (const key in info) {
|
typeof value === 'object' &&
|
||||||
const value = (info as Record<string, unknown>)[key];
|
value !== null &&
|
||||||
let displayValue: string;
|
'name' in value
|
||||||
|
) {
|
||||||
if (value === null || typeof value === 'undefined') {
|
displayValue = String((value as { name: string }).name);
|
||||||
displayValue = '- Not Set -';
|
} else if (typeof value === 'object') {
|
||||||
} else if (typeof value === 'object' && value !== null && 'name' in value) {
|
try {
|
||||||
displayValue = String((value as { name: string }).name);
|
displayValue = JSON.stringify(value);
|
||||||
} else if (typeof value === 'object') {
|
} catch {
|
||||||
try {
|
displayValue = '[object Object]';
|
||||||
displayValue = JSON.stringify(value);
|
}
|
||||||
} catch {
|
} else if (
|
||||||
displayValue = '[object Object]';
|
(key === 'CreationDate' || key === 'ModDate') &&
|
||||||
}
|
typeof value === 'string'
|
||||||
} else if ((key === 'CreationDate' || key === 'ModDate') && typeof value === 'string') {
|
) {
|
||||||
displayValue = parsePdfDate(value);
|
displayValue = parsePdfDate(value);
|
||||||
} else {
|
|
||||||
displayValue = String(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
pageState.metadata[key] = displayValue;
|
|
||||||
infoSection.ul.appendChild(createListItem(key, displayValue));
|
|
||||||
}
|
|
||||||
} else {
|
} 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
|
pageState.metadata[key] = displayValue;
|
||||||
const fieldsSection = createSection('Interactive Form Fields');
|
infoSection.ul.appendChild(createListItem(key, displayValue));
|
||||||
if (fieldObjects && Object.keys(fieldObjects).length > 0) {
|
}
|
||||||
for (const fieldName in fieldObjects) {
|
} else {
|
||||||
const field = (fieldObjects as Record<string, Array<{ fieldValue?: unknown }>>)[fieldName][0];
|
infoSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
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() {
|
async function updateUI() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
|
|
||||||
if (!fileDisplayArea) return;
|
if (!fileDisplayArea) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (pageState.file) {
|
if (pageState.file) {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = pageState.file.name;
|
nameSpan.textContent = pageState.file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = `${formatBytes(pageState.file.size)}`;
|
metaSpan.textContent = `${formatBytes(pageState.file.size)}`;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
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.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.onclick = function () {
|
removeBtn.onclick = function () {
|
||||||
resetState();
|
resetState();
|
||||||
};
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
await displayMetadata();
|
await displayMetadata();
|
||||||
|
|
||||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||||
} else {
|
} else {
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyMetadataAsJson() {
|
function copyMetadataAsJson() {
|
||||||
const jsonString = JSON.stringify(pageState.metadata, null, 2);
|
const jsonString = JSON.stringify(pageState.metadata, null, 2);
|
||||||
navigator.clipboard.writeText(jsonString).then(function () {
|
navigator.clipboard
|
||||||
showAlert('Copied', 'Metadata copied to clipboard as JSON.');
|
.writeText(jsonString)
|
||||||
}).catch(function (err) {
|
.then(function () {
|
||||||
console.error('Failed to copy:', err);
|
showAlert('Copied', 'Metadata copied to clipboard as JSON.');
|
||||||
showAlert('Error', 'Failed to copy metadata to clipboard.');
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
console.error('Failed to copy:', err);
|
||||||
|
showAlert('Error', 'Failed to copy metadata to clipboard.');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileSelect(files: FileList | null) {
|
function handleFileSelect(files: FileList | null) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
if (
|
||||||
pageState.file = file;
|
file.type === 'application/pdf' ||
|
||||||
updateUI();
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
}
|
) {
|
||||||
|
pageState.file = file;
|
||||||
|
updateUI();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const copyBtn = document.getElementById('copy-metadata');
|
const copyBtn = document.getElementById('copy-metadata');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', function () {
|
backBtn.addEventListener('click', function () {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
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('click', function () {
|
||||||
fileInput.addEventListener('change', function (e) {
|
fileInput.value = '';
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', function (e) {
|
if (copyBtn) {
|
||||||
e.preventDefault();
|
copyBtn.addEventListener('click', copyMetadataAsJson);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,3 +54,4 @@ export * from './page-preview-type.ts';
|
|||||||
export * from './add-page-labels-type.ts';
|
export * from './add-page-labels-type.ts';
|
||||||
export * from './pdf-to-tiff-type.ts';
|
export * from './pdf-to-tiff-type.ts';
|
||||||
export * from './pdf-to-cbz-type.ts';
|
export * from './pdf-to-cbz-type.ts';
|
||||||
|
export * from './password-prompt-type.ts';
|
||||||
|
|||||||
7
src/js/types/password-prompt-type.ts
Normal file
7
src/js/types/password-prompt-type.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
||||||
|
|
||||||
|
export interface LoadedPdf {
|
||||||
|
pdf: PDFDocumentProxy;
|
||||||
|
bytes: ArrayBuffer;
|
||||||
|
file: File;
|
||||||
|
}
|
||||||
847
src/js/utils/password-prompt.ts
Normal file
847
src/js/utils/password-prompt.ts
Normal 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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
1272
src/tests/password-prompt.test.ts
Normal file
1272
src/tests/password-prompt.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user