- 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:
alam00000
2026-03-26 13:40:21 +05:30
parent 9d362b1cf8
commit 9278774b8a
110 changed files with 1413 additions and 1196 deletions

View File

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

View File

@@ -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();

View File

@@ -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,
});

View File

@@ -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
);
}
});
}

View File

@@ -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;

View File

@@ -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');

View File

@@ -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, '');

View File

@@ -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);

View File

@@ -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);
}
});

View File

@@ -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();

View File

@@ -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 }
);

View File

@@ -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();
}
});
}

View File

@@ -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`

View File

@@ -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();

View File

@@ -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();

View File

@@ -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);

View File

@@ -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);
}
});

View File

@@ -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();

View File

@@ -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);

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -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,
});
}
}

View File

@@ -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');

View File

@@ -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();

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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(

View File

@@ -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);

View File

@@ -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');

View File

@@ -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();

View File

@@ -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);

View File

@@ -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);
}
});

View File

@@ -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();

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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(

View File

@@ -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');

View File

@@ -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 }
);
}
}

View File

@@ -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');

View File

@@ -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';
}