refactor pdf decrypt

This commit is contained in:
alam00000
2026-03-15 14:28:37 +05:30
parent 11368314dd
commit 47c1be6df1
5 changed files with 495 additions and 177 deletions

View File

@@ -2,9 +2,9 @@ import { showAlert } from '../ui.js';
import { import {
downloadFile, downloadFile,
formatBytes, formatBytes,
initializeQpdf,
readFileAsArrayBuffer, readFileAsArrayBuffer,
} from '../utils/helpers.js'; } from '../utils/helpers.js';
import { decryptPdfBytes } from '../utils/pdf-decrypt.js';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import JSZip from 'jszip'; import JSZip from 'jszip';
import { DecryptPdfState } from '@/types'; import { DecryptPdfState } from '@/types';
@@ -115,79 +115,39 @@ async function decryptPdf() {
const loaderModal = document.getElementById('loader-modal'); const loaderModal = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text'); const loaderText = document.getElementById('loader-text');
let qpdf: any;
try { try {
if (loaderModal) loaderModal.classList.remove('hidden'); if (loaderModal) loaderModal.classList.remove('hidden');
if (loaderText) loaderText.textContent = 'Initializing decryption...'; if (loaderText) loaderText.textContent = 'Initializing decryption...';
qpdf = await initializeQpdf();
if (pageState.files.length === 1) { if (pageState.files.length === 1) {
// Single file: decrypt and download directly // Single file: decrypt and download directly
const file = pageState.files[0]; const file = pageState.files[0];
const inputPath = '/input.pdf'; if (loaderText) loaderText.textContent = 'Reading encrypted PDF...';
const outputPath = '/output.pdf'; const fileBuffer = await readFileAsArrayBuffer(file);
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
try { if (loaderText) loaderText.textContent = 'Decrypting PDF...';
if (loaderText) loaderText.textContent = 'Reading encrypted PDF...'; const { bytes: decryptedBytes } = await decryptPdfBytes(
const fileBuffer = await readFileAsArrayBuffer(file); uint8Array,
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer); password
qpdf.FS.writeFile(inputPath, uint8Array); );
if (loaderText) loaderText.textContent = 'Decrypting PDF...'; if (loaderText) loaderText.textContent = 'Preparing download...';
const args = [ const blob = new Blob([decryptedBytes.slice().buffer], {
inputPath, type: 'application/pdf',
'--password=' + password, });
'--decrypt', downloadFile(blob, `unlocked-${file.name}`);
outputPath,
];
try { if (loaderModal) loaderModal.classList.add('hidden');
qpdf.callMain(args); showAlert(
} catch (qpdfError: any) { 'Success',
if ( 'PDF decrypted successfully! Your download has started.',
qpdfError.message?.includes('invalid password') || 'success',
qpdfError.message?.includes('password') () => {
) { resetState();
throw new Error('INVALID_PASSWORD');
}
throw qpdfError;
} }
);
if (loaderText) loaderText.textContent = 'Preparing download...';
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
if (outputFile.length === 0) {
throw new Error('Decryption resulted in an empty file.');
}
const blob = new Blob([new Uint8Array(outputFile)], {
type: 'application/pdf',
});
downloadFile(blob, `unlocked-${file.name}`);
if (loaderModal) loaderModal.classList.add('hidden');
showAlert(
'Success',
'PDF decrypted successfully! Your download has started.',
'success',
() => {
resetState();
}
);
} finally {
try {
if (qpdf?.FS) {
if (qpdf.FS.analyzePath(inputPath).exists)
qpdf.FS.unlink(inputPath);
if (qpdf.FS.analyzePath(outputPath).exists)
qpdf.FS.unlink(outputPath);
}
} catch (cleanupError) {
console.warn('Failed to cleanup WASM FS:', cleanupError);
}
}
} else { } else {
// Multiple files: decrypt all and download as ZIP // Multiple files: decrypt all and download as ZIP
const zip = new JSZip(); const zip = new JSZip();
@@ -196,8 +156,6 @@ async function decryptPdf() {
for (let i = 0; i < pageState.files.length; i++) { for (let i = 0; i < pageState.files.length; i++) {
const file = pageState.files[i]; const file = pageState.files[i];
const inputPath = `/input_${i}.pdf`;
const outputPath = `/output_${i}.pdf`;
if (loaderText) if (loaderText)
loaderText.textContent = `Decrypting ${file.name} (${i + 1}/${pageState.files.length})...`; loaderText.textContent = `Decrypting ${file.name} (${i + 1}/${pageState.files.length})...`;
@@ -205,55 +163,16 @@ async function decryptPdf() {
try { try {
const fileBuffer = await readFileAsArrayBuffer(file); const fileBuffer = await readFileAsArrayBuffer(file);
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer); const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
qpdf.FS.writeFile(inputPath, uint8Array); const { bytes: decryptedBytes } = await decryptPdfBytes(
uint8Array,
password
);
const args = [ zip.file(`unlocked-${file.name}`, decryptedBytes, { binary: true });
inputPath,
'--password=' + password,
'--decrypt',
outputPath,
];
try {
qpdf.callMain(args);
} catch (qpdfError: any) {
if (
qpdfError.message?.includes('invalid password') ||
qpdfError.message?.includes('password')
) {
throw new Error(`Invalid password for ${file.name}`);
}
throw qpdfError;
}
const outputFile = qpdf.FS.readFile(outputPath, {
encoding: 'binary',
});
if (!outputFile || outputFile.length === 0) {
throw new Error(
`Decryption resulted in an empty file for ${file.name}.`
);
}
zip.file(`unlocked-${file.name}`, outputFile, { binary: true });
successCount++; successCount++;
} catch (fileError: any) { } catch (fileError: any) {
errorCount++; errorCount++;
console.error(`Failed to decrypt ${file.name}:`, fileError); console.error(`Failed to decrypt ${file.name}:`, fileError);
} finally {
try {
if (qpdf?.FS) {
if (qpdf.FS.analyzePath(inputPath).exists)
qpdf.FS.unlink(inputPath);
if (qpdf.FS.analyzePath(outputPath).exists)
qpdf.FS.unlink(outputPath);
}
} catch (cleanupError) {
console.warn(
`Failed to cleanup WASM FS for ${file.name}:`,
cleanupError
);
}
} }
} }

172
src/js/utils/pdf-decrypt.ts Normal file
View File

@@ -0,0 +1,172 @@
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'));
}

View File

@@ -4,7 +4,7 @@ import { pdfSocket } from '../sockets';
import type { SocketData } from '../types'; import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types'; import { requirePdfInput, processBatch } from '../types';
import { PDFDocument } from 'pdf-lib'; import { PDFDocument } from 'pdf-lib';
import { initializeQpdf } from '../../utils/helpers.js'; import { decryptPdfBytes } from '../../utils/pdf-decrypt.js';
export class DecryptNode extends BaseWorkflowNode { export class DecryptNode extends BaseWorkflowNode {
readonly category = 'Secure PDF' as const; readonly category = 'Secure PDF' as const;
@@ -33,35 +33,13 @@ export class DecryptNode extends BaseWorkflowNode {
return { return {
pdf: await processBatch(pdfInputs, async (input) => { pdf: await processBatch(pdfInputs, async (input) => {
const qpdf = await initializeQpdf(); const { bytes: resultBytes } = await decryptPdfBytes(
const uid = `${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; input.bytes,
const inputPath = `/tmp/input_decrypt_${uid}.pdf`; password
const outputPath = `/tmp/output_decrypt_${uid}.pdf`; );
const document = await PDFDocument.load(resultBytes, {
let decryptedData: Uint8Array; throwOnInvalidObject: false,
try { });
qpdf.FS.writeFile(inputPath, input.bytes);
const args = password
? [inputPath, '--password=' + password, '--decrypt', outputPath]
: [inputPath, '--decrypt', outputPath];
qpdf.callMain(args);
decryptedData = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
} finally {
try {
qpdf.FS.unlink(inputPath);
} catch {
/* cleanup */
}
try {
qpdf.FS.unlink(outputPath);
} catch {
/* cleanup */
}
}
const resultBytes = new Uint8Array(decryptedData);
const document = await PDFDocument.load(resultBytes);
return { return {
type: 'pdf', type: 'pdf',

View File

@@ -3,7 +3,8 @@ import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets'; import { pdfSocket } from '../sockets';
import type { PDFData, SocketData, MultiPDFData } from '../types'; import type { PDFData, SocketData, MultiPDFData } from '../types';
import { PDFDocument } from 'pdf-lib'; import { PDFDocument } from 'pdf-lib';
import { readFileAsArrayBuffer, initializeQpdf } from '../../utils/helpers.js'; import { readFileAsArrayBuffer } from '../../utils/helpers.js';
import { decryptPdfBytes } from '../../utils/pdf-decrypt.js';
export class EncryptedPDFError extends Error { export class EncryptedPDFError extends Error {
constructor(public readonly filename: string) { constructor(public readonly filename: string) {
@@ -63,44 +64,16 @@ export class PDFInputNode extends BaseWorkflowNode {
async addDecryptedFile(file: File, password: string): Promise<void> { async addDecryptedFile(file: File, password: string): Promise<void> {
const arrayBuffer = await readFileAsArrayBuffer(file); const arrayBuffer = await readFileAsArrayBuffer(file);
const bytes = new Uint8Array(arrayBuffer as ArrayBuffer); const bytes = new Uint8Array(arrayBuffer as ArrayBuffer);
const qpdf = await initializeQpdf(); const { bytes: decryptedBytes } = await decryptPdfBytes(bytes, password);
const uid = `${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; const document = await PDFDocument.load(decryptedBytes, {
const inputPath = `/tmp/input_decrypt_${uid}.pdf`; throwOnInvalidObject: false,
const outputPath = `/tmp/output_decrypt_${uid}.pdf`; });
this.files.push({
try { type: 'pdf',
qpdf.FS.writeFile(inputPath, bytes); document,
qpdf.callMain([ bytes: decryptedBytes,
inputPath, filename: file.name,
'--password=' + password, });
'--decrypt',
outputPath,
]);
const decryptedData = qpdf.FS.readFile(outputPath, {
encoding: 'binary',
});
const decryptedBytes = new Uint8Array(decryptedData);
const document = await PDFDocument.load(decryptedBytes, {
throwOnInvalidObject: false,
});
this.files.push({
type: 'pdf',
document,
bytes: decryptedBytes,
filename: file.name,
});
} finally {
try {
qpdf.FS.unlink(inputPath);
} catch {
/* cleanup */
}
try {
qpdf.FS.unlink(outputPath);
} catch {
/* cleanup */
}
}
} }
async setFile(file: File): Promise<void> { async setFile(file: File): Promise<void> {

View File

@@ -0,0 +1,276 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const {
mockGetCpdf,
mockIsCpdfAvailable,
mockLoadPyMuPDF,
mockIsPyMuPDFAvailable,
mockPdfDocumentLoad,
mockReadFileAsArrayBuffer,
} = vi.hoisted(() => ({
mockGetCpdf: vi.fn(),
mockIsCpdfAvailable: vi.fn(),
mockLoadPyMuPDF: vi.fn(),
mockIsPyMuPDFAvailable: vi.fn(),
mockPdfDocumentLoad: vi.fn(),
mockReadFileAsArrayBuffer: vi.fn(),
}));
vi.mock('../js/utils/cpdf-helper', () => ({
getCpdf: mockGetCpdf,
isCpdfAvailable: mockIsCpdfAvailable,
}));
vi.mock('../js/utils/pymupdf-loader', () => ({
loadPyMuPDF: mockLoadPyMuPDF,
isPyMuPDFAvailable: mockIsPyMuPDFAvailable,
}));
vi.mock('pdf-lib', () => ({
PDFDocument: {
load: mockPdfDocumentLoad,
},
}));
vi.mock('../js/utils/helpers.js', () => ({
readFileAsArrayBuffer: mockReadFileAsArrayBuffer,
}));
import * as pdfDecryptModule from '../js/utils/pdf-decrypt';
describe('pdf decrypt', () => {
let infoSpy: ReturnType<typeof vi.spyOn>;
let warnSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
vi.clearAllMocks();
infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
mockIsCpdfAvailable.mockReturnValue(true);
mockIsPyMuPDFAvailable.mockReturnValue(true);
mockPdfDocumentLoad.mockResolvedValue({ loaded: true });
mockReadFileAsArrayBuffer.mockResolvedValue(
new Uint8Array([1, 2, 3]).buffer
);
});
it('uses CoherentPDF first when it succeeds', async () => {
const cpdf = {
setSlow: vi.fn(),
fromMemory: vi.fn().mockReturnValue({ pdf: true }),
isEncrypted: vi.fn().mockReturnValue(true),
decryptPdf: vi.fn(),
decryptPdfOwner: vi.fn(),
toMemory: vi.fn().mockReturnValue(new Uint8Array([4, 5, 6])),
deletePdf: vi.fn(),
};
mockGetCpdf.mockResolvedValue(cpdf);
const result = await pdfDecryptModule.decryptPdfBytes(
new Uint8Array([9]),
'1234'
);
expect(result).toEqual({
bytes: new Uint8Array([4, 5, 6]),
engine: 'cpdf',
});
expect(infoSpy).toHaveBeenCalledWith(
'[PDF Decrypt] Decryption succeeded with CoherentPDF'
);
expect(cpdf.decryptPdf).toHaveBeenCalledWith({ pdf: true }, '1234');
expect(mockLoadPyMuPDF).not.toHaveBeenCalled();
});
it('falls back to PyMuPDF when CoherentPDF fails', async () => {
const cpdf = {
setSlow: vi.fn(),
fromMemory: vi.fn().mockImplementation(() => {
throw new Error('cpdf failed');
}),
isEncrypted: vi.fn(),
decryptPdf: vi.fn(),
decryptPdfOwner: vi.fn(),
toMemory: vi.fn(),
deletePdf: vi.fn(),
};
const pymupdfDocument = {
needsPass: true,
isEncrypted: true,
authenticate: vi.fn().mockReturnValue(true),
save: vi.fn().mockReturnValue(new Uint8Array([7, 8, 9])),
close: vi.fn(),
};
mockGetCpdf.mockResolvedValue(cpdf);
mockLoadPyMuPDF.mockResolvedValue({
open: vi.fn().mockResolvedValue(pymupdfDocument),
});
const result = await pdfDecryptModule.decryptPdfBytes(
new Uint8Array([9]),
'1234'
);
expect(result).toEqual({
bytes: new Uint8Array([7, 8, 9]),
engine: 'pymupdf',
});
expect(warnSpy).toHaveBeenCalledWith(
'[PDF Decrypt] Decryption with CoherentPDF failed. Falling back to PyMuPDF. Reason: cpdf failed'
);
expect(infoSpy).toHaveBeenCalledWith(
'[PDF Decrypt] Decryption succeeded with PyMuPDF'
);
expect(pymupdfDocument.authenticate).toHaveBeenCalledWith('1234');
expect(pymupdfDocument.close).toHaveBeenCalledTimes(1);
});
it('normalizes raw CoherentPDF error strings before fallback logging', async () => {
const cpdf = {
setSlow: vi.fn(),
fromMemory: vi.fn().mockImplementation(() => {
throw '0,248,Exports.CPDFError,32,ERROR: decryptPdfOwner: Pdf.PDFError("Bad or missing /P entry")';
}),
isEncrypted: vi.fn(),
decryptPdf: vi.fn(),
decryptPdfOwner: vi.fn(),
toMemory: vi.fn(),
deletePdf: vi.fn(),
};
const pymupdfDocument = {
needsPass: false,
isEncrypted: false,
authenticate: vi.fn(),
save: vi.fn().mockReturnValue(new Uint8Array([7, 8, 9])),
close: vi.fn(),
};
mockGetCpdf.mockResolvedValue(cpdf);
mockLoadPyMuPDF.mockResolvedValue({
open: vi.fn().mockResolvedValue(pymupdfDocument),
});
const result = await pdfDecryptModule.decryptPdfBytes(
new Uint8Array([9]),
'1234'
);
expect(result.engine).toBe('pymupdf');
expect(warnSpy).toHaveBeenCalledWith(
'[PDF Decrypt] Decryption with CoherentPDF failed. Falling back to PyMuPDF. Reason: Bad or missing /P entry'
);
});
it('aggregates engine errors when decryption fails', async () => {
const cpdf = {
setSlow: vi.fn(),
fromMemory: vi.fn().mockImplementation(() => {
throw new Error('cpdf failed');
}),
isEncrypted: vi.fn(),
decryptPdf: vi.fn(),
decryptPdfOwner: vi.fn(),
toMemory: vi.fn(),
deletePdf: vi.fn(),
};
const pymupdfDocument = {
needsPass: true,
isEncrypted: true,
authenticate: vi.fn().mockReturnValue(false),
save: vi.fn(),
close: vi.fn(),
};
mockGetCpdf.mockResolvedValue(cpdf);
mockLoadPyMuPDF.mockResolvedValue({
open: vi.fn().mockResolvedValue(pymupdfDocument),
});
await expect(
pdfDecryptModule.decryptPdfBytes(new Uint8Array([9]), '1234')
).rejects.toThrow(
'CoherentPDF: cpdf failed\nPyMuPDF: Invalid PDF password.'
);
expect(warnSpy).toHaveBeenCalledWith(
'[PDF Decrypt] PyMuPDF decryption failed: Invalid PDF password.'
);
expect(pymupdfDocument.close).toHaveBeenCalledTimes(1);
});
it('uses the shared decrypt helper in the workflow decrypt node', async () => {
const decryptSpy = vi
.spyOn(pdfDecryptModule, 'decryptPdfBytes')
.mockResolvedValue({
bytes: new Uint8Array([8, 8, 8]),
engine: 'cpdf',
});
const { DecryptNode } = await import('../js/workflow/nodes/decrypt-node');
const node = new DecryptNode();
const passwordControl = node.controls['password'] as { value?: string };
passwordControl.value = 'secret';
const result = await node.data({
pdf: [
{
type: 'pdf',
document: {} as never,
bytes: new Uint8Array([1, 2, 3]),
filename: 'locked.pdf',
},
],
});
expect(decryptSpy).toHaveBeenCalledWith(
new Uint8Array([1, 2, 3]),
'secret'
);
expect(mockPdfDocumentLoad).toHaveBeenCalledWith(
new Uint8Array([8, 8, 8]),
{
throwOnInvalidObject: false,
}
);
expect(result.pdf).toEqual({
type: 'pdf',
document: { loaded: true },
bytes: new Uint8Array([8, 8, 8]),
filename: 'locked_decrypted.pdf',
});
});
it('uses the shared decrypt helper when adding a decrypted workflow input file', async () => {
const decryptSpy = vi
.spyOn(pdfDecryptModule, 'decryptPdfBytes')
.mockResolvedValue({
bytes: new Uint8Array([6, 6, 6]),
engine: 'cpdf',
});
const { PDFInputNode } =
await import('../js/workflow/nodes/pdf-input-node');
const node = new PDFInputNode();
const file = new File([new Uint8Array([1, 2, 3])], 'locked.pdf', {
type: 'application/pdf',
});
await node.addDecryptedFile(file, 'secret');
expect(mockReadFileAsArrayBuffer).toHaveBeenCalledWith(file);
expect(decryptSpy).toHaveBeenCalledWith(
new Uint8Array([1, 2, 3]),
'secret'
);
expect(mockPdfDocumentLoad).toHaveBeenCalledWith(
new Uint8Array([6, 6, 6]),
{
throwOnInvalidObject: false,
}
);
expect(node.getFileCount()).toBe(1);
expect(node.getFilenames()).toEqual(['locked.pdf']);
});
});