Files
bentopdf/src/js/utils/pdf-decrypt.ts
2026-03-15 14:29:30 +05:30

173 lines
4.6 KiB
TypeScript

import { getCpdf, isCpdfAvailable } from './cpdf-helper';
import { isPyMuPDFAvailable, loadPyMuPDF } from './pymupdf-loader';
export type PdfDecryptEngine = 'cpdf' | 'pymupdf';
export interface PdfDecryptResult {
bytes: Uint8Array;
engine: PdfDecryptEngine;
}
const DECRYPT_LOG_PREFIX = '[PDF Decrypt]';
function getErrorMessage(error: unknown): string {
if (error instanceof Error && error.message) {
return error.message;
}
return String(error);
}
function normalizeErrorMessage(error: unknown): string {
const message = getErrorMessage(error).trim();
const cpdfReasonMatch = message.match(/Pdf\.PDFError\("([^"]+)"\)/);
if (cpdfReasonMatch?.[1]) {
return cpdfReasonMatch[1];
}
const trailingErrorMatch = message.match(/ERROR:\s*[^:]+:\s*(.+)$/);
if (trailingErrorMatch?.[1]) {
return trailingErrorMatch[1].trim();
}
return message;
}
function copyBytes(bytes: Uint8Array): Uint8Array {
return Uint8Array.from(bytes);
}
function cleanupCpdfDocument(cpdf: unknown, pdf: unknown): void {
if (!cpdf || !pdf || typeof cpdf !== 'object' || !('deletePdf' in cpdf)) {
return;
}
try {
(cpdf as { deletePdf: (document: unknown) => void }).deletePdf(pdf);
} catch (cleanupError) {
console.warn(
`${DECRYPT_LOG_PREFIX} Failed to cleanup CoherentPDF document: ${normalizeErrorMessage(cleanupError)}`
);
}
}
async function decryptWithCpdf(
inputBytes: Uint8Array,
password: string
): Promise<Uint8Array> {
const cpdf = await getCpdf();
cpdf.setSlow();
let pdf: unknown | null = null;
try {
pdf = cpdf.fromMemory(new Uint8Array(inputBytes), password);
if (cpdf.isEncrypted(pdf)) {
try {
cpdf.decryptPdf(pdf, password);
} catch {
cpdf.decryptPdfOwner(pdf, password);
}
}
const outputBytes = cpdf.toMemory(pdf, false, false);
pdf = null;
if (!(outputBytes instanceof Uint8Array) || outputBytes.length === 0) {
throw new Error('CoherentPDF produced an empty decrypted file.');
}
return copyBytes(outputBytes);
} catch (error) {
if (pdf) {
cleanupCpdfDocument(cpdf, pdf);
}
throw new Error(normalizeErrorMessage(error), { cause: error });
}
}
async function decryptWithPyMuPDF(
inputBytes: Uint8Array,
password: string
): Promise<Uint8Array> {
const pymupdf = await loadPyMuPDF();
const document = await pymupdf.open(
new Blob([new Uint8Array(inputBytes)], { type: 'application/pdf' })
);
try {
if (document.needsPass || document.isEncrypted) {
const authenticated = document.authenticate(password);
if (!authenticated) {
throw new Error('Invalid PDF password.');
}
}
const outputBytes = document.save();
if (!(outputBytes instanceof Uint8Array) || outputBytes.length === 0) {
throw new Error('PyMuPDF produced an empty decrypted file.');
}
return copyBytes(outputBytes);
} finally {
document.close();
}
}
export async function decryptPdfBytes(
inputBytes: Uint8Array,
password: string
): Promise<PdfDecryptResult> {
const errors: string[] = [];
if (isCpdfAvailable()) {
console.info(`${DECRYPT_LOG_PREFIX} Trying CoherentPDF decryption`);
try {
const result: PdfDecryptResult = {
bytes: await decryptWithCpdf(inputBytes, password),
engine: 'cpdf',
};
console.info(
`${DECRYPT_LOG_PREFIX} Decryption succeeded with CoherentPDF`
);
return result;
} catch (error) {
const errorMessage = normalizeErrorMessage(error);
console.warn(
`${DECRYPT_LOG_PREFIX} Decryption with CoherentPDF failed. Falling back to PyMuPDF. Reason: ${errorMessage}`
);
errors.push(`CoherentPDF: ${errorMessage}`);
}
} else {
console.info(
`${DECRYPT_LOG_PREFIX} CoherentPDF is not configured, skipping to PyMuPDF`
);
errors.push('CoherentPDF: not configured.');
}
if (isPyMuPDFAvailable()) {
console.info(`${DECRYPT_LOG_PREFIX} Trying PyMuPDF decryption`);
try {
const result: PdfDecryptResult = {
bytes: await decryptWithPyMuPDF(inputBytes, password),
engine: 'pymupdf',
};
console.info(`${DECRYPT_LOG_PREFIX} Decryption succeeded with PyMuPDF`);
return result;
} catch (error) {
const errorMessage = normalizeErrorMessage(error);
console.warn(
`${DECRYPT_LOG_PREFIX} PyMuPDF decryption failed: ${errorMessage}`
);
errors.push(`PyMuPDF: ${errorMessage}`);
}
} else {
console.warn(`${DECRYPT_LOG_PREFIX} PyMuPDF is not configured`);
errors.push('PyMuPDF: not configured.');
}
throw new Error(errors.join('\n'));
}