- Refactor PDF loading across workflow nodes to use loadPdfDocument utility
- Replaced direct calls to PDFDocument.load with loadPdfDocument in multiple nodes to standardize PDF loading process.
This commit is contained in:
@@ -2,13 +2,13 @@ import { AddAttachmentState } from '@/types';
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import { isCpdfAvailable } from '../utils/cpdf-helper.js';
|
||||
import {
|
||||
showWasmRequiredDialog,
|
||||
WasmProvider,
|
||||
} from '../utils/wasm-provider.js';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
const worker = new Worker(
|
||||
import.meta.env.BASE_URL + 'workers/add-attachments.worker.js'
|
||||
@@ -139,9 +139,7 @@ async function updateUI() {
|
||||
pageState.file = result.file;
|
||||
showLoader('Loading PDF...');
|
||||
|
||||
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
pageState.pdfDoc = await loadPdfDocument(result.bytes);
|
||||
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { AddBlankPageState } from '@/types';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
const pageState: AddBlankPageState = {
|
||||
file: null,
|
||||
@@ -84,8 +85,7 @@ async function updateUI() {
|
||||
}
|
||||
showLoader('Loading PDF...');
|
||||
pageState.file = result.file;
|
||||
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
pageState.pdfDoc = await loadPdfDocument(result.bytes, {
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
result.pdf.destroy();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import type {
|
||||
AddPageLabelsState,
|
||||
LabelRule,
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
normalizePageLabelStartValue,
|
||||
resolvePageLabelStyle,
|
||||
} from '../utils/page-labels.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
type AddPageLabelsCpdf = {
|
||||
setSlow?: () => void;
|
||||
@@ -174,8 +174,7 @@ async function handleFiles(files: FileList) {
|
||||
showLoader(translate('tools:addPageLabels.loadingPdf', 'Loading PDF...'));
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
const pdfDoc = await loadPdfDocument(arrayBuffer as ArrayBuffer, {
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -175,7 +175,9 @@ async function loadPdfInViewer(file: File) {
|
||||
enablePermissions: false,
|
||||
};
|
||||
localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs));
|
||||
} catch {}
|
||||
} catch (e) {
|
||||
console.warn('Failed to update pdfjs.preferences in localStorage', e);
|
||||
}
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.className = 'w-full h-full border-0';
|
||||
@@ -221,7 +223,12 @@ function setupAnnotationViewer(iframe: HTMLIFrameElement) {
|
||||
'editorStampButton'
|
||||
) as HTMLButtonElement | null;
|
||||
stampBtn?.click();
|
||||
} catch {}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'Failed to auto-click stamp button in annotation editor',
|
||||
e
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { AddWatermarkState, PageWatermarkConfig } from '@/types';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
@@ -121,7 +122,7 @@ async function handleFiles(files: FileList) {
|
||||
if (!result) return;
|
||||
showLoader('Loading PDF...');
|
||||
const pdfBytes = new Uint8Array(result.bytes);
|
||||
pageState.pdfDoc = await PDFLibDocument.load(pdfBytes);
|
||||
pageState.pdfDoc = await loadPdfDocument(pdfBytes);
|
||||
pageState.file = result.file;
|
||||
pageState.pdfBytes = pdfBytes;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js';
|
||||
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
||||
import { BackgroundColorState } from '@/types';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
const pageState: BackgroundColorState = { file: null, pdfDoc: null };
|
||||
|
||||
@@ -63,9 +64,7 @@ async function handleFiles(files: FileList) {
|
||||
if (!result) return;
|
||||
showLoader('Loading PDF...');
|
||||
result.pdf.destroy();
|
||||
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
pageState.pdfDoc = await loadPdfDocument(result.bytes);
|
||||
pageState.file = result.file;
|
||||
updateFileDisplay();
|
||||
document.getElementById('options-panel')?.classList.remove('hidden');
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js';
|
||||
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
|
||||
import { StandardFonts, rgb } from 'pdf-lib';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import JSZip from 'jszip';
|
||||
import Sortable from 'sortablejs';
|
||||
import { FileEntry, Position, StylePreset } from '@/types';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
const FONT_MAP: Record<string, keyof typeof StandardFonts> = {
|
||||
Helvetica: 'Helvetica',
|
||||
@@ -184,9 +185,7 @@ async function handleFiles(fileList: FileList) {
|
||||
if (!result) continue;
|
||||
showLoader('Loading PDFs...');
|
||||
result.pdf.destroy();
|
||||
const pdfDoc = await PDFDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
const pdfDoc = await loadPdfDocument(result.bytes);
|
||||
files.push({ file: result.file, pageCount: pdfDoc.getPageCount() });
|
||||
}
|
||||
|
||||
@@ -475,7 +474,7 @@ async function applyBatesNumbers() {
|
||||
|
||||
for (const entry of files) {
|
||||
const arrayBuffer = await entry.file.arrayBuffer();
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer);
|
||||
const pdfDoc = await loadPdfDocument(arrayBuffer);
|
||||
const font = await pdfDoc.embedFont(StandardFonts[fontName]);
|
||||
const pages = pdfDoc.getPages();
|
||||
const fileName = entry.file.name.replace(/\.pdf$/i, '');
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
hexToRgb,
|
||||
} from '../utils/helpers.js';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
import {
|
||||
BookmarkNode,
|
||||
BookmarkTree,
|
||||
@@ -1240,7 +1241,7 @@ async function loadPDF(e?: Event): Promise<void> {
|
||||
selectedBookmarks.clear();
|
||||
collapsedNodes.clear();
|
||||
|
||||
pdfLibDoc = await PDFDocument.load(result.bytes, { ignoreEncryption: true });
|
||||
pdfLibDoc = await loadPdfDocument(result.bytes, { ignoreEncryption: true });
|
||||
pdfJsDoc = result.pdf;
|
||||
|
||||
if (gotoPageInput) gotoPageInput.max = String(pdfJsDoc.numPages);
|
||||
|
||||
@@ -1,283 +1,330 @@
|
||||
import { showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, initializeQpdf, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import {
|
||||
downloadFile,
|
||||
formatBytes,
|
||||
initializeQpdf,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import { ChangePermissionsState } from '@/types';
|
||||
|
||||
const pageState: ChangePermissionsState = {
|
||||
file: null,
|
||||
file: null,
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
pageState.file = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
const currentPassword = document.getElementById('current-password') as HTMLInputElement;
|
||||
if (currentPassword) currentPassword.value = '';
|
||||
const currentPassword = document.getElementById(
|
||||
'current-password'
|
||||
) as HTMLInputElement;
|
||||
if (currentPassword) currentPassword.value = '';
|
||||
|
||||
const newUserPassword = document.getElementById('new-user-password') as HTMLInputElement;
|
||||
if (newUserPassword) newUserPassword.value = '';
|
||||
const newUserPassword = document.getElementById(
|
||||
'new-user-password'
|
||||
) as HTMLInputElement;
|
||||
if (newUserPassword) newUserPassword.value = '';
|
||||
|
||||
const newOwnerPassword = document.getElementById('new-owner-password') as HTMLInputElement;
|
||||
if (newOwnerPassword) newOwnerPassword.value = '';
|
||||
const newOwnerPassword = document.getElementById(
|
||||
'new-owner-password'
|
||||
) as HTMLInputElement;
|
||||
if (newOwnerPassword) newOwnerPassword.value = '';
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className =
|
||||
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (
|
||||
file.type === 'application/pdf' ||
|
||||
file.name.toLowerCase().endsWith('.pdf')
|
||||
) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function changePermissions() {
|
||||
if (!pageState.file) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
if (!pageState.file) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPassword =
|
||||
(document.getElementById('current-password') as HTMLInputElement)?.value ||
|
||||
'';
|
||||
const newUserPassword =
|
||||
(document.getElementById('new-user-password') as HTMLInputElement)?.value ||
|
||||
'';
|
||||
const newOwnerPassword =
|
||||
(document.getElementById('new-owner-password') as HTMLInputElement)
|
||||
?.value || '';
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
|
||||
try {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Initializing...';
|
||||
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Reading PDF...';
|
||||
const fileBuffer = await readFileAsArrayBuffer(pageState.file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Processing PDF permissions...';
|
||||
|
||||
const args = [inputPath];
|
||||
|
||||
if (currentPassword) {
|
||||
args.push('--password=' + currentPassword);
|
||||
}
|
||||
|
||||
const currentPassword = (document.getElementById('current-password') as HTMLInputElement)?.value || '';
|
||||
const newUserPassword = (document.getElementById('new-user-password') as HTMLInputElement)?.value || '';
|
||||
const newOwnerPassword = (document.getElementById('new-owner-password') as HTMLInputElement)?.value || '';
|
||||
const shouldEncrypt = newUserPassword || newOwnerPassword;
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
if (shouldEncrypt) {
|
||||
const finalUserPassword = newUserPassword;
|
||||
const finalOwnerPassword = newOwnerPassword;
|
||||
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
args.push('--encrypt', finalUserPassword, finalOwnerPassword, '256');
|
||||
|
||||
const allowPrinting = (
|
||||
document.getElementById('allow-printing') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowCopying = (
|
||||
document.getElementById('allow-copying') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowModifying = (
|
||||
document.getElementById('allow-modifying') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowAnnotating = (
|
||||
document.getElementById('allow-annotating') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowFillingForms = (
|
||||
document.getElementById('allow-filling-forms') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowDocumentAssembly = (
|
||||
document.getElementById('allow-document-assembly') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowPageExtraction = (
|
||||
document.getElementById('allow-page-extraction') as HTMLInputElement
|
||||
)?.checked;
|
||||
|
||||
if (finalOwnerPassword) {
|
||||
if (!allowModifying) args.push('--modify=none');
|
||||
if (!allowCopying) args.push('--extract=n');
|
||||
if (!allowPrinting) args.push('--print=none');
|
||||
if (!allowAnnotating) args.push('--annotate=n');
|
||||
if (!allowDocumentAssembly) args.push('--assemble=n');
|
||||
if (!allowFillingForms) args.push('--form=n');
|
||||
if (!allowPageExtraction) args.push('--extract=n');
|
||||
if (!allowModifying) args.push('--modify-other=n');
|
||||
} else if (finalUserPassword) {
|
||||
args.push('--allow-insecure');
|
||||
}
|
||||
} else {
|
||||
args.push('--decrypt');
|
||||
}
|
||||
|
||||
args.push('--', outputPath);
|
||||
try {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Initializing...';
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
|
||||
qpdf = await initializeQpdf();
|
||||
const errorMsg = qpdfError.message || '';
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Reading PDF...';
|
||||
const fileBuffer = await readFileAsArrayBuffer(pageState.file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
if (
|
||||
errorMsg.includes('invalid password') ||
|
||||
errorMsg.includes('incorrect password') ||
|
||||
errorMsg.includes('password')
|
||||
) {
|
||||
throw new Error('INVALID_PASSWORD', { cause: qpdfError });
|
||||
}
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Processing PDF permissions...';
|
||||
if (
|
||||
errorMsg.includes('encrypted') ||
|
||||
errorMsg.includes('password required')
|
||||
) {
|
||||
throw new Error('PASSWORD_REQUIRED', { cause: qpdfError });
|
||||
}
|
||||
|
||||
const args = [inputPath];
|
||||
|
||||
if (currentPassword) {
|
||||
args.push('--password=' + currentPassword);
|
||||
}
|
||||
|
||||
const shouldEncrypt = newUserPassword || newOwnerPassword;
|
||||
|
||||
if (shouldEncrypt) {
|
||||
const finalUserPassword = newUserPassword;
|
||||
const finalOwnerPassword = newOwnerPassword;
|
||||
|
||||
args.push('--encrypt', finalUserPassword, finalOwnerPassword, '256');
|
||||
|
||||
const allowPrinting = (document.getElementById('allow-printing') as HTMLInputElement)?.checked;
|
||||
const allowCopying = (document.getElementById('allow-copying') as HTMLInputElement)?.checked;
|
||||
const allowModifying = (document.getElementById('allow-modifying') as HTMLInputElement)?.checked;
|
||||
const allowAnnotating = (document.getElementById('allow-annotating') as HTMLInputElement)?.checked;
|
||||
const allowFillingForms = (document.getElementById('allow-filling-forms') as HTMLInputElement)?.checked;
|
||||
const allowDocumentAssembly = (document.getElementById('allow-document-assembly') as HTMLInputElement)?.checked;
|
||||
const allowPageExtraction = (document.getElementById('allow-page-extraction') as HTMLInputElement)?.checked;
|
||||
|
||||
if (finalOwnerPassword) {
|
||||
if (!allowModifying) args.push('--modify=none');
|
||||
if (!allowCopying) args.push('--extract=n');
|
||||
if (!allowPrinting) args.push('--print=none');
|
||||
if (!allowAnnotating) args.push('--annotate=n');
|
||||
if (!allowDocumentAssembly) args.push('--assemble=n');
|
||||
if (!allowFillingForms) args.push('--form=n');
|
||||
if (!allowPageExtraction) args.push('--extract=n');
|
||||
if (!allowModifying) args.push('--modify-other=n');
|
||||
} else if (finalUserPassword) {
|
||||
args.push('--allow-insecure');
|
||||
}
|
||||
} else {
|
||||
args.push('--decrypt');
|
||||
}
|
||||
|
||||
args.push('--', outputPath);
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
|
||||
const errorMsg = qpdfError.message || '';
|
||||
|
||||
if (
|
||||
errorMsg.includes('invalid password') ||
|
||||
errorMsg.includes('incorrect password') ||
|
||||
errorMsg.includes('password')
|
||||
) {
|
||||
throw new Error('INVALID_PASSWORD');
|
||||
}
|
||||
|
||||
if (
|
||||
errorMsg.includes('encrypted') ||
|
||||
errorMsg.includes('password required')
|
||||
) {
|
||||
throw new Error('PASSWORD_REQUIRED');
|
||||
}
|
||||
|
||||
throw new Error('Processing failed: ' + errorMsg || 'Unknown error');
|
||||
}
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Preparing download...';
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
throw new Error('Processing resulted in an empty file.');
|
||||
}
|
||||
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `permissions-changed-${pageState.file.name}`);
|
||||
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
|
||||
let successMessage = 'PDF permissions changed successfully!';
|
||||
if (!shouldEncrypt) {
|
||||
successMessage = 'PDF decrypted successfully! All encryption and restrictions removed.';
|
||||
}
|
||||
|
||||
showAlert('Success', successMessage, 'success', () => { resetState(); });
|
||||
} catch (error: any) {
|
||||
console.error('Error during PDF permission change:', error);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
|
||||
if (error.message === 'INVALID_PASSWORD') {
|
||||
showAlert(
|
||||
'Incorrect Password',
|
||||
'The current password you entered is incorrect. Please try again.'
|
||||
);
|
||||
} else if (error.message === 'PASSWORD_REQUIRED') {
|
||||
showAlert(
|
||||
'Password Required',
|
||||
'This PDF is password-protected. Please enter the current password to proceed.'
|
||||
);
|
||||
} else {
|
||||
showAlert(
|
||||
'Processing Failed',
|
||||
`An error occurred: ${error.message || 'The PDF might be corrupted or password protected.'}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) { }
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) { }
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
throw new Error('Processing failed: ' + errorMsg || 'Unknown error', {
|
||||
cause: qpdfError,
|
||||
});
|
||||
}
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Preparing download...';
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
throw new Error('Processing resulted in an empty file.');
|
||||
}
|
||||
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `permissions-changed-${pageState.file.name}`);
|
||||
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
|
||||
let successMessage = 'PDF permissions changed successfully!';
|
||||
if (!shouldEncrypt) {
|
||||
successMessage =
|
||||
'PDF decrypted successfully! All encryption and restrictions removed.';
|
||||
}
|
||||
|
||||
showAlert('Success', successMessage, 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error during PDF permission change:', error);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
|
||||
if (error.message === 'INVALID_PASSWORD') {
|
||||
showAlert(
|
||||
'Incorrect Password',
|
||||
'The current password you entered is incorrect. Please try again.'
|
||||
);
|
||||
} else if (error.message === 'PASSWORD_REQUIRED') {
|
||||
showAlert(
|
||||
'Password Required',
|
||||
'This PDF is password-protected. Please enter the current password to proceed.'
|
||||
);
|
||||
} else {
|
||||
showAlert(
|
||||
'Processing Failed',
|
||||
`An error occurred: ${error.message || 'The PDF might be corrupted or password protected.'}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink input file from WASM FS', e);
|
||||
}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink output file from WASM FS', e);
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return (
|
||||
f.type === 'application/pdf' ||
|
||||
f.name.toLowerCase().endsWith('.pdf')
|
||||
);
|
||||
});
|
||||
}
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', changePermissions);
|
||||
}
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', changePermissions);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { CombineSinglePageState } from '@/types';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
@@ -81,8 +82,7 @@ async function updateUI() {
|
||||
showLoader('Loading PDF...');
|
||||
result.pdf.destroy();
|
||||
pageState.file = result.file;
|
||||
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
pageState.pdfDoc = await loadPdfDocument(result.bytes, {
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
hideLoader();
|
||||
|
||||
@@ -6,6 +6,7 @@ import Cropper from 'cropperjs';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import { CropperState } from '@/types';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
@@ -305,10 +306,9 @@ async function performCrop() {
|
||||
async function performMetadataCrop(
|
||||
cropData: Record<number, any>
|
||||
): Promise<Uint8Array> {
|
||||
const pdfToModify = await PDFLibDocument.load(
|
||||
cropperState.originalPdfBytes!,
|
||||
{ throwOnInvalidObject: false }
|
||||
);
|
||||
const pdfToModify = await loadPdfDocument(cropperState.originalPdfBytes!, {
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
|
||||
for (const pageNum in cropData) {
|
||||
const pdfJsPage = await cropperState.pdfDoc.getPage(Number(pageNum));
|
||||
@@ -349,7 +349,7 @@ async function performFlatteningCrop(
|
||||
cropData: Record<number, any>
|
||||
): Promise<Uint8Array> {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const sourcePdfDocForCopying = await PDFLibDocument.load(
|
||||
const sourcePdfDocForCopying = await loadPdfDocument(
|
||||
cropperState.originalPdfBytes!,
|
||||
{ throwOnInvalidObject: false }
|
||||
);
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
getPDFDocument,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import Cropper from 'cropperjs';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.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();
|
||||
|
||||
// --- Global State for the Cropper Tool ---
|
||||
const cropperState = {
|
||||
@@ -145,21 +153,21 @@ async function performMetadataCrop(pdfToModify: any, cropData: any) {
|
||||
|
||||
// Define the 4 corners of the crop rectangle in visual coordinates (Top-Left origin)
|
||||
const visualCorners = [
|
||||
{ x: cropX, y: cropY }, // TL
|
||||
{ x: cropX + cropW, y: cropY }, // TR
|
||||
{ x: cropX + cropW, y: cropY + cropH }, // BR
|
||||
{ x: cropX, y: cropY + cropH }, // BL
|
||||
{ x: cropX, y: cropY }, // TL
|
||||
{ x: cropX + cropW, y: cropY }, // TR
|
||||
{ x: cropX + cropW, y: cropY + cropH }, // BR
|
||||
{ x: cropX, y: cropY + cropH }, // BL
|
||||
];
|
||||
|
||||
// This handles rotation, media box offsets, and coordinate system flips automatically
|
||||
const pdfCorners = visualCorners.map(p => {
|
||||
const pdfCorners = visualCorners.map((p) => {
|
||||
return viewport.convertToPdfPoint(p.x, p.y);
|
||||
});
|
||||
|
||||
// Find the bounding box of the converted points in PDF coordinates
|
||||
// convertToPdfPoint returns [x, y] arrays
|
||||
const pdfXs = pdfCorners.map(p => p[0]);
|
||||
const pdfYs = pdfCorners.map(p => p[1]);
|
||||
const pdfXs = pdfCorners.map((p) => p[0]);
|
||||
const pdfYs = pdfCorners.map((p) => p[1]);
|
||||
|
||||
const minX = Math.min(...pdfXs);
|
||||
const maxX = Math.max(...pdfXs);
|
||||
@@ -179,9 +187,9 @@ async function performFlatteningCrop(cropData: any) {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
|
||||
// Load the original PDF with pdf-lib to copy un-cropped pages from
|
||||
const sourcePdfDocForCopying = await PDFLibDocument.load(
|
||||
cropperState.originalPdfBytes,
|
||||
{ignoreEncryption: true, throwOnInvalidObject: false}
|
||||
const sourcePdfDocForCopying = await loadPdfDocument(
|
||||
cropperState.originalPdfBytes,
|
||||
{ ignoreEncryption: true, throwOnInvalidObject: false }
|
||||
);
|
||||
const totalPages = cropperState.pdfDoc.numPages;
|
||||
|
||||
@@ -224,7 +232,11 @@ async function performFlatteningCrop(cropData: any) {
|
||||
const jpegQuality = 0.9;
|
||||
|
||||
const jpegBytes = await new Promise((res) =>
|
||||
finalCanvas.toBlob((blob) => blob.arrayBuffer().then(res), 'image/jpeg', jpegQuality)
|
||||
finalCanvas.toBlob(
|
||||
(blob) => blob.arrayBuffer().then(res),
|
||||
'image/jpeg',
|
||||
jpegQuality
|
||||
)
|
||||
);
|
||||
const embeddedImage = await newPdfDoc.embedJpg(jpegBytes as ArrayBuffer);
|
||||
const newPage = newPdfDoc.addPage([finalWidth, finalHeight]);
|
||||
@@ -273,80 +285,73 @@ export async function setupCropperTool() {
|
||||
.getElementById('next-page')
|
||||
.addEventListener('click', () => changePage(1));
|
||||
|
||||
document
|
||||
.getElementById('crop-button')
|
||||
.addEventListener('click', async () => {
|
||||
// Get the last known crop from the active page before processing
|
||||
saveCurrentCrop();
|
||||
document.getElementById('crop-button').addEventListener('click', async () => {
|
||||
// Get the last known crop from the active page before processing
|
||||
saveCurrentCrop();
|
||||
|
||||
const isDestructive = (
|
||||
document.getElementById('destructive-crop-toggle') as HTMLInputElement
|
||||
).checked;
|
||||
const isApplyToAll = (
|
||||
document.getElementById('apply-to-all-toggle') as HTMLInputElement
|
||||
).checked;
|
||||
const isDestructive = (
|
||||
document.getElementById('destructive-crop-toggle') as HTMLInputElement
|
||||
).checked;
|
||||
const isApplyToAll = (
|
||||
document.getElementById('apply-to-all-toggle') as HTMLInputElement
|
||||
).checked;
|
||||
|
||||
let finalCropData = {};
|
||||
if (isApplyToAll) {
|
||||
const currentCrop =
|
||||
cropperState.pageCrops[cropperState.currentPageNum];
|
||||
if (!currentCrop) {
|
||||
showAlert('No Crop Area', 'Please select an area to crop first.');
|
||||
return;
|
||||
}
|
||||
// Apply the active page's crop to all pages
|
||||
for (let i = 1; i <= cropperState.pdfDoc.numPages; i++) {
|
||||
finalCropData[i] = currentCrop;
|
||||
}
|
||||
} else {
|
||||
// If not applying to all, only process pages with saved crops
|
||||
finalCropData = Object.keys(cropperState.pageCrops).reduce(
|
||||
(obj, key) => {
|
||||
obj[key] = cropperState.pageCrops[key];
|
||||
return obj;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
if (Object.keys(finalCropData).length === 0) {
|
||||
showAlert(
|
||||
'No Crop Area',
|
||||
'Please select an area on at least one page to crop.'
|
||||
);
|
||||
let finalCropData = {};
|
||||
if (isApplyToAll) {
|
||||
const currentCrop = cropperState.pageCrops[cropperState.currentPageNum];
|
||||
if (!currentCrop) {
|
||||
showAlert('No Crop Area', 'Please select an area to crop first.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Applying crop...');
|
||||
|
||||
try {
|
||||
let finalPdfBytes;
|
||||
if (isDestructive) {
|
||||
const newPdfDoc = await performFlatteningCrop(finalCropData);
|
||||
finalPdfBytes = await newPdfDoc.save();
|
||||
} else {
|
||||
const pdfToModify = await PDFLibDocument.load(
|
||||
cropperState.originalPdfBytes,
|
||||
{ignoreEncryption: true, throwOnInvalidObject: false}
|
||||
);
|
||||
await performMetadataCrop(pdfToModify, finalCropData);
|
||||
finalPdfBytes = await pdfToModify.save();
|
||||
}
|
||||
|
||||
const fileName = isDestructive
|
||||
? 'flattened_crop.pdf'
|
||||
: 'standard_crop.pdf';
|
||||
downloadFile(
|
||||
new Blob([finalPdfBytes], { type: 'application/pdf' }),
|
||||
fileName
|
||||
);
|
||||
showAlert('Success', 'Crop complete! Your download has started.');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred during cropping.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
// Apply the active page's crop to all pages
|
||||
for (let i = 1; i <= cropperState.pdfDoc.numPages; i++) {
|
||||
finalCropData[i] = currentCrop;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If not applying to all, only process pages with saved crops
|
||||
finalCropData = Object.keys(cropperState.pageCrops).reduce((obj, key) => {
|
||||
obj[key] = cropperState.pageCrops[key];
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.keys(finalCropData).length === 0) {
|
||||
showAlert(
|
||||
'No Crop Area',
|
||||
'Please select an area on at least one page to crop.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Applying crop...');
|
||||
|
||||
try {
|
||||
let finalPdfBytes;
|
||||
if (isDestructive) {
|
||||
const newPdfDoc = await performFlatteningCrop(finalCropData);
|
||||
finalPdfBytes = await newPdfDoc.save();
|
||||
} else {
|
||||
const pdfToModify = await loadPdfDocument(
|
||||
cropperState.originalPdfBytes,
|
||||
{ ignoreEncryption: true, throwOnInvalidObject: false }
|
||||
);
|
||||
await performMetadataCrop(pdfToModify, finalCropData);
|
||||
finalPdfBytes = await pdfToModify.save();
|
||||
}
|
||||
|
||||
const fileName = isDestructive
|
||||
? 'flattened_crop.pdf'
|
||||
: 'standard_crop.pdf';
|
||||
downloadFile(
|
||||
new Blob([finalPdfBytes], { type: 'application/pdf' }),
|
||||
fileName
|
||||
);
|
||||
showAlert('Success', 'Crop complete! Your download has started.');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred during cropping.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import {
|
||||
downloadFile,
|
||||
parsePageRanges,
|
||||
} from '../utils/helpers.js';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { deletePdfPages } from '../utils/pdf-operations.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { DeletePagesState } from '@/types';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
@@ -94,8 +94,7 @@ async function handleFile(file: File) {
|
||||
}
|
||||
showLoader('Loading PDF...');
|
||||
deleteState.file = result.file;
|
||||
deleteState.pdfDoc = await PDFDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
deleteState.pdfDoc = await loadPdfDocument(result.bytes, {
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
deleteState.pdfJsDoc = result.pdf;
|
||||
@@ -272,7 +271,7 @@ async function deletePages() {
|
||||
);
|
||||
const baseName = deleteState.file?.name.replace('.pdf', '') || 'document';
|
||||
downloadFile(
|
||||
new Blob([resultBytes as unknown as BlobPart], {
|
||||
new Blob([new Uint8Array(resultBytes)], {
|
||||
type: 'application/pdf',
|
||||
}),
|
||||
`${baseName}_pages_removed.pdf`
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
const pageState: DividePagesState = {
|
||||
file: null,
|
||||
@@ -87,9 +88,7 @@ async function updateUI() {
|
||||
pageState.file = result.file;
|
||||
showLoader('Loading PDF...');
|
||||
|
||||
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
pageState.pdfDoc = await loadPdfDocument(result.bytes);
|
||||
pageState.totalPages = pageState.pdfDoc.getPageCount();
|
||||
hideLoader();
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@ import { EditMetadataState } from '@/types';
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument, PDFName, PDFString } from 'pdf-lib';
|
||||
import { PDFName, PDFString } from 'pdf-lib';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
const pageState: EditMetadataState = {
|
||||
file: null,
|
||||
@@ -228,8 +229,7 @@ async function updateUI() {
|
||||
showLoader('Loading PDF...');
|
||||
result.pdf.destroy();
|
||||
pageState.file = result.file;
|
||||
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
pageState.pdfDoc = await loadPdfDocument(result.bytes, {
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
hideLoader();
|
||||
|
||||
@@ -207,7 +207,7 @@ export function renderEmailToHtml(
|
||||
): string {
|
||||
const { includeCcBcc = true, includeAttachments = true } = options;
|
||||
|
||||
let processedHtml = '';
|
||||
let processedHtml: string;
|
||||
if (email.htmlBody) {
|
||||
const sanitizedHtml = sanitizeEmailHtml(email.htmlBody);
|
||||
processedHtml = processInlineImages(sanitizedHtml, email.attachments);
|
||||
|
||||
@@ -1,243 +1,267 @@
|
||||
import { showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, initializeQpdf, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import {
|
||||
downloadFile,
|
||||
formatBytes,
|
||||
initializeQpdf,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import { EncryptPdfState } from '@/types';
|
||||
|
||||
const pageState: EncryptPdfState = {
|
||||
file: null,
|
||||
file: null,
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
pageState.file = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
const userPasswordInput = document.getElementById('user-password-input') as HTMLInputElement;
|
||||
if (userPasswordInput) userPasswordInput.value = '';
|
||||
const userPasswordInput = document.getElementById(
|
||||
'user-password-input'
|
||||
) as HTMLInputElement;
|
||||
if (userPasswordInput) userPasswordInput.value = '';
|
||||
|
||||
const ownerPasswordInput = document.getElementById('owner-password-input') as HTMLInputElement;
|
||||
if (ownerPasswordInput) ownerPasswordInput.value = '';
|
||||
const ownerPasswordInput = document.getElementById(
|
||||
'owner-password-input'
|
||||
) as HTMLInputElement;
|
||||
if (ownerPasswordInput) ownerPasswordInput.value = '';
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className =
|
||||
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (
|
||||
file.type === 'application/pdf' ||
|
||||
file.name.toLowerCase().endsWith('.pdf')
|
||||
) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function encryptPdf() {
|
||||
if (!pageState.file) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
if (!pageState.file) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const userPassword =
|
||||
(document.getElementById('user-password-input') as HTMLInputElement)
|
||||
?.value || '';
|
||||
const ownerPasswordInput =
|
||||
(document.getElementById('owner-password-input') as HTMLInputElement)
|
||||
?.value || '';
|
||||
|
||||
if (!userPassword) {
|
||||
showAlert('Input Required', 'Please enter a user password.');
|
||||
return;
|
||||
}
|
||||
|
||||
const ownerPassword = ownerPasswordInput || userPassword;
|
||||
const hasDistinctOwnerPassword = ownerPasswordInput !== '';
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
|
||||
try {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Initializing encryption...';
|
||||
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Reading PDF...';
|
||||
const fileBuffer = await readFileAsArrayBuffer(pageState.file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
if (loaderText)
|
||||
loaderText.textContent = 'Encrypting PDF with 256-bit AES...';
|
||||
|
||||
const args = [inputPath, '--encrypt', userPassword, ownerPassword, '256'];
|
||||
|
||||
// Only add restrictions if a distinct owner password was provided
|
||||
if (hasDistinctOwnerPassword) {
|
||||
args.push(
|
||||
'--modify=none',
|
||||
'--extract=n',
|
||||
'--print=none',
|
||||
'--accessibility=n',
|
||||
'--annotate=n',
|
||||
'--assemble=n',
|
||||
'--form=n',
|
||||
'--modify-other=n'
|
||||
);
|
||||
}
|
||||
|
||||
const userPassword = (document.getElementById('user-password-input') as HTMLInputElement)?.value || '';
|
||||
const ownerPasswordInput = (document.getElementById('owner-password-input') as HTMLInputElement)?.value || '';
|
||||
|
||||
if (!userPassword) {
|
||||
showAlert('Input Required', 'Please enter a user password.');
|
||||
return;
|
||||
}
|
||||
|
||||
const ownerPassword = ownerPasswordInput || userPassword;
|
||||
const hasDistinctOwnerPassword = ownerPasswordInput !== '';
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
args.push('--', outputPath);
|
||||
|
||||
try {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Initializing encryption...';
|
||||
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Reading PDF...';
|
||||
const fileBuffer = await readFileAsArrayBuffer(pageState.file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Encrypting PDF with 256-bit AES...';
|
||||
|
||||
const args = [inputPath, '--encrypt', userPassword, ownerPassword, '256'];
|
||||
|
||||
// Only add restrictions if a distinct owner password was provided
|
||||
if (hasDistinctOwnerPassword) {
|
||||
args.push(
|
||||
'--modify=none',
|
||||
'--extract=n',
|
||||
'--print=none',
|
||||
'--accessibility=n',
|
||||
'--annotate=n',
|
||||
'--assemble=n',
|
||||
'--form=n',
|
||||
'--modify-other=n'
|
||||
);
|
||||
}
|
||||
|
||||
args.push('--', outputPath);
|
||||
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
throw new Error(
|
||||
'Encryption failed: ' + (qpdfError.message || 'Unknown error')
|
||||
);
|
||||
}
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Preparing download...';
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
throw new Error('Encryption resulted in an empty file.');
|
||||
}
|
||||
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `encrypted-${pageState.file.name}`);
|
||||
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
|
||||
let successMessage = 'PDF encrypted successfully with 256-bit AES!';
|
||||
if (!hasDistinctOwnerPassword) {
|
||||
successMessage +=
|
||||
' Note: Without a separate owner password, the PDF has no usage restrictions.';
|
||||
}
|
||||
|
||||
showAlert('Success', successMessage, 'success', () => { resetState(); });
|
||||
} catch (error: any) {
|
||||
console.error('Error during PDF encryption:', error);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
showAlert(
|
||||
'Encryption Failed',
|
||||
`An error occurred: ${error.message || 'The PDF might be corrupted.'}`
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink input file:', e);
|
||||
}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink output file:', e);
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
throw new Error(
|
||||
'Encryption failed: ' + (qpdfError.message || 'Unknown error'),
|
||||
{ cause: qpdfError }
|
||||
);
|
||||
}
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Preparing download...';
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
throw new Error('Encryption resulted in an empty file.');
|
||||
}
|
||||
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `encrypted-${pageState.file.name}`);
|
||||
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
|
||||
let successMessage = 'PDF encrypted successfully with 256-bit AES!';
|
||||
if (!hasDistinctOwnerPassword) {
|
||||
successMessage +=
|
||||
' Note: Without a separate owner password, the PDF has no usage restrictions.';
|
||||
}
|
||||
|
||||
showAlert('Success', successMessage, 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error during PDF encryption:', error);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
showAlert(
|
||||
'Encryption Failed',
|
||||
`An error occurred: ${error.message || 'The PDF might be corrupted.'}`
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink input file:', e);
|
||||
}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink output file:', e);
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return (
|
||||
f.type === 'application/pdf' ||
|
||||
f.name.toLowerCase().endsWith('.pdf')
|
||||
);
|
||||
});
|
||||
}
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', encryptPdf);
|
||||
}
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', encryptPdf);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import JSZip from 'jszip';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
interface ExtractState {
|
||||
file: File | null;
|
||||
@@ -99,8 +100,7 @@ async function handleFile(file: File) {
|
||||
showLoader('Loading PDF...');
|
||||
extractState.file = result.file;
|
||||
result.pdf.destroy();
|
||||
extractState.pdfDoc = await PDFDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
extractState.pdfDoc = await loadPdfDocument(result.bytes, {
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
extractState.totalPages = extractState.pdfDoc.getPageCount();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { icons, createIcons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
import { FlattenPdfState } from '@/types';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
const pageState: FlattenPdfState = {
|
||||
files: [],
|
||||
@@ -115,7 +116,7 @@ async function flattenPdf() {
|
||||
|
||||
const file = pageState.files[0];
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer);
|
||||
const pdfDoc = await loadPdfDocument(arrayBuffer);
|
||||
|
||||
try {
|
||||
flattenFormsInDoc(pdfDoc);
|
||||
@@ -154,7 +155,7 @@ async function flattenPdf() {
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer);
|
||||
const pdfDoc = await loadPdfDocument(arrayBuffer);
|
||||
|
||||
try {
|
||||
flattenFormsInDoc(pdfDoc);
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
PageData,
|
||||
} from '@/types';
|
||||
import { extractExistingFields as extractExistingPdfFields } from './form-creator-extraction.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
let fields: FormField[] = [];
|
||||
let selectedField: FormField | null = null;
|
||||
@@ -3140,9 +3141,7 @@ async function handlePdfUpload(file: File) {
|
||||
if (!result) return;
|
||||
const arrayBuffer = result.bytes;
|
||||
uploadedPdfjsDoc = result.pdf;
|
||||
uploadedPdfDoc = await PDFDocument.load(arrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
uploadedPdfDoc = await loadPdfDocument(arrayBuffer);
|
||||
|
||||
// Check for existing fields and update counter
|
||||
existingFieldNames.clear();
|
||||
|
||||
@@ -6,9 +6,10 @@ import {
|
||||
formatBytes,
|
||||
parsePageRanges,
|
||||
} from '../utils/helpers.js';
|
||||
import { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib';
|
||||
import { rgb, StandardFonts } from 'pdf-lib';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { HeaderFooterState } from '@/types';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
const pageState: HeaderFooterState = { file: null, pdfDoc: null };
|
||||
|
||||
@@ -68,9 +69,7 @@ async function handleFiles(files: FileList) {
|
||||
if (!result) return;
|
||||
showLoader('Loading PDF...');
|
||||
|
||||
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
pageState.pdfDoc = await loadPdfDocument(result.bytes);
|
||||
pageState.file = result.file;
|
||||
result.pdf.destroy();
|
||||
|
||||
@@ -253,9 +252,12 @@ async function addHeaderFooter() {
|
||||
resetState();
|
||||
}
|
||||
);
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Could not add header or footer.');
|
||||
showAlert(
|
||||
'Error',
|
||||
e instanceof Error ? e.message : 'Could not add header or footer.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
@@ -203,7 +203,9 @@ async function preprocessFile(file: File): Promise<File> {
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`Failed to convert HEIC: ${file.name}`, e);
|
||||
throw new Error(`Failed to process HEIC file: ${file.name}`);
|
||||
throw new Error(`Failed to process HEIC file: ${file.name}`, {
|
||||
cause: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +249,9 @@ async function preprocessFile(file: File): Promise<File> {
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`Failed to convert WebP: ${file.name}`, e);
|
||||
throw new Error(`Failed to process WebP file: ${file.name}`);
|
||||
throw new Error(`Failed to process WebP file: ${file.name}`, {
|
||||
cause: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { applyInvertColors } from '../utils/image-effects.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { InvertColorsState } from '@/types';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
@@ -70,9 +71,7 @@ async function handleFiles(files: FileList) {
|
||||
if (!result) return;
|
||||
showLoader('Loading PDF...');
|
||||
result.pdf.destroy();
|
||||
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
pageState.pdfDoc = await loadPdfDocument(result.bytes);
|
||||
pageState.file = result.file;
|
||||
updateFileDisplay();
|
||||
document.getElementById('options-panel')?.classList.remove('hidden');
|
||||
|
||||
@@ -3,6 +3,7 @@ import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
interface NUpState {
|
||||
file: File | null;
|
||||
@@ -74,8 +75,7 @@ async function updateUI() {
|
||||
showLoader('Loading PDF...');
|
||||
result.pdf.destroy();
|
||||
pageState.file = result.file;
|
||||
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
pageState.pdfDoc = await loadPdfDocument(result.bytes, {
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
hideLoader();
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PDFDocument } from 'pdf-lib';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import Sortable from 'sortablejs';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
@@ -176,8 +177,7 @@ async function handleFile(file: File) {
|
||||
if (!result) return;
|
||||
showLoader('Loading PDF...');
|
||||
|
||||
organizeState.pdfDoc = await PDFDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
organizeState.pdfDoc = await loadPdfDocument(result.bytes, {
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
organizeState.pdfJsDoc = result.pdf;
|
||||
|
||||
@@ -5,16 +5,20 @@ import {
|
||||
convertPoints,
|
||||
} from '../utils/helpers.js';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import { PageDimensionsState } from '@/types';
|
||||
import {
|
||||
PageDimensionsState,
|
||||
AnalyzedPageData,
|
||||
UniqueSizeEntry,
|
||||
} from '@/types';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
const pageState: PageDimensionsState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
};
|
||||
|
||||
let analyzedPagesData: any[] = [];
|
||||
let analyzedPagesData: AnalyzedPageData[] = [];
|
||||
|
||||
function calculateAspectRatio(width: number, height: number): string {
|
||||
const ratio = width / height;
|
||||
@@ -35,11 +39,12 @@ function calculateArea(width: number, height: number, unit: string): string {
|
||||
convertedArea = (areaInPoints / (72 * 72)) * (25.4 * 25.4);
|
||||
unitSuffix = 'mm²';
|
||||
break;
|
||||
case 'px':
|
||||
case 'px': {
|
||||
const pxPerPoint = 96 / 72;
|
||||
convertedArea = areaInPoints * (pxPerPoint * pxPerPoint);
|
||||
unitSuffix = 'px²';
|
||||
break;
|
||||
}
|
||||
default:
|
||||
convertedArea = areaInPoints;
|
||||
unitSuffix = 'pt²';
|
||||
@@ -52,8 +57,8 @@ function calculateArea(width: number, height: number, unit: string): string {
|
||||
function getSummaryStats() {
|
||||
const totalPages = analyzedPagesData.length;
|
||||
|
||||
const uniqueSizes = new Map();
|
||||
analyzedPagesData.forEach((pageData: any) => {
|
||||
const uniqueSizes = new Map<string, UniqueSizeEntry>();
|
||||
analyzedPagesData.forEach((pageData) => {
|
||||
const key = `${pageData.width.toFixed(2)}x${pageData.height.toFixed(2)}`;
|
||||
const label = `${pageData.standardSize} (${pageData.orientation})`;
|
||||
uniqueSizes.set(key, {
|
||||
@@ -110,7 +115,7 @@ function renderSummary() {
|
||||
<ul class="space-y-1 text-sm text-gray-300">
|
||||
${stats.uniqueSizes
|
||||
.map(
|
||||
(size: any) => `
|
||||
(size: UniqueSizeEntry) => `
|
||||
<li>• ${size.label}: ${size.count} page${size.count > 1 ? 's' : ''}</li>
|
||||
`
|
||||
)
|
||||
@@ -145,7 +150,7 @@ function renderTable(unit: string) {
|
||||
|
||||
const pageNumCell = document.createElement('td');
|
||||
pageNumCell.className = 'px-4 py-3 text-white';
|
||||
pageNumCell.textContent = pageData.pageNum;
|
||||
pageNumCell.textContent = String(pageData.pageNum);
|
||||
|
||||
const dimensionsCell = document.createElement('td');
|
||||
dimensionsCell.className = 'px-4 py-3 text-gray-300';
|
||||
@@ -202,7 +207,7 @@ function exportToCSV() {
|
||||
];
|
||||
const csvRows = [headers.join(',')];
|
||||
|
||||
analyzedPagesData.forEach((pageData: any) => {
|
||||
analyzedPagesData.forEach((pageData) => {
|
||||
const width = convertPoints(pageData.width, unit);
|
||||
const height = convertPoints(pageData.height, unit);
|
||||
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
|
||||
@@ -239,7 +244,7 @@ function analyzeAndDisplayDimensions() {
|
||||
analyzedPagesData = [];
|
||||
const pages = pageState.pdfDoc.getPages();
|
||||
|
||||
pages.forEach((page: any, index: number) => {
|
||||
pages.forEach((page, index) => {
|
||||
const { width, height } = page.getSize();
|
||||
const rotation = page.getRotation().angle || 0;
|
||||
|
||||
@@ -341,9 +346,7 @@ async function handleFileSelect(files: FileList | null) {
|
||||
result.pdf.destroy();
|
||||
|
||||
pageState.file = result.file;
|
||||
pageState.pdfDoc = await PDFDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
pageState.pdfDoc = await loadPdfDocument(result.bytes);
|
||||
updateUI();
|
||||
analyzeAndDisplayDimensions();
|
||||
} catch (e) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type PageNumberPosition,
|
||||
type PageNumberFormat,
|
||||
} from '../utils/pdf-operations.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
interface PageState {
|
||||
file: File | null;
|
||||
@@ -89,9 +90,7 @@ async function handleFiles(files: FileList) {
|
||||
if (!result) return;
|
||||
showLoader('Loading PDF...');
|
||||
|
||||
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
pageState.pdfDoc = await loadPdfDocument(result.bytes);
|
||||
pageState.file = result.file;
|
||||
result.pdf.destroy();
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument, degrees, PageSizes } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.mjs',
|
||||
@@ -98,7 +99,7 @@ async function updateUI() {
|
||||
pageState.pdfBytes = new Uint8Array(result.bytes);
|
||||
pageState.pdfjsDoc = result.pdf;
|
||||
|
||||
pageState.pdfDoc = await PDFLibDocument.load(pageState.pdfBytes, {
|
||||
pageState.pdfDoc = await loadPdfDocument(pageState.pdfBytes, {
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
|
||||
@@ -430,7 +431,7 @@ async function createBooklet() {
|
||||
showLoader('Creating Booklet...');
|
||||
|
||||
try {
|
||||
const sourceDoc = await PDFLibDocument.load(pageState.pdfBytes.slice());
|
||||
const sourceDoc = await loadPdfDocument(pageState.pdfBytes.slice());
|
||||
const rotationMode =
|
||||
(
|
||||
document.querySelector(
|
||||
|
||||
@@ -20,6 +20,7 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
).toString();
|
||||
|
||||
import { t } from '../i18n/i18n';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
interface PageData {
|
||||
id: string; // Unique ID for DOM reconciliation
|
||||
@@ -435,8 +436,7 @@ async function loadPdfs(files: File[]) {
|
||||
pwResult.pdf.destroy();
|
||||
arrayBuffer = pwResult.bytes as ArrayBuffer;
|
||||
|
||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
const pdfDoc = await loadPdfDocument(arrayBuffer, {
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
currentPdfDocs.push(pdfDoc);
|
||||
@@ -859,8 +859,7 @@ async function handleInsertPdf(e: Event) {
|
||||
if (!pwResult) return;
|
||||
pwResult.pdf.destroy();
|
||||
|
||||
const pdfDoc = await PDFLibDocument.load(pwResult.bytes, {
|
||||
ignoreEncryption: true,
|
||||
const pdfDoc = await loadPdfDocument(pwResult.bytes, {
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
currentPdfDocs.push(pdfDoc);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PDFDocument, PDFName } from 'pdf-lib';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
// State management
|
||||
const pageState: { pdfDoc: PDFDocument | null; file: File | null } = {
|
||||
@@ -108,9 +109,7 @@ async function handleFileUpload(file: File) {
|
||||
if (!result) return;
|
||||
showLoader('Loading PDF...');
|
||||
result.pdf.destroy();
|
||||
pageState.pdfDoc = await PDFDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
pageState.pdfDoc = await loadPdfDocument(result.bytes);
|
||||
pageState.file = result.file;
|
||||
updateFileDisplay();
|
||||
document.getElementById('options-panel')?.classList.remove('hidden');
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { initPagePreview } from '../utils/page-preview.js';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
@@ -122,9 +123,7 @@ async function handleFileUpload(file: File) {
|
||||
if (!result) return;
|
||||
showLoader('Loading PDF...');
|
||||
result.pdf.destroy();
|
||||
pageState.pdfDoc = await PDFDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
pageState.pdfDoc = await loadPdfDocument(result.bytes);
|
||||
pageState.file = result.file;
|
||||
pageState.detectedBlankPages = [];
|
||||
updateFileDisplay();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { PDFDocument, PDFName } from 'pdf-lib';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
interface PageState {
|
||||
file: File | null;
|
||||
@@ -150,9 +151,7 @@ async function removeMetadata() {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Removing all metadata...';
|
||||
result.pdf.destroy();
|
||||
const pdfDoc = await PDFDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
const pdfDoc = await loadPdfDocument(result.bytes);
|
||||
|
||||
removeMetadataFromDoc(pdfDoc);
|
||||
|
||||
|
||||
@@ -1,230 +1,250 @@
|
||||
import { showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, initializeQpdf, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import {
|
||||
downloadFile,
|
||||
formatBytes,
|
||||
initializeQpdf,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import { RemoveRestrictionsState } from '@/types';
|
||||
|
||||
const pageState: RemoveRestrictionsState = {
|
||||
file: null,
|
||||
file: null,
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
pageState.file = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
const passwordInput = document.getElementById('owner-password-remove') as HTMLInputElement;
|
||||
if (passwordInput) passwordInput.value = '';
|
||||
const passwordInput = document.getElementById(
|
||||
'owner-password-remove'
|
||||
) as HTMLInputElement;
|
||||
if (passwordInput) passwordInput.value = '';
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className =
|
||||
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (
|
||||
file.type === 'application/pdf' ||
|
||||
file.name.toLowerCase().endsWith('.pdf')
|
||||
) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function removeRestrictions() {
|
||||
if (!pageState.file) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
if (!pageState.file) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const password =
|
||||
(document.getElementById('owner-password-remove') as HTMLInputElement)
|
||||
?.value || '';
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
|
||||
try {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Initializing...';
|
||||
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Reading PDF...';
|
||||
const fileBuffer = await readFileAsArrayBuffer(pageState.file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Removing restrictions...';
|
||||
|
||||
const args = [inputPath];
|
||||
|
||||
if (password) {
|
||||
args.push(`--password=${password}`);
|
||||
}
|
||||
|
||||
const password = (document.getElementById('owner-password-remove') as HTMLInputElement)?.value || '';
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
args.push('--decrypt', '--remove-restrictions', '--', outputPath);
|
||||
|
||||
try {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Initializing...';
|
||||
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Reading PDF...';
|
||||
const fileBuffer = await readFileAsArrayBuffer(pageState.file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Removing restrictions...';
|
||||
|
||||
const args = [inputPath];
|
||||
|
||||
if (password) {
|
||||
args.push(`--password=${password}`);
|
||||
}
|
||||
|
||||
args.push('--decrypt', '--remove-restrictions', '--', outputPath);
|
||||
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
if (
|
||||
qpdfError.message?.includes('password') ||
|
||||
qpdfError.message?.includes('encrypt')
|
||||
) {
|
||||
throw new Error(
|
||||
'Failed to remove restrictions. The PDF may require the correct owner password.'
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Failed to remove restrictions: ' +
|
||||
(qpdfError.message || 'Unknown error')
|
||||
);
|
||||
}
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Preparing download...';
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
throw new Error('Operation resulted in an empty file.');
|
||||
}
|
||||
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `unrestricted-${pageState.file.name}`);
|
||||
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
|
||||
showAlert(
|
||||
'Success',
|
||||
'PDF restrictions removed successfully! The file is now fully editable and printable.',
|
||||
'success',
|
||||
() => { resetState(); }
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
if (
|
||||
qpdfError.message?.includes('password') ||
|
||||
qpdfError.message?.includes('encrypt')
|
||||
) {
|
||||
throw new Error(
|
||||
'Failed to remove restrictions. The PDF may require the correct owner password.',
|
||||
{ cause: qpdfError }
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error('Error during restriction removal:', error);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
showAlert(
|
||||
'Operation Failed',
|
||||
`An error occurred: ${error.message || 'The PDF might be corrupted or password-protected.'}`
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink input file:', e);
|
||||
}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink output file:', e);
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Failed to remove restrictions: ' +
|
||||
(qpdfError.message || 'Unknown error'),
|
||||
{ cause: qpdfError }
|
||||
);
|
||||
}
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Preparing download...';
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
throw new Error('Operation resulted in an empty file.');
|
||||
}
|
||||
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `unrestricted-${pageState.file.name}`);
|
||||
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
|
||||
showAlert(
|
||||
'Success',
|
||||
'PDF restrictions removed successfully! The file is now fully editable and printable.',
|
||||
'success',
|
||||
() => {
|
||||
resetState();
|
||||
}
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error('Error during restriction removal:', error);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
showAlert(
|
||||
'Operation Failed',
|
||||
`An error occurred: ${error.message || 'The PDF might be corrupted or password-protected.'}`
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink input file:', e);
|
||||
}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink output file:', e);
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return (
|
||||
f.type === 'application/pdf' ||
|
||||
f.name.toLowerCase().endsWith('.pdf')
|
||||
);
|
||||
});
|
||||
}
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', removeRestrictions);
|
||||
}
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', removeRestrictions);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import JSZip from 'jszip';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
interface ReverseState {
|
||||
files: File[];
|
||||
@@ -79,7 +80,7 @@ function updateUI() {
|
||||
|
||||
async function reverseSingleFile(file: File): Promise<Uint8Array> {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
||||
const pdfDoc = await loadPdfDocument(arrayBuffer);
|
||||
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const pageCount = pdfDoc.getPageCount();
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '../utils/render-utils.js';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
@@ -235,8 +236,7 @@ async function updateUI() {
|
||||
}
|
||||
showLoader('Loading PDF...');
|
||||
|
||||
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
pageState.pdfDoc = await loadPdfDocument(result.bytes, {
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { rotatePdfPages } from '../utils/pdf-operations.js';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
@@ -207,8 +208,7 @@ async function updateUI() {
|
||||
}
|
||||
showLoader('Loading PDF...');
|
||||
|
||||
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
pageState.pdfDoc = await loadPdfDocument(result.bytes, {
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -6,16 +6,9 @@ import {
|
||||
downloadFile,
|
||||
} from '../utils/helpers.js';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { t } from '../i18n/i18n';
|
||||
|
||||
interface SignState {
|
||||
file: File | null;
|
||||
pdfDoc: any;
|
||||
viewerIframe: HTMLIFrameElement | null;
|
||||
viewerReady: boolean;
|
||||
blobUrl: string | null;
|
||||
}
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
import type { SignState, PDFViewerWindow } from '@/types';
|
||||
|
||||
const signState: SignState = {
|
||||
file: null,
|
||||
@@ -124,7 +117,7 @@ async function updateFileDisplay() {
|
||||
|
||||
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.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
signState.file = null;
|
||||
signState.pdfDoc = null;
|
||||
@@ -180,20 +173,26 @@ async function setupSignTool() {
|
||||
signState.viewerIframe = iframe;
|
||||
|
||||
const pdfBytes = await readFileAsArrayBuffer(signState.file);
|
||||
const blob = new Blob([pdfBytes as BlobPart], { type: 'application/pdf' });
|
||||
const blob = new Blob([new Uint8Array(pdfBytes as ArrayBuffer)], {
|
||||
type: 'application/pdf',
|
||||
});
|
||||
signState.blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
try {
|
||||
const existingPrefsRaw = localStorage.getItem('pdfjs.preferences');
|
||||
const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {};
|
||||
delete (existingPrefs as any).annotationEditorMode;
|
||||
const existingPrefs: Record<string, unknown> = existingPrefsRaw
|
||||
? JSON.parse(existingPrefsRaw)
|
||||
: {};
|
||||
delete existingPrefs.annotationEditorMode;
|
||||
const newPrefs = {
|
||||
...existingPrefs,
|
||||
enableSignatureEditor: true,
|
||||
enablePermissions: false,
|
||||
};
|
||||
localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs));
|
||||
} catch {}
|
||||
} catch (e) {
|
||||
console.warn('Failed to update pdfjs.preferences in localStorage', e);
|
||||
}
|
||||
|
||||
const viewerUrl = new URL(
|
||||
`${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html`,
|
||||
@@ -206,7 +205,7 @@ async function setupSignTool() {
|
||||
hideLoader();
|
||||
signState.viewerReady = true;
|
||||
try {
|
||||
const viewerWindow: any = iframe.contentWindow;
|
||||
const viewerWindow = iframe.contentWindow as PDFViewerWindow | null;
|
||||
if (viewerWindow && viewerWindow.PDFViewerApplication) {
|
||||
const app = viewerWindow.PDFViewerApplication;
|
||||
const doc = viewerWindow.document;
|
||||
@@ -235,7 +234,12 @@ async function setupSignTool() {
|
||||
'editorHighlightButton'
|
||||
) as HTMLButtonElement | null;
|
||||
highlightBtn?.click();
|
||||
} catch {}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'Failed to auto-click highlight button in PDF viewer',
|
||||
e
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -258,7 +262,8 @@ async function applyAndSaveSignatures() {
|
||||
}
|
||||
|
||||
try {
|
||||
const viewerWindow: any = signState.viewerIframe.contentWindow;
|
||||
const viewerWindow = signState.viewerIframe
|
||||
.contentWindow as PDFViewerWindow | null;
|
||||
if (!viewerWindow || !viewerWindow.PDFViewerApplication) {
|
||||
showAlert('Viewer not ready', 'The PDF viewer is still initializing.');
|
||||
return;
|
||||
@@ -277,11 +282,11 @@ async function applyAndSaveSignatures() {
|
||||
app.pdfDocument.annotationStorage
|
||||
);
|
||||
const pdfBytes = new Uint8Array(rawPdfBytes);
|
||||
const pdfDoc = await PDFDocument.load(pdfBytes);
|
||||
const pdfDoc = await loadPdfDocument(pdfBytes);
|
||||
pdfDoc.getForm().flatten();
|
||||
const flattenedPdfBytes = await pdfDoc.save();
|
||||
|
||||
const blob = new Blob([flattenedPdfBytes as BlobPart], {
|
||||
const blob = new Blob([new Uint8Array(flattenedPdfBytes)], {
|
||||
type: 'application/pdf',
|
||||
});
|
||||
downloadFile(
|
||||
|
||||
@@ -14,6 +14,7 @@ import { isCpdfAvailable } from '../utils/cpdf-helper.js';
|
||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||
import JSZip from 'jszip';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
// @ts-ignore
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
@@ -98,9 +99,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
result.pdf.destroy();
|
||||
state.files[0] = result.file;
|
||||
state.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
state.pdfDoc = await loadPdfDocument(result.bytes);
|
||||
}
|
||||
// Update page count
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${state.pdfDoc.getPageCount()} pages`;
|
||||
@@ -148,9 +147,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
result.pdf.destroy();
|
||||
state.files[0] = result.file;
|
||||
state.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
state.pdfDoc = await loadPdfDocument(result.bytes);
|
||||
showLoader('Rendering page previews...');
|
||||
} else {
|
||||
throw new Error('No PDF document loaded');
|
||||
|
||||
@@ -247,7 +247,8 @@ async function convertToPdf() {
|
||||
} catch (error) {
|
||||
console.error(`Failed to process ${file.name}:`, error);
|
||||
throw new Error(
|
||||
`Could not process "${file.name}". The file may be corrupted.`
|
||||
`Could not process "${file.name}". The file may be corrupted.`,
|
||||
{ cause: error }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { TextColorState } from '@/types';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
@@ -75,9 +76,7 @@ async function handleFiles(files: FileList) {
|
||||
if (!result) return;
|
||||
showLoader('Loading PDF...');
|
||||
result.pdf.destroy();
|
||||
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
pageState.pdfDoc = await loadPdfDocument(result.bytes);
|
||||
pageState.file = result.file;
|
||||
updateFileDisplay();
|
||||
document.getElementById('options-panel')?.classList.remove('hidden');
|
||||
|
||||
@@ -1,238 +1,260 @@
|
||||
import forge from 'node-forge';
|
||||
import { ExtractedSignature, SignatureValidationResult } from '@/types';
|
||||
|
||||
|
||||
export function extractSignatures(pdfBytes: Uint8Array): ExtractedSignature[] {
|
||||
const signatures: ExtractedSignature[] = [];
|
||||
const pdfString = new TextDecoder('latin1').decode(pdfBytes);
|
||||
const signatures: ExtractedSignature[] = [];
|
||||
const pdfString = new TextDecoder('latin1').decode(pdfBytes);
|
||||
|
||||
// Find all signature objects for /Type /Sig
|
||||
const sigRegex = /\/Type\s*\/Sig\b/g;
|
||||
let sigMatch;
|
||||
let sigIndex = 0;
|
||||
// Find all signature objects for /Type /Sig
|
||||
const sigRegex = /\/Type\s*\/Sig\b/g;
|
||||
let sigMatch;
|
||||
let sigIndex = 0;
|
||||
|
||||
while ((sigMatch = sigRegex.exec(pdfString)) !== null) {
|
||||
try {
|
||||
const searchStart = Math.max(0, sigMatch.index - 5000);
|
||||
const searchEnd = Math.min(pdfString.length, sigMatch.index + 10000);
|
||||
const context = pdfString.substring(searchStart, searchEnd);
|
||||
const byteRangeMatch = context.match(/\/ByteRange\s*\[\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s*\]/);
|
||||
if (!byteRangeMatch) continue;
|
||||
while ((sigMatch = sigRegex.exec(pdfString)) !== null) {
|
||||
try {
|
||||
const searchStart = Math.max(0, sigMatch.index - 5000);
|
||||
const searchEnd = Math.min(pdfString.length, sigMatch.index + 10000);
|
||||
const context = pdfString.substring(searchStart, searchEnd);
|
||||
const byteRangeMatch = context.match(
|
||||
/\/ByteRange\s*\[\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s*\]/
|
||||
);
|
||||
if (!byteRangeMatch) continue;
|
||||
|
||||
const byteRange = [
|
||||
parseInt(byteRangeMatch[1], 10),
|
||||
parseInt(byteRangeMatch[2], 10),
|
||||
parseInt(byteRangeMatch[3], 10),
|
||||
parseInt(byteRangeMatch[4], 10),
|
||||
];
|
||||
const byteRange = [
|
||||
parseInt(byteRangeMatch[1], 10),
|
||||
parseInt(byteRangeMatch[2], 10),
|
||||
parseInt(byteRangeMatch[3], 10),
|
||||
parseInt(byteRangeMatch[4], 10),
|
||||
];
|
||||
|
||||
const contentsMatch = context.match(/\/Contents\s*<([0-9A-Fa-f]+)>/);
|
||||
if (!contentsMatch) continue;
|
||||
const contentsMatch = context.match(/\/Contents\s*<([0-9A-Fa-f]+)>/);
|
||||
if (!contentsMatch) continue;
|
||||
|
||||
const hexContents = contentsMatch[1];
|
||||
const contentsBytes = hexToBytes(hexContents);
|
||||
const hexContents = contentsMatch[1];
|
||||
const contentsBytes = hexToBytes(hexContents);
|
||||
|
||||
const reasonMatch = context.match(/\/Reason\s*\(([^)]*)\)/);
|
||||
const locationMatch = context.match(/\/Location\s*\(([^)]*)\)/);
|
||||
const contactMatch = context.match(/\/ContactInfo\s*\(([^)]*)\)/);
|
||||
const nameMatch = context.match(/\/Name\s*\(([^)]*)\)/);
|
||||
const timeMatch = context.match(/\/M\s*\(D:(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/);
|
||||
const reasonMatch = context.match(/\/Reason\s*\(([^)]*)\)/);
|
||||
const locationMatch = context.match(/\/Location\s*\(([^)]*)\)/);
|
||||
const contactMatch = context.match(/\/ContactInfo\s*\(([^)]*)\)/);
|
||||
const nameMatch = context.match(/\/Name\s*\(([^)]*)\)/);
|
||||
const timeMatch = context.match(
|
||||
/\/M\s*\(D:(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/
|
||||
);
|
||||
|
||||
let signingTime: string | undefined;
|
||||
if (timeMatch) {
|
||||
signingTime = `${timeMatch[1]}-${timeMatch[2]}-${timeMatch[3]}T${timeMatch[4]}:${timeMatch[5]}:${timeMatch[6]}`;
|
||||
}
|
||||
let signingTime: string | undefined;
|
||||
if (timeMatch) {
|
||||
signingTime = `${timeMatch[1]}-${timeMatch[2]}-${timeMatch[3]}T${timeMatch[4]}:${timeMatch[5]}:${timeMatch[6]}`;
|
||||
}
|
||||
|
||||
signatures.push({
|
||||
index: sigIndex++,
|
||||
contents: contentsBytes,
|
||||
byteRange,
|
||||
reason: reasonMatch ? decodeURIComponent(escape(reasonMatch[1])) : undefined,
|
||||
location: locationMatch ? decodeURIComponent(escape(locationMatch[1])) : undefined,
|
||||
contactInfo: contactMatch ? decodeURIComponent(escape(contactMatch[1])) : undefined,
|
||||
name: nameMatch ? decodeURIComponent(escape(nameMatch[1])) : undefined,
|
||||
signingTime,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Error extracting signature at index', sigIndex, e);
|
||||
}
|
||||
signatures.push({
|
||||
index: sigIndex++,
|
||||
contents: contentsBytes,
|
||||
byteRange,
|
||||
reason: reasonMatch
|
||||
? decodeURIComponent(escape(reasonMatch[1]))
|
||||
: undefined,
|
||||
location: locationMatch
|
||||
? decodeURIComponent(escape(locationMatch[1]))
|
||||
: undefined,
|
||||
contactInfo: contactMatch
|
||||
? decodeURIComponent(escape(contactMatch[1]))
|
||||
: undefined,
|
||||
name: nameMatch ? decodeURIComponent(escape(nameMatch[1])) : undefined,
|
||||
signingTime,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Error extracting signature at index', sigIndex, e);
|
||||
}
|
||||
}
|
||||
|
||||
return signatures;
|
||||
return signatures;
|
||||
}
|
||||
|
||||
export function validateSignature(
|
||||
signature: ExtractedSignature,
|
||||
pdfBytes: Uint8Array,
|
||||
trustedCert?: forge.pki.Certificate
|
||||
signature: ExtractedSignature,
|
||||
pdfBytes: Uint8Array,
|
||||
trustedCert?: forge.pki.Certificate
|
||||
): SignatureValidationResult {
|
||||
const result: SignatureValidationResult = {
|
||||
signatureIndex: signature.index,
|
||||
isValid: false,
|
||||
signerName: 'Unknown',
|
||||
issuer: 'Unknown',
|
||||
validFrom: new Date(0),
|
||||
validTo: new Date(0),
|
||||
isExpired: false,
|
||||
isSelfSigned: false,
|
||||
isTrusted: false,
|
||||
algorithms: { digest: 'Unknown', signature: 'Unknown' },
|
||||
serialNumber: '',
|
||||
byteRange: signature.byteRange,
|
||||
coverageStatus: 'unknown',
|
||||
reason: signature.reason,
|
||||
location: signature.location,
|
||||
contactInfo: signature.contactInfo,
|
||||
};
|
||||
const result: SignatureValidationResult = {
|
||||
signatureIndex: signature.index,
|
||||
isValid: false,
|
||||
signerName: 'Unknown',
|
||||
issuer: 'Unknown',
|
||||
validFrom: new Date(0),
|
||||
validTo: new Date(0),
|
||||
isExpired: false,
|
||||
isSelfSigned: false,
|
||||
isTrusted: false,
|
||||
algorithms: { digest: 'Unknown', signature: 'Unknown' },
|
||||
serialNumber: '',
|
||||
byteRange: signature.byteRange,
|
||||
coverageStatus: 'unknown',
|
||||
reason: signature.reason,
|
||||
location: signature.location,
|
||||
contactInfo: signature.contactInfo,
|
||||
};
|
||||
|
||||
try {
|
||||
const binaryString = String.fromCharCode.apply(null, Array.from(signature.contents));
|
||||
const asn1 = forge.asn1.fromDer(binaryString);
|
||||
const p7 = forge.pkcs7.messageFromAsn1(asn1) as any;
|
||||
try {
|
||||
const binaryString = String.fromCharCode.apply(
|
||||
null,
|
||||
Array.from(signature.contents)
|
||||
);
|
||||
const asn1 = forge.asn1.fromDer(binaryString);
|
||||
const p7 = forge.pkcs7.messageFromAsn1(asn1) as any;
|
||||
|
||||
if (!p7.certificates || p7.certificates.length === 0) {
|
||||
result.errorMessage = 'No certificates found in signature';
|
||||
return result;
|
||||
}
|
||||
|
||||
const signerCert = p7.certificates[0] as forge.pki.Certificate;
|
||||
|
||||
const subjectCN = signerCert.subject.getField('CN');
|
||||
const subjectO = signerCert.subject.getField('O');
|
||||
const subjectE = signerCert.subject.getField('E') || signerCert.subject.getField('emailAddress');
|
||||
const issuerCN = signerCert.issuer.getField('CN');
|
||||
const issuerO = signerCert.issuer.getField('O');
|
||||
|
||||
result.signerName = (subjectCN?.value as string) ?? 'Unknown';
|
||||
result.signerOrg = subjectO?.value as string | undefined;
|
||||
result.signerEmail = subjectE?.value as string | undefined;
|
||||
result.issuer = (issuerCN?.value as string) ?? 'Unknown';
|
||||
result.issuerOrg = issuerO?.value as string | undefined;
|
||||
result.validFrom = signerCert.validity.notBefore;
|
||||
result.validTo = signerCert.validity.notAfter;
|
||||
result.serialNumber = signerCert.serialNumber;
|
||||
|
||||
const now = new Date();
|
||||
result.isExpired = now > result.validTo || now < result.validFrom;
|
||||
|
||||
result.isSelfSigned = signerCert.isIssuer(signerCert);
|
||||
|
||||
// Check trust against provided certificate
|
||||
if (trustedCert) {
|
||||
try {
|
||||
const isTrustedIssuer = trustedCert.isIssuer(signerCert);
|
||||
const isSameCert = signerCert.serialNumber === trustedCert.serialNumber;
|
||||
|
||||
let chainTrusted = false;
|
||||
for (const cert of p7.certificates) {
|
||||
if (trustedCert.isIssuer(cert) ||
|
||||
(cert as forge.pki.Certificate).serialNumber === trustedCert.serialNumber) {
|
||||
chainTrusted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result.isTrusted = isTrustedIssuer || isSameCert || chainTrusted;
|
||||
} catch {
|
||||
result.isTrusted = false;
|
||||
}
|
||||
}
|
||||
|
||||
result.algorithms = {
|
||||
digest: getDigestAlgorithmName(signerCert.siginfo?.algorithmOid || ''),
|
||||
signature: getSignatureAlgorithmName(signerCert.signatureOid || ''),
|
||||
};
|
||||
|
||||
// Parse signing time if available in signature
|
||||
if (signature.signingTime) {
|
||||
result.signatureDate = new Date(signature.signingTime);
|
||||
} else {
|
||||
// Try to extract from authenticated attributes
|
||||
try {
|
||||
const signedData = p7 as any;
|
||||
if (signedData.rawCapture?.authenticatedAttributes) {
|
||||
// Look for signing time attribute
|
||||
for (const attr of signedData.rawCapture.authenticatedAttributes) {
|
||||
if (attr.type === forge.pki.oids.signingTime) {
|
||||
result.signatureDate = attr.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
if (signature.byteRange && signature.byteRange.length === 4) {
|
||||
const [start1, len1, start2, len2] = signature.byteRange;
|
||||
const totalCovered = len1 + len2;
|
||||
const expectedEnd = start2 + len2;
|
||||
|
||||
if (expectedEnd === pdfBytes.length) {
|
||||
result.coverageStatus = 'full';
|
||||
} else if (expectedEnd < pdfBytes.length) {
|
||||
result.coverageStatus = 'partial';
|
||||
}
|
||||
}
|
||||
|
||||
result.isValid = true;
|
||||
|
||||
} catch (e) {
|
||||
result.errorMessage = e instanceof Error ? e.message : 'Failed to parse signature';
|
||||
if (!p7.certificates || p7.certificates.length === 0) {
|
||||
result.errorMessage = 'No certificates found in signature';
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
const signerCert = p7.certificates[0] as forge.pki.Certificate;
|
||||
|
||||
const subjectCN = signerCert.subject.getField('CN');
|
||||
const subjectO = signerCert.subject.getField('O');
|
||||
const subjectE =
|
||||
signerCert.subject.getField('E') ||
|
||||
signerCert.subject.getField('emailAddress');
|
||||
const issuerCN = signerCert.issuer.getField('CN');
|
||||
const issuerO = signerCert.issuer.getField('O');
|
||||
|
||||
result.signerName = (subjectCN?.value as string) ?? 'Unknown';
|
||||
result.signerOrg = subjectO?.value as string | undefined;
|
||||
result.signerEmail = subjectE?.value as string | undefined;
|
||||
result.issuer = (issuerCN?.value as string) ?? 'Unknown';
|
||||
result.issuerOrg = issuerO?.value as string | undefined;
|
||||
result.validFrom = signerCert.validity.notBefore;
|
||||
result.validTo = signerCert.validity.notAfter;
|
||||
result.serialNumber = signerCert.serialNumber;
|
||||
|
||||
const now = new Date();
|
||||
result.isExpired = now > result.validTo || now < result.validFrom;
|
||||
|
||||
result.isSelfSigned = signerCert.isIssuer(signerCert);
|
||||
|
||||
// Check trust against provided certificate
|
||||
if (trustedCert) {
|
||||
try {
|
||||
const isTrustedIssuer = trustedCert.isIssuer(signerCert);
|
||||
const isSameCert = signerCert.serialNumber === trustedCert.serialNumber;
|
||||
|
||||
let chainTrusted = false;
|
||||
for (const cert of p7.certificates) {
|
||||
if (
|
||||
trustedCert.isIssuer(cert) ||
|
||||
(cert as forge.pki.Certificate).serialNumber ===
|
||||
trustedCert.serialNumber
|
||||
) {
|
||||
chainTrusted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result.isTrusted = isTrustedIssuer || isSameCert || chainTrusted;
|
||||
} catch {
|
||||
result.isTrusted = false;
|
||||
}
|
||||
}
|
||||
|
||||
result.algorithms = {
|
||||
digest: getDigestAlgorithmName(signerCert.siginfo?.algorithmOid || ''),
|
||||
signature: getSignatureAlgorithmName(signerCert.signatureOid || ''),
|
||||
};
|
||||
|
||||
// Parse signing time if available in signature
|
||||
if (signature.signingTime) {
|
||||
result.signatureDate = new Date(signature.signingTime);
|
||||
} else {
|
||||
// Try to extract from authenticated attributes
|
||||
try {
|
||||
const signedData = p7 as any;
|
||||
if (signedData.rawCapture?.authenticatedAttributes) {
|
||||
// Look for signing time attribute
|
||||
for (const attr of signedData.rawCapture.authenticatedAttributes) {
|
||||
if (attr.type === forge.pki.oids.signingTime) {
|
||||
result.signatureDate = attr.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'Failed to extract signing time from authenticated attributes',
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (signature.byteRange && signature.byteRange.length === 4) {
|
||||
const [start1, len1, start2, len2] = signature.byteRange;
|
||||
const totalCovered = len1 + len2;
|
||||
const expectedEnd = start2 + len2;
|
||||
|
||||
if (expectedEnd === pdfBytes.length) {
|
||||
result.coverageStatus = 'full';
|
||||
} else if (expectedEnd < pdfBytes.length) {
|
||||
result.coverageStatus = 'partial';
|
||||
}
|
||||
}
|
||||
|
||||
result.isValid = true;
|
||||
} catch (e) {
|
||||
result.errorMessage =
|
||||
e instanceof Error ? e.message : 'Failed to parse signature';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function validatePdfSignatures(
|
||||
pdfBytes: Uint8Array,
|
||||
trustedCert?: forge.pki.Certificate
|
||||
pdfBytes: Uint8Array,
|
||||
trustedCert?: forge.pki.Certificate
|
||||
): Promise<SignatureValidationResult[]> {
|
||||
const signatures = extractSignatures(pdfBytes);
|
||||
return signatures.map(sig => validateSignature(sig, pdfBytes, trustedCert));
|
||||
const signatures = extractSignatures(pdfBytes);
|
||||
return signatures.map((sig) => validateSignature(sig, pdfBytes, trustedCert));
|
||||
}
|
||||
|
||||
export function countSignatures(pdfBytes: Uint8Array): number {
|
||||
return extractSignatures(pdfBytes).length;
|
||||
return extractSignatures(pdfBytes).length;
|
||||
}
|
||||
|
||||
function hexToBytes(hex: string): Uint8Array {
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
||||
}
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
||||
}
|
||||
|
||||
let actualLength = bytes.length;
|
||||
while (actualLength > 0 && bytes[actualLength - 1] === 0) {
|
||||
actualLength--;
|
||||
}
|
||||
let actualLength = bytes.length;
|
||||
while (actualLength > 0 && bytes[actualLength - 1] === 0) {
|
||||
actualLength--;
|
||||
}
|
||||
|
||||
return bytes.slice(0, actualLength);
|
||||
return bytes.slice(0, actualLength);
|
||||
}
|
||||
|
||||
function getDigestAlgorithmName(oid: string): string {
|
||||
const digestAlgorithms: Record<string, string> = {
|
||||
'1.2.840.113549.2.5': 'MD5',
|
||||
'1.3.14.3.2.26': 'SHA-1',
|
||||
'2.16.840.1.101.3.4.2.1': 'SHA-256',
|
||||
'2.16.840.1.101.3.4.2.2': 'SHA-384',
|
||||
'2.16.840.1.101.3.4.2.3': 'SHA-512',
|
||||
'2.16.840.1.101.3.4.2.4': 'SHA-224',
|
||||
};
|
||||
return digestAlgorithms[oid] || oid || 'Unknown';
|
||||
const digestAlgorithms: Record<string, string> = {
|
||||
'1.2.840.113549.2.5': 'MD5',
|
||||
'1.3.14.3.2.26': 'SHA-1',
|
||||
'2.16.840.1.101.3.4.2.1': 'SHA-256',
|
||||
'2.16.840.1.101.3.4.2.2': 'SHA-384',
|
||||
'2.16.840.1.101.3.4.2.3': 'SHA-512',
|
||||
'2.16.840.1.101.3.4.2.4': 'SHA-224',
|
||||
};
|
||||
return digestAlgorithms[oid] || oid || 'Unknown';
|
||||
}
|
||||
|
||||
function getSignatureAlgorithmName(oid: string): string {
|
||||
const signatureAlgorithms: Record<string, string> = {
|
||||
'1.2.840.113549.1.1.1': 'RSA',
|
||||
'1.2.840.113549.1.1.5': 'RSA with SHA-1',
|
||||
'1.2.840.113549.1.1.11': 'RSA with SHA-256',
|
||||
'1.2.840.113549.1.1.12': 'RSA with SHA-384',
|
||||
'1.2.840.113549.1.1.13': 'RSA with SHA-512',
|
||||
'1.2.840.10045.2.1': 'ECDSA',
|
||||
'1.2.840.10045.4.1': 'ECDSA with SHA-1',
|
||||
'1.2.840.10045.4.3.2': 'ECDSA with SHA-256',
|
||||
'1.2.840.10045.4.3.3': 'ECDSA with SHA-384',
|
||||
'1.2.840.10045.4.3.4': 'ECDSA with SHA-512',
|
||||
};
|
||||
return signatureAlgorithms[oid] || oid || 'Unknown';
|
||||
const signatureAlgorithms: Record<string, string> = {
|
||||
'1.2.840.113549.1.1.1': 'RSA',
|
||||
'1.2.840.113549.1.1.5': 'RSA with SHA-1',
|
||||
'1.2.840.113549.1.1.11': 'RSA with SHA-256',
|
||||
'1.2.840.113549.1.1.12': 'RSA with SHA-384',
|
||||
'1.2.840.113549.1.1.13': 'RSA with SHA-512',
|
||||
'1.2.840.10045.2.1': 'ECDSA',
|
||||
'1.2.840.10045.4.1': 'ECDSA with SHA-1',
|
||||
'1.2.840.10045.4.3.2': 'ECDSA with SHA-256',
|
||||
'1.2.840.10045.4.3.3': 'ECDSA with SHA-384',
|
||||
'1.2.840.10045.4.3.4': 'ECDSA with SHA-512',
|
||||
};
|
||||
return signatureAlgorithms[oid] || oid || 'Unknown';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user