- 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

@@ -653,7 +653,9 @@ export async function extractPageModel(
fontNameMap.set(internalName, fontObj.name);
}
}
} catch {}
} catch (e) {
console.warn(`Failed to resolve font name for "${internalName}"`, e);
}
}
const rawItems = sortCompareTextItems(

View File

@@ -15,6 +15,7 @@ import {
import { downloadFile } from '../../utils/helpers.ts';
import { computeComparisonForPair } from '../../logic/compare-render.ts';
import { LRUCache } from '../lru-cache.ts';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
const HIGHLIGHT_COLORS: Record<
string,
@@ -112,8 +113,8 @@ export async function exportComparePdf(
]);
const [libDoc1, libDoc2] = await Promise.all([
bytes1 ? PDFDocument.load(bytes1, { ignoreEncryption: true }) : null,
bytes2 ? PDFDocument.load(bytes2, { ignoreEncryption: true }) : null,
bytes1 ? loadPdfDocument(bytes1, { ignoreEncryption: true }) : null,
bytes2 ? loadPdfDocument(bytes2, { ignoreEncryption: true }) : null,
]);
const includeChange = options?.includeChange ?? (() => true);

View File

@@ -15,7 +15,6 @@ import {
import { setupCanvasEditor } from '../canvasEditor.js';
import { toolLogic } from '../logic/index.js';
import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { icons, createIcons } from 'lucide';
import Sortable from 'sortablejs';
import { makeUniqueFileKey } from '../utils/deduplicate-filename.js';
@@ -29,6 +28,7 @@ import {
singlePdfLoadTools,
} from '../config/pdf-tools.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',
@@ -71,9 +71,7 @@ async function handleSinglePdfUpload(toolId: string, file: File) {
}
const pdfBytes = await readFileAsArrayBuffer(file);
state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
ignoreEncryption: true,
});
state.pdfDoc = await loadPdfDocument(pdfBytes as ArrayBuffer);
hideLoader();
if (
@@ -88,7 +86,7 @@ async function handleSinglePdfUpload(toolId: string, file: File) {
return;
}
const decryptedBytes = await readFileAsArrayBuffer(decryptedFile);
state.pdfDoc = await PDFLibDocument.load(decryptedBytes as ArrayBuffer);
state.pdfDoc = await loadPdfDocument(decryptedBytes as ArrayBuffer);
state.files = [decryptedFile];
}
@@ -603,9 +601,7 @@ async function handleMultiFileUpload(toolId: string) {
const pdfFilesLoaded = await Promise.all(
pdfFilesUnloaded.map(async (file) => {
const pdfBytes = await readFileAsArrayBuffer(file);
const pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
ignoreEncryption: true,
});
const pdfDoc = await loadPdfDocument(pdfBytes as ArrayBuffer);
return {
file,

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

View File

@@ -1,6 +1,22 @@
import { PDFDocument } from 'pdf-lib';
export interface PageDimensionsState {
file: File | null;
pdfDoc: PDFDocument | null;
file: File | null;
pdfDoc: PDFDocument | null;
}
export interface AnalyzedPageData {
pageNum: number;
width: number;
height: number;
orientation: string;
standardSize: string;
rotation: number;
}
export interface UniqueSizeEntry {
count: number;
label: string;
width: number;
height: number;
}

View File

@@ -1,5 +1,32 @@
import { PDFDocument } from 'pdf-lib';
export interface SignPdfState {
file: File | null;
pdfBytes: ArrayBuffer | null;
signatureData: string | null;
file: File | null;
pdfBytes: ArrayBuffer | null;
signatureData: string | null;
}
export interface PDFViewerEventBus {
_on: (event: string, callback: () => void) => void;
dispatch: (event: string, data: Record<string, unknown>) => void;
}
export interface PDFViewerApplication {
eventBus?: PDFViewerEventBus;
pdfDocument?: {
saveDocument: (storage: unknown) => Promise<ArrayBuffer>;
annotationStorage: unknown;
};
}
export interface PDFViewerWindow extends Window {
PDFViewerApplication?: PDFViewerApplication;
}
export interface SignState {
file: File | null;
pdfDoc: PDFDocument | null;
viewerIframe: HTMLIFrameElement | null;
viewerReady: boolean;
blobUrl: string | null;
}

View File

@@ -97,7 +97,9 @@ export async function performCondenseCompression(
return { ...result, usedFallback: true };
} catch (fallbackError: any) {
const msg = fallbackError?.message || String(fallbackError);
throw new Error(`PDF compression failed: ${msg}`);
throw new Error(`PDF compression failed: ${msg}`, {
cause: fallbackError,
});
}
}
}

View File

@@ -52,7 +52,8 @@ export async function loadGhostscript(): Promise<GhostscriptInterface> {
} catch (error: any) {
loadPromise = null;
throw new Error(
`Failed to load Ghostscript from ${normalizedUrl}: ${error.message}`
`Failed to load Ghostscript from ${normalizedUrl}: ${error.message}`,
{ cause: error }
);
}
})();

View File

@@ -4,12 +4,9 @@
* Requires user to configure Ghostscript URL in WASM Settings.
*/
import {
getWasmBaseUrl,
fetchWasmFile,
isWasmAvailable,
} from '../config/wasm-cdn-config.js';
import { PDFDocument, PDFDict, PDFName, PDFArray } from 'pdf-lib';
import { getWasmBaseUrl, isWasmAvailable } from '../config/wasm-cdn-config.js';
import { PDFDict, PDFName, PDFArray } from 'pdf-lib';
import { loadPdfDocument } from './load-pdf-document.js';
interface GhostscriptModule {
FS: {
@@ -160,7 +157,9 @@ export async function convertToPdfA(
);
} catch (e) {
console.error('[Ghostscript] Failed to setup PDF/A assets:', e);
throw new Error('Conversion failed: could not create PDF/A definition');
throw new Error('Conversion failed: could not create PDF/A definition', {
cause: e,
});
}
const args = [
@@ -200,7 +199,7 @@ export async function convertToPdfA(
exitCode = gs.callMain(args);
} catch (e) {
console.error('[Ghostscript] Exception:', e);
throw new Error(`Ghostscript threw an exception: ${e}`);
throw new Error(`Ghostscript threw an exception: ${e}`, { cause: e });
}
console.log('[Ghostscript] Exit code:', exitCode);
@@ -208,23 +207,23 @@ export async function convertToPdfA(
if (exitCode !== 0) {
try {
gs.FS.unlink(inputPath);
} catch {
/* ignore */
} catch (e) {
console.warn('[Ghostscript] Failed to clean up temp file:', e);
}
try {
gs.FS.unlink(outputPath);
} catch {
/* ignore */
} catch (e) {
console.warn('[Ghostscript] Failed to clean up temp file:', e);
}
try {
gs.FS.unlink(iccPath);
} catch {
/* ignore */
} catch (e) {
console.warn('[Ghostscript] Failed to clean up temp file:', e);
}
try {
gs.FS.unlink(pdfaDefPath);
} catch {
/* ignore */
} catch (e) {
console.warn('[Ghostscript] Failed to clean up temp file:', e);
}
throw new Error(`Ghostscript conversion failed with exit code ${exitCode}`);
}
@@ -237,29 +236,29 @@ export async function convertToPdfA(
output = gs.FS.readFile(outputPath);
} catch (e) {
console.error('[Ghostscript] Failed to read output:', e);
throw new Error('Ghostscript did not produce output file');
throw new Error('Ghostscript did not produce output file', { cause: e });
}
// Cleanup
try {
gs.FS.unlink(inputPath);
} catch {
/* ignore */
} catch (e) {
console.warn('[Ghostscript] Failed to clean up temp file:', e);
}
try {
gs.FS.unlink(outputPath);
} catch {
/* ignore */
} catch (e) {
console.warn('[Ghostscript] Failed to clean up temp file:', e);
}
try {
gs.FS.unlink(iccPath);
} catch {
/* ignore */
} catch (e) {
console.warn('[Ghostscript] Failed to clean up temp file:', e);
}
try {
gs.FS.unlink(pdfaDefPath);
} catch {
/* ignore */
} catch (e) {
console.warn('[Ghostscript] Failed to clean up temp file:', e);
}
if (level !== 'PDF/A-1b') {
@@ -282,8 +281,7 @@ export async function convertToPdfA(
async function addPageGroupDictionaries(
pdfData: Uint8Array
): Promise<Uint8Array> {
const pdfDoc = await PDFDocument.load(pdfData, {
ignoreEncryption: true,
const pdfDoc = await loadPdfDocument(pdfData, {
updateMetadata: false,
});
@@ -352,7 +350,7 @@ async function addPageGroupDictionaries(
}
if (iccProfileRef) {
pdfDoc.context.enumerateIndirectObjects().forEach(([ref, obj]) => {
pdfDoc.context.enumerateIndirectObjects().forEach(([_ref, obj]) => {
if (
obj instanceof PDFDict ||
(obj && typeof obj === 'object' && 'dict' in obj)
@@ -437,17 +435,23 @@ export async function convertFontsToOutlines(
} catch (e) {
try {
gs.FS.unlink(inputPath);
} catch {}
throw new Error(`Ghostscript threw an exception: ${e}`);
} catch (e2) {
console.warn('[Ghostscript] Failed to clean up temp file:', e2);
}
throw new Error(`Ghostscript threw an exception: ${e}`, { cause: e });
}
if (exitCode !== 0) {
try {
gs.FS.unlink(inputPath);
} catch {}
} catch (e) {
console.warn('[Ghostscript] Failed to clean up temp file:', e);
}
try {
gs.FS.unlink(outputPath);
} catch {}
} catch (e) {
console.warn('[Ghostscript] Failed to clean up temp file:', e);
}
throw new Error(`Ghostscript conversion failed with exit code ${exitCode}`);
}
@@ -455,15 +459,19 @@ export async function convertFontsToOutlines(
try {
output = gs.FS.readFile(outputPath);
} catch (e) {
throw new Error('Ghostscript did not produce output file');
throw new Error('Ghostscript did not produce output file', { cause: e });
}
try {
gs.FS.unlink(inputPath);
} catch {}
} catch (e) {
console.warn('[Ghostscript] Failed to clean up temp file:', e);
}
try {
gs.FS.unlink(outputPath);
} catch {}
} catch (e) {
console.warn('[Ghostscript] Failed to clean up temp file:', e);
}
return output;
}

View File

@@ -0,0 +1,14 @@
import { PDFDocument } from 'pdf-lib';
type LoadOptions = Parameters<typeof PDFDocument.load>[1];
type PDFDocumentInstance = Awaited<ReturnType<typeof PDFDocument.load>>;
export async function loadPdfDocument(
pdf: Uint8Array | ArrayBuffer,
options?: LoadOptions
): Promise<PDFDocumentInstance> {
return PDFDocument.load(pdf, {
ignoreEncryption: true,
...options,
});
}

View File

@@ -1,11 +1,12 @@
import { PDFDocument, degrees, rgb, StandardFonts, PageSizes } from 'pdf-lib';
import { loadPdfDocument } from './load-pdf-document.js';
export async function mergePdfs(
pdfBytesList: Uint8Array[]
): Promise<Uint8Array> {
const mergedDoc = await PDFDocument.create();
for (const bytes of pdfBytesList) {
const srcDoc = await PDFDocument.load(bytes);
const srcDoc = await loadPdfDocument(bytes);
const copiedPages = await mergedDoc.copyPages(
srcDoc,
srcDoc.getPageIndices()
@@ -19,7 +20,7 @@ export async function splitPdf(
pdfBytes: Uint8Array,
pageIndices: number[]
): Promise<Uint8Array> {
const srcDoc = await PDFDocument.load(pdfBytes);
const srcDoc = await loadPdfDocument(pdfBytes);
const newPdf = await PDFDocument.create();
const copiedPages = await newPdf.copyPages(srcDoc, pageIndices);
copiedPages.forEach((page) => newPdf.addPage(page));
@@ -30,7 +31,7 @@ export async function rotatePdfUniform(
pdfBytes: Uint8Array,
angle: number
): Promise<Uint8Array> {
const srcDoc = await PDFDocument.load(pdfBytes);
const srcDoc = await loadPdfDocument(pdfBytes);
const newPdfDoc = await PDFDocument.create();
const pageCount = srcDoc.getPageCount();
@@ -75,7 +76,7 @@ export async function rotatePdfPages(
pdfBytes: Uint8Array,
rotations: number[]
): Promise<Uint8Array> {
const srcDoc = await PDFDocument.load(pdfBytes);
const srcDoc = await loadPdfDocument(pdfBytes);
const newPdfDoc = await PDFDocument.create();
const pageCount = srcDoc.getPageCount();
@@ -121,7 +122,7 @@ export async function deletePdfPages(
pdfBytes: Uint8Array,
pagesToDelete: Set<number>
): Promise<Uint8Array> {
const srcDoc = await PDFDocument.load(pdfBytes);
const srcDoc = await loadPdfDocument(pdfBytes);
const totalPages = srcDoc.getPageCount();
const pagesToKeep: number[] = [];
@@ -196,7 +197,7 @@ export async function addTextWatermark(
pdfBytes: Uint8Array,
options: TextWatermarkOptions
): Promise<Uint8Array> {
const pdfDoc = await PDFDocument.load(pdfBytes);
const pdfDoc = await loadPdfDocument(pdfBytes);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
@@ -274,7 +275,7 @@ export async function addImageWatermark(
pdfBytes: Uint8Array,
options: ImageWatermarkOptions
): Promise<Uint8Array> {
const pdfDoc = await PDFDocument.load(pdfBytes);
const pdfDoc = await loadPdfDocument(pdfBytes);
const image =
options.imageType === 'png'
? await pdfDoc.embedPng(options.imageBytes)
@@ -330,7 +331,7 @@ export async function addPageNumbers(
pdfBytes: Uint8Array,
options: PageNumberOptions
): Promise<Uint8Array> {
const pdfDoc = await PDFDocument.load(pdfBytes);
const pdfDoc = await loadPdfDocument(pdfBytes);
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
const pages = pdfDoc.getPages();
const totalPages = pages.length;
@@ -481,7 +482,7 @@ export async function fixPageSize(
[targetWidth, targetHeight] = [targetHeight, targetWidth];
}
const sourceDoc = await PDFDocument.load(pdfBytes);
const sourceDoc = await loadPdfDocument(pdfBytes);
const outputDoc = await PDFDocument.create();
for (const sourcePage of sourceDoc.getPages()) {

View File

@@ -67,7 +67,9 @@ export async function loadPyMuPDF(): Promise<any> {
return cachedPyMuPDF;
} catch (error: any) {
loadPromise = null;
throw new Error(`Failed to load PyMuPDF from CDN: ${error.message}`);
throw new Error(`Failed to load PyMuPDF from CDN: ${error.message}`, {
cause: error,
});
}
})();

View File

@@ -1,4 +1,5 @@
import { PDFDocument, PDFName } from 'pdf-lib';
import { loadPdfDocument } from './load-pdf-document.js';
export interface SanitizeOptions {
flattenForms: boolean;
@@ -503,7 +504,7 @@ export async function sanitizePdf(
pdfBytes: Uint8Array,
options: SanitizeOptions
): Promise<{ pdfDoc: PDFDocument; bytes: Uint8Array }> {
const pdfDoc = await PDFDocument.load(pdfBytes);
const pdfDoc = await loadPdfDocument(pdfBytes);
if (options.flattenForms) {
try {

View File

@@ -3,7 +3,7 @@ import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class AddBlankPageNode extends BaseWorkflowNode {
readonly category = 'Organize & Manage' as const;
@@ -47,7 +47,7 @@ export class AddBlankPageNode extends BaseWorkflowNode {
return {
pdf: await processBatch(pdfInputs, async (input) => {
const pdfDoc = await PDFDocument.load(input.bytes);
const pdfDoc = await loadPdfDocument(input.bytes);
const firstPage = pdfDoc.getPages()[0];
const { width, height } = firstPage
? firstPage.getSize()

View File

@@ -5,6 +5,7 @@ import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument, rgb } from 'pdf-lib';
import { hexToRgb } from '../../utils/helpers.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class BackgroundColorNode extends BaseWorkflowNode {
readonly category = 'Edit & Annotate' as const;
@@ -34,7 +35,7 @@ export class BackgroundColorNode extends BaseWorkflowNode {
return {
pdf: await processBatch(pdfInputs, async (input) => {
const srcDoc = await PDFDocument.load(input.bytes);
const srcDoc = await loadPdfDocument(input.bytes);
const newDoc = await PDFDocument.create();
for (let i = 0; i < srcDoc.getPageCount(); i++) {

View File

@@ -4,6 +4,7 @@ import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument, PageSizes } from 'pdf-lib';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
const paperSizeLookup: Record<string, [number, number]> = {
Letter: PageSizes.Letter,
@@ -88,7 +89,7 @@ export class BookletNode extends BaseWorkflowNode {
return {
pdf: await processBatch(pdfInputs, async (input) => {
const sourceDoc = await PDFDocument.load(input.bytes);
const sourceDoc = await loadPdfDocument(input.bytes);
const totalPages = sourceDoc.getPageCount();
const pagesPerSheet = rows * cols;
const outputDoc = await PDFDocument.create();

View File

@@ -2,8 +2,8 @@ import { ClassicPreset } from 'rete';
import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { PDFData, SocketData, MultiPDFData } from '../types';
import { PDFDocument } from 'pdf-lib';
import { loadPyMuPDF } from '../../utils/pymupdf-loader.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class CbzToPdfNode extends BaseWorkflowNode {
readonly category = 'Input' as const;
@@ -55,7 +55,7 @@ export class CbzToPdfNode extends BaseWorkflowNode {
for (const file of this.files) {
const blob = await pymupdf.convertToPdf(file, { filetype: 'cbz' });
const bytes = new Uint8Array(await blob.arrayBuffer());
const document = await PDFDocument.load(bytes);
const document = await loadPdfDocument(bytes);
results.push({
type: 'pdf',
document,

View File

@@ -3,13 +3,13 @@ import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib';
import {
performCondenseCompression,
performPhotonCompression,
} from '../../utils/compress.js';
import type { CondenseCustomSettings } from '../../utils/compress.js';
import { isPyMuPDFAvailable } from '../../utils/pymupdf-loader.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class CompressNode extends BaseWorkflowNode {
readonly category = 'Optimize & Repair' as const;
@@ -115,7 +115,7 @@ export class CompressNode extends BaseWorkflowNode {
pdfBytes = await performPhotonCompression(arrayBuffer, level);
}
const document = await PDFDocument.load(pdfBytes);
const document = await loadPdfDocument(pdfBytes);
return {
type: 'pdf',

View File

@@ -3,7 +3,7 @@ import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class CropNode extends BaseWorkflowNode {
readonly category = 'Edit & Annotate' as const;
@@ -51,7 +51,7 @@ export class CropNode extends BaseWorkflowNode {
return {
pdf: await processBatch(pdfInputs, async (input) => {
const pdfDoc = await PDFDocument.load(input.bytes);
const pdfDoc = await loadPdfDocument(input.bytes);
const pages = pdfDoc.getPages();
for (const page of pages) {

View File

@@ -3,8 +3,8 @@ import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib';
import { decryptPdfBytes } from '../../utils/pdf-decrypt.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class DecryptNode extends BaseWorkflowNode {
readonly category = 'Secure PDF' as const;
@@ -37,7 +37,7 @@ export class DecryptNode extends BaseWorkflowNode {
input.bytes,
password
);
const document = await PDFDocument.load(resultBytes, {
const document = await loadPdfDocument(resultBytes, {
throwOnInvalidObject: false,
});

View File

@@ -4,7 +4,7 @@ import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { deletePdfPages, parseDeletePages } from '../../utils/pdf-operations';
import { PDFDocument } from 'pdf-lib';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class DeletePagesNode extends BaseWorkflowNode {
readonly category = 'Organize & Manage' as const;
@@ -32,11 +32,11 @@ export class DeletePagesNode extends BaseWorkflowNode {
return {
pdf: await processBatch(pdfInputs, async (input) => {
const srcDoc = await PDFDocument.load(input.bytes);
const srcDoc = await loadPdfDocument(input.bytes);
const totalPages = srcDoc.getPageCount();
const pagesToDelete = parseDeletePages(deleteStr, totalPages);
const resultBytes = await deletePdfPages(input.bytes, pagesToDelete);
const resultDoc = await PDFDocument.load(resultBytes);
const resultDoc = await loadPdfDocument(resultBytes);
return {
type: 'pdf',
document: resultDoc,

View File

@@ -3,8 +3,8 @@ import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib';
import { loadPyMuPDF } from '../../utils/pymupdf-loader.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class DeskewNode extends BaseWorkflowNode {
readonly category = 'Optimize & Repair' as const;
@@ -51,7 +51,7 @@ export class DeskewNode extends BaseWorkflowNode {
});
const bytes = new Uint8Array(await resultPdf.arrayBuffer());
const document = await PDFDocument.load(bytes);
const document = await loadPdfDocument(bytes);
return {
type: 'pdf',

View File

@@ -3,13 +3,13 @@ import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib';
import {
signPdf,
parsePfxFile,
parseCombinedPem,
} from '../../logic/digital-sign-pdf.js';
import type { CertificateData } from '@/types';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class DigitalSignNode extends BaseWorkflowNode {
readonly category = 'Secure PDF' as const;
@@ -117,7 +117,7 @@ export class DigitalSignNode extends BaseWorkflowNode {
});
const bytes = new Uint8Array(signedBytes);
const document = await PDFDocument.load(bytes);
const document = await loadPdfDocument(bytes);
return {
type: 'pdf',

View File

@@ -5,6 +5,7 @@ import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib';
import { parsePageRange } from '../../utils/pdf-operations';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class DividePagesNode extends BaseWorkflowNode {
readonly category = 'Organize & Manage' as const;
@@ -42,7 +43,7 @@ export class DividePagesNode extends BaseWorkflowNode {
return {
pdf: await processBatch(pdfInputs, async (input) => {
const srcDoc = await PDFDocument.load(input.bytes);
const srcDoc = await loadPdfDocument(input.bytes);
const newDoc = await PDFDocument.create();
const totalPages = srcDoc.getPageCount();

View File

@@ -3,7 +3,7 @@ import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class EditMetadataNode extends BaseWorkflowNode {
readonly category = 'Organize & Manage' as const;
@@ -61,7 +61,7 @@ export class EditMetadataNode extends BaseWorkflowNode {
return {
pdf: await processBatch(pdfInputs, async (input) => {
const pdfDoc = await PDFDocument.load(input.bytes);
const pdfDoc = await loadPdfDocument(input.bytes);
if (title) pdfDoc.setTitle(title);
if (author) pdfDoc.setAuthor(author);

View File

@@ -2,9 +2,9 @@ import { ClassicPreset } from 'rete';
import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { PDFData, SocketData, MultiPDFData } from '../types';
import { PDFDocument } from 'pdf-lib';
import { parseEmailFile, renderEmailToHtml } from '../../logic/email-to-pdf.js';
import { loadPyMuPDF } from '../../utils/pymupdf-loader.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class EmailToPdfNode extends BaseWorkflowNode {
readonly category = 'Input' as const;
@@ -87,7 +87,11 @@ export class EmailToPdfNode extends BaseWorkflowNode {
pageSize,
});
const pdfBlob = await (pymupdf as any).htmlToPdf(htmlContent, {
const pdfBlob = await (
pymupdf as unknown as {
htmlToPdf(html: string, options: unknown): Promise<Blob>;
}
).htmlToPdf(htmlContent, {
pageSize,
margins: { top: 50, right: 50, bottom: 50, left: 50 },
attachments: email.attachments
@@ -99,7 +103,7 @@ export class EmailToPdfNode extends BaseWorkflowNode {
});
const bytes = new Uint8Array(await pdfBlob.arrayBuffer());
const document = await PDFDocument.load(bytes);
const document = await loadPdfDocument(bytes);
results.push({
type: 'pdf',
document,

View File

@@ -3,8 +3,8 @@ import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib';
import { initializeQpdf } from '../../utils/helpers.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class EncryptNode extends BaseWorkflowNode {
readonly category = 'Secure PDF' as const;
@@ -90,9 +90,7 @@ export class EncryptNode extends BaseWorkflowNode {
}
const resultBytes = new Uint8Array(encryptedData);
const document = await PDFDocument.load(resultBytes, {
ignoreEncryption: true,
});
const document = await loadPdfDocument(resultBytes);
return {
type: 'pdf',

View File

@@ -2,8 +2,8 @@ import { ClassicPreset } from 'rete';
import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { PDFData, SocketData, MultiPDFData } from '../types';
import { PDFDocument } from 'pdf-lib';
import { loadPyMuPDF } from '../../utils/pymupdf-loader.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class EpubToPdfNode extends BaseWorkflowNode {
readonly category = 'Input' as const;
@@ -54,7 +54,7 @@ export class EpubToPdfNode extends BaseWorkflowNode {
for (const file of this.files) {
const blob = await pymupdf.convertToPdf(file, { filetype: 'epub' });
const bytes = new Uint8Array(await blob.arrayBuffer());
const document = await PDFDocument.load(bytes);
const document = await loadPdfDocument(bytes);
results.push({
type: 'pdf',
document,

View File

@@ -2,8 +2,8 @@ import { ClassicPreset } from 'rete';
import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { PDFData, SocketData, MultiPDFData } from '../types';
import { PDFDocument } from 'pdf-lib';
import { getLibreOfficeConverter } from '../../utils/libreoffice-loader.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class ExcelToPdfNode extends BaseWorkflowNode {
readonly category = 'Input' as const;
@@ -62,7 +62,7 @@ export class ExcelToPdfNode extends BaseWorkflowNode {
for (const file of this.files) {
const resultBlob = await converter.convertToPdf(file);
const bytes = new Uint8Array(await resultBlob.arrayBuffer());
const document = await PDFDocument.load(bytes);
const document = await loadPdfDocument(bytes);
results.push({
type: 'pdf',
document,

View File

@@ -5,6 +5,7 @@ import type { SocketData, MultiPDFData } from '../types';
import { requirePdfInput, extractAllPdfs } from '../types';
import { PDFDocument } from 'pdf-lib';
import { parsePageRange } from '../../utils/pdf-operations';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class ExtractPagesNode extends BaseWorkflowNode {
readonly category = 'Organize & Manage' as const;
@@ -37,7 +38,7 @@ export class ExtractPagesNode extends BaseWorkflowNode {
const allItems = [];
for (const input of allPdfs) {
const srcDoc = await PDFDocument.load(input.bytes);
const srcDoc = await loadPdfDocument(input.bytes);
const totalPages = srcDoc.getPageCount();
const indices = parsePageRange(rangeStr, totalPages);

View File

@@ -2,8 +2,8 @@ import { ClassicPreset } from 'rete';
import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { PDFData, SocketData, MultiPDFData } from '../types';
import { PDFDocument } from 'pdf-lib';
import { loadPyMuPDF } from '../../utils/pymupdf-loader.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class Fb2ToPdfNode extends BaseWorkflowNode {
readonly category = 'Input' as const;
@@ -54,7 +54,7 @@ export class Fb2ToPdfNode extends BaseWorkflowNode {
for (const file of this.files) {
const blob = await pymupdf.convertToPdf(file, { filetype: 'fb2' });
const bytes = new Uint8Array(await blob.arrayBuffer());
const document = await PDFDocument.load(bytes);
const document = await loadPdfDocument(bytes);
results.push({
type: 'pdf',
document,

View File

@@ -4,8 +4,8 @@ import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { fixPageSize } from '../../utils/pdf-operations';
import { PDFDocument } from 'pdf-lib';
import { hexToRgb } from '../../utils/helpers.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class FixPageSizeNode extends BaseWorkflowNode {
readonly category = 'Organize & Manage' as const;
@@ -90,7 +90,7 @@ export class FixPageSizeNode extends BaseWorkflowNode {
customUnits,
});
const resultDoc = await PDFDocument.load(resultBytes);
const resultDoc = await loadPdfDocument(resultBytes);
return {
type: 'pdf',

View File

@@ -3,8 +3,8 @@ import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib';
import { flattenAnnotations } from '../../utils/flatten-annotations.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class FlattenNode extends BaseWorkflowNode {
readonly category = 'Secure PDF' as const;
@@ -24,7 +24,7 @@ export class FlattenNode extends BaseWorkflowNode {
return {
pdf: await processBatch(pdfInputs, async (input) => {
const pdfDoc = await PDFDocument.load(input.bytes);
const pdfDoc = await loadPdfDocument(input.bytes);
try {
const form = pdfDoc.getForm();

View File

@@ -3,8 +3,8 @@ import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib';
import { convertFontsToOutlines } from '../../utils/ghostscript-loader.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class FontToOutlineNode extends BaseWorkflowNode {
readonly category = 'Optimize & Repair' as const;
@@ -28,7 +28,7 @@ export class FontToOutlineNode extends BaseWorkflowNode {
new Uint8Array(input.bytes)
);
const bytes = new Uint8Array(resultBytes);
const document = await PDFDocument.load(bytes);
const document = await loadPdfDocument(bytes);
return {
type: 'pdf',

View File

@@ -3,8 +3,9 @@ import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
import { StandardFonts, rgb } from 'pdf-lib';
import { hexToRgb } from '../../utils/helpers.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class HeaderFooterNode extends BaseWorkflowNode {
readonly category = 'Edit & Annotate' as const;
@@ -89,7 +90,7 @@ export class HeaderFooterNode extends BaseWorkflowNode {
pdf: await processBatch(pdfInputs, async (input) => {
if (!hasAny) return input;
const pdfDoc = await PDFDocument.load(input.bytes);
const pdfDoc = await loadPdfDocument(input.bytes);
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
const pages = pdfDoc.getPages();
const totalPages = pages.length;

View File

@@ -2,8 +2,8 @@ import { ClassicPreset } from 'rete';
import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { PDFData, SocketData } from '../types';
import { PDFDocument } from 'pdf-lib';
import { loadPyMuPDF } from '../../utils/pymupdf-loader.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class ImageInputNode extends BaseWorkflowNode {
readonly category = 'Input' as const;
@@ -57,7 +57,7 @@ export class ImageInputNode extends BaseWorkflowNode {
const pymupdf = await loadPyMuPDF();
const pdfBlob = await pymupdf.imagesToPdf(this.files);
const bytes = new Uint8Array(await pdfBlob.arrayBuffer());
const document = await PDFDocument.load(bytes);
const document = await loadPdfDocument(bytes);
const result: PDFData = {
type: 'pdf',

View File

@@ -2,8 +2,8 @@ import { ClassicPreset } from 'rete';
import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { PDFData, SocketData, MultiPDFData } from '../types';
import { PDFDocument } from 'pdf-lib';
import { loadPyMuPDF } from '../../utils/pymupdf-loader.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class JsonToPdfNode extends BaseWorkflowNode {
readonly category = 'Input' as const;
@@ -65,7 +65,7 @@ export class JsonToPdfNode extends BaseWorkflowNode {
pageSize: 'a4',
});
const bytes = new Uint8Array(await pdfBlob.arrayBuffer());
const pdfDoc = await PDFDocument.load(bytes);
const pdfDoc = await loadPdfDocument(bytes);
results.push({
type: 'pdf',
document: pdfDoc,

View File

@@ -3,8 +3,8 @@ import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib';
import { initializeQpdf } from '../../utils/helpers.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class LinearizeNode extends BaseWorkflowNode {
readonly category = 'Optimize & Repair' as const;
@@ -50,7 +50,7 @@ export class LinearizeNode extends BaseWorkflowNode {
}
}
const document = await PDFDocument.load(resultBytes);
const document = await loadPdfDocument(resultBytes);
return {
type: 'pdf',

View File

@@ -2,9 +2,9 @@ import { ClassicPreset } from 'rete';
import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { PDFData, SocketData, MultiPDFData } from '../types';
import { PDFDocument } from 'pdf-lib';
import { loadPyMuPDF } from '../../utils/pymupdf-loader.js';
import MarkdownIt from 'markdown-it';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
const md = new MarkdownIt({ html: true, linkify: true, typographer: true });
@@ -65,11 +65,15 @@ export class MarkdownToPdfNode extends BaseWorkflowNode {
blockquote { border-left: 3px solid #ccc; margin-left: 0; padding-left: 12px; color: #555; }
table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ccc; padding: 6px 12px; }
</style></head><body>${md.render(textContent)}</body></html>`;
const pdfBlob = await (pymupdf as any).htmlToPdf(htmlContent, {
const pdfBlob = await (
pymupdf as unknown as {
htmlToPdf(html: string, options: unknown): Promise<Blob>;
}
).htmlToPdf(htmlContent, {
pageSize: 'a4',
});
const bytes = new Uint8Array(await pdfBlob.arrayBuffer());
const pdfDoc = await PDFDocument.load(bytes);
const pdfDoc = await loadPdfDocument(bytes);
results.push({
type: 'pdf',
document: pdfDoc,

View File

@@ -4,7 +4,7 @@ import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { extractAllPdfs } from '../types';
import { mergePdfs } from '../../utils/pdf-operations';
import { PDFDocument } from 'pdf-lib';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class MergeNode extends BaseWorkflowNode {
readonly category = 'Organize & Manage' as const;
@@ -26,7 +26,7 @@ export class MergeNode extends BaseWorkflowNode {
throw new Error('No PDFs connected to Merge node');
const mergedBytes = await mergePdfs(allPdfs.map((p) => p.bytes));
const mergedDoc = await PDFDocument.load(mergedBytes);
const mergedDoc = await loadPdfDocument(mergedBytes);
return {
pdf: {

View File

@@ -2,8 +2,8 @@ import { ClassicPreset } from 'rete';
import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { PDFData, SocketData, MultiPDFData } from '../types';
import { PDFDocument } from 'pdf-lib';
import { loadPyMuPDF } from '../../utils/pymupdf-loader.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class MobiToPdfNode extends BaseWorkflowNode {
readonly category = 'Input' as const;
@@ -54,7 +54,7 @@ export class MobiToPdfNode extends BaseWorkflowNode {
for (const file of this.files) {
const blob = await pymupdf.convertToPdf(file, { filetype: 'mobi' });
const bytes = new Uint8Array(await blob.arrayBuffer());
const document = await PDFDocument.load(bytes);
const document = await loadPdfDocument(bytes);
results.push({
type: 'pdf',
document,

View File

@@ -5,6 +5,7 @@ import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument, rgb } from 'pdf-lib';
import { hexToRgb } from '../../utils/helpers.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class NUpNode extends BaseWorkflowNode {
readonly category = 'Organize & Manage' as const;
@@ -73,7 +74,7 @@ export class NUpNode extends BaseWorkflowNode {
};
const [cols, rows] = gridDims[n];
const srcDoc = await PDFDocument.load(input.bytes);
const srcDoc = await loadPdfDocument(input.bytes);
const newDoc = await PDFDocument.create();
const pageCount = srcDoc.getPageCount();
const firstPage = srcDoc.getPages()[0];

View File

@@ -2,8 +2,8 @@ import { ClassicPreset } from 'rete';
import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { PDFData, SocketData, MultiPDFData } from '../types';
import { PDFDocument } from 'pdf-lib';
import { getLibreOfficeConverter } from '../../utils/libreoffice-loader.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class OdgToPdfNode extends BaseWorkflowNode {
readonly category = 'Input' as const;
@@ -56,7 +56,7 @@ export class OdgToPdfNode extends BaseWorkflowNode {
for (const file of this.files) {
const resultBlob = await converter.convertToPdf(file);
const bytes = new Uint8Array(await resultBlob.arrayBuffer());
const document = await PDFDocument.load(bytes);
const document = await loadPdfDocument(bytes);
results.push({
type: 'pdf',
document,

View File

@@ -8,8 +8,8 @@ import {
type PageNumberPosition,
type PageNumberFormat,
} from '../../utils/pdf-operations';
import { PDFDocument } from 'pdf-lib';
import { hexToRgb } from '../../utils/helpers.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class PageNumbersNode extends BaseWorkflowNode {
readonly category = 'Edit & Annotate' as const;
@@ -68,7 +68,7 @@ export class PageNumbersNode extends BaseWorkflowNode {
color: { r: c.r, g: c.g, b: c.b },
});
const resultDoc = await PDFDocument.load(resultBytes);
const resultDoc = await loadPdfDocument(resultBytes);
return {
type: 'pdf',

View File

@@ -2,8 +2,8 @@ import { ClassicPreset } from 'rete';
import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { PDFData, SocketData, MultiPDFData } from '../types';
import { PDFDocument } from 'pdf-lib';
import { getLibreOfficeConverter } from '../../utils/libreoffice-loader.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class PagesToPdfNode extends BaseWorkflowNode {
readonly category = 'Input' as const;
@@ -56,7 +56,7 @@ export class PagesToPdfNode extends BaseWorkflowNode {
for (const file of this.files) {
const resultBlob = await converter.convertToPdf(file);
const bytes = new Uint8Array(await resultBlob.arrayBuffer());
const document = await PDFDocument.load(bytes);
const document = await loadPdfDocument(bytes);
results.push({
type: 'pdf',
document,

View File

@@ -2,9 +2,9 @@ import { ClassicPreset } from 'rete';
import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { PDFData, SocketData, MultiPDFData } from '../types';
import { PDFDocument } from 'pdf-lib';
import { readFileAsArrayBuffer } from '../../utils/helpers.js';
import { decryptPdfBytes } from '../../utils/pdf-decrypt.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class EncryptedPDFError extends Error {
constructor(public readonly filename: string) {
@@ -31,15 +31,14 @@ export class PDFInputNode extends BaseWorkflowNode {
let isEncrypted = false;
try {
await PDFDocument.load(bytes, { throwOnInvalidObject: false });
await loadPdfDocument(bytes, { throwOnInvalidObject: false });
} catch {
isEncrypted = true;
}
if (isEncrypted) {
try {
await PDFDocument.load(bytes, {
ignoreEncryption: true,
await loadPdfDocument(bytes, {
throwOnInvalidObject: false,
});
} catch {
@@ -50,7 +49,7 @@ export class PDFInputNode extends BaseWorkflowNode {
throw new EncryptedPDFError(file.name);
}
const document = await PDFDocument.load(bytes, {
const document = await loadPdfDocument(bytes, {
throwOnInvalidObject: false,
});
this.files.push({
@@ -65,7 +64,7 @@ export class PDFInputNode extends BaseWorkflowNode {
const arrayBuffer = await readFileAsArrayBuffer(file);
const bytes = new Uint8Array(arrayBuffer as ArrayBuffer);
const { bytes: decryptedBytes } = await decryptPdfBytes(bytes, password);
const document = await PDFDocument.load(decryptedBytes, {
const document = await loadPdfDocument(decryptedBytes, {
throwOnInvalidObject: false,
});
this.files.push({

View File

@@ -43,7 +43,7 @@ export class PdfToCsvNode extends BaseWorkflowNode {
for (let i = 0; i < pageCount; i++) {
const page = doc.getPage(i);
const tables = page.findTables();
tables.forEach((table: any) => {
tables.forEach((table: { rows: (string | null)[][] }) => {
allRows.push(...table.rows);
});
}

View File

@@ -5,6 +5,7 @@ import type { SocketData, PDFData } from '../types';
import { requirePdfInput, extractAllPdfs } from '../types';
import { downloadFile } from '../../utils/helpers.js';
import * as pdfjsLib from 'pdfjs-dist';
import type JSZip from 'jszip';
import { loadPyMuPDF } from '../../utils/pymupdf-loader.js';
export class PdfToImagesNode extends BaseWorkflowNode {
@@ -31,7 +32,7 @@ export class PdfToImagesNode extends BaseWorkflowNode {
private async addPdfPages(
pdf: PDFData,
zip: any,
zip: JSZip,
format: string,
mimeType: string,
quality: number,
@@ -59,7 +60,10 @@ export class PdfToImagesNode extends BaseWorkflowNode {
}
}
private async addPdfPagesAsSvg(allPdfs: PDFData[], zip: any): Promise<void> {
private async addPdfPagesAsSvg(
allPdfs: PDFData[],
zip: JSZip
): Promise<void> {
const pymupdf = await loadPyMuPDF();
for (const pdf of allPdfs) {
const blob = new Blob([new Uint8Array(pdf.bytes)], {

View File

@@ -3,9 +3,9 @@ import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib';
import { loadGhostscript } from '../../utils/ghostscript-dynamic-loader.js';
import { loadPyMuPDF } from '../../utils/pymupdf-loader.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class PdfToPdfANode extends BaseWorkflowNode {
readonly category = 'Optimize & Repair' as const;
@@ -68,7 +68,7 @@ export class PdfToPdfANode extends BaseWorkflowNode {
);
const bytes = new Uint8Array(resultBuffer);
const document = await PDFDocument.load(bytes);
const document = await loadPdfDocument(bytes);
return {
type: 'pdf',

View File

@@ -36,7 +36,7 @@ export class PdfToXlsxNode extends BaseWorkflowNode {
for (let i = 0; i < pageCount; i++) {
const page = doc.getPage(i);
const tables = page.findTables();
tables.forEach((table: any) => {
tables.forEach((table: { rows: (string | null)[][] }) => {
allTables.push({ page: i + 1, rows: table.rows });
});
}

View File

@@ -2,8 +2,8 @@ import { ClassicPreset } from 'rete';
import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { PDFData, SocketData, MultiPDFData } from '../types';
import { PDFDocument } from 'pdf-lib';
import { getLibreOfficeConverter } from '../../utils/libreoffice-loader.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class PowerPointToPdfNode extends BaseWorkflowNode {
readonly category = 'Input' as const;
@@ -61,7 +61,7 @@ export class PowerPointToPdfNode extends BaseWorkflowNode {
for (const file of this.files) {
const resultBlob = await converter.convertToPdf(file);
const bytes = new Uint8Array(await resultBlob.arrayBuffer());
const document = await PDFDocument.load(bytes);
const document = await loadPdfDocument(bytes);
results.push({
type: 'pdf',
document,

View File

@@ -2,8 +2,8 @@ import { ClassicPreset } from 'rete';
import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { PDFData, SocketData, MultiPDFData } from '../types';
import { PDFDocument } from 'pdf-lib';
import { getLibreOfficeConverter } from '../../utils/libreoffice-loader.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class PubToPdfNode extends BaseWorkflowNode {
readonly category = 'Input' as const;
@@ -56,7 +56,7 @@ export class PubToPdfNode extends BaseWorkflowNode {
for (const file of this.files) {
const resultBlob = await converter.convertToPdf(file);
const bytes = new Uint8Array(await resultBlob.arrayBuffer());
const document = await PDFDocument.load(bytes);
const document = await loadPdfDocument(bytes);
results.push({
type: 'pdf',
document,

View File

@@ -3,8 +3,8 @@ import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib';
import { loadPyMuPDF } from '../../utils/pymupdf-loader.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class RasterizeNode extends BaseWorkflowNode {
readonly category = 'Optimize & Repair' as const;
@@ -65,7 +65,7 @@ export class RasterizeNode extends BaseWorkflowNode {
});
const bytes = new Uint8Array(await rasterizedBlob.arrayBuffer());
const document = await PDFDocument.load(bytes);
const document = await loadPdfDocument(bytes);
return {
type: 'pdf',

View File

@@ -3,9 +3,9 @@ import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib';
import { loadPyMuPDF } from '../../utils/pymupdf-loader.js';
import { hexToRgb } from '../../utils/helpers.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class RedactNode extends BaseWorkflowNode {
readonly category = 'Secure PDF' as const;
@@ -106,7 +106,7 @@ export class RedactNode extends BaseWorkflowNode {
const resultBytes = new Uint8Array(doc.save());
doc.close();
const resultDoc = await PDFDocument.load(resultBytes);
const resultDoc = await loadPdfDocument(resultBytes);
return {
type: 'pdf',

View File

@@ -3,7 +3,8 @@ import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument, PDFName } from 'pdf-lib';
import { PDFName } from 'pdf-lib';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class RemoveAnnotationsNode extends BaseWorkflowNode {
readonly category = 'Edit & Annotate' as const;
@@ -23,7 +24,7 @@ export class RemoveAnnotationsNode extends BaseWorkflowNode {
return {
pdf: await processBatch(pdfInputs, async (input) => {
const pdfDoc = await PDFDocument.load(input.bytes);
const pdfDoc = await loadPdfDocument(input.bytes);
const pages = pdfDoc.getPages();
for (const page of pages) {

View File

@@ -5,6 +5,7 @@ import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class RemoveBlankPagesNode extends BaseWorkflowNode {
readonly category = 'Edit & Annotate' as const;
@@ -61,7 +62,7 @@ export class RemoveBlankPagesNode extends BaseWorkflowNode {
pdf: await processBatch(pdfInputs, async (input) => {
const pdfjsDoc = await pdfjsLib.getDocument({ data: input.bytes })
.promise;
const srcDoc = await PDFDocument.load(input.bytes);
const srcDoc = await loadPdfDocument(input.bytes);
const nonBlankIndices: number[] = [];
for (let i = 1; i <= pdfjsDoc.numPages; i++) {

View File

@@ -3,8 +3,8 @@ import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib';
import { initializeQpdf } from '../../utils/helpers.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class RepairNode extends BaseWorkflowNode {
readonly category = 'Optimize & Repair' as const;
@@ -49,8 +49,7 @@ export class RepairNode extends BaseWorkflowNode {
}
const resultBytes = new Uint8Array(repairedData);
const resultDoc = await PDFDocument.load(resultBytes, {
ignoreEncryption: true,
const resultDoc = await loadPdfDocument(resultBytes, {
throwOnInvalidObject: false,
});

View File

@@ -4,6 +4,7 @@ import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class ReversePagesNode extends BaseWorkflowNode {
readonly category = 'Organize & Manage' as const;
@@ -23,7 +24,7 @@ export class ReversePagesNode extends BaseWorkflowNode {
return {
pdf: await processBatch(pdfInputs, async (input) => {
const srcDoc = await PDFDocument.load(input.bytes);
const srcDoc = await loadPdfDocument(input.bytes);
const pageCount = srcDoc.getPageCount();
const newDoc = await PDFDocument.create();
const reversedIndices = Array.from(

View File

@@ -4,7 +4,7 @@ import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { rotatePdfUniform } from '../../utils/pdf-operations';
import { PDFDocument } from 'pdf-lib';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class RotateNode extends BaseWorkflowNode {
readonly category = 'Organize & Manage' as const;
@@ -33,7 +33,7 @@ export class RotateNode extends BaseWorkflowNode {
return {
pdf: await processBatch(pdfInputs, async (input) => {
const resultBytes = await rotatePdfUniform(input.bytes, angle);
const resultDoc = await PDFDocument.load(resultBytes);
const resultDoc = await loadPdfDocument(resultBytes);
return {
type: 'pdf',
document: resultDoc,

View File

@@ -4,7 +4,7 @@ import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { splitPdf, parsePageRange } from '../../utils/pdf-operations';
import { PDFDocument } from 'pdf-lib';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
export class SplitNode extends BaseWorkflowNode {
readonly category = 'Organize & Manage' as const;
@@ -32,11 +32,11 @@ export class SplitNode extends BaseWorkflowNode {
return {
pdf: await processBatch(pdfInputs, async (input) => {
const srcDoc = await PDFDocument.load(input.bytes);
const srcDoc = await loadPdfDocument(input.bytes);
const totalPages = srcDoc.getPageCount();
const indices = parsePageRange(rangeStr, totalPages);
const resultBytes = await splitPdf(input.bytes, indices);
const resultDoc = await PDFDocument.load(resultBytes);
const resultDoc = await loadPdfDocument(resultBytes);
return {
type: 'pdf',
document: resultDoc,

Some files were not shown because too many files have changed in this diff Show More