mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-08 16:57:14 +01:00
213 lines
5.9 KiB
TypeScript
213 lines
5.9 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { readFileSync } from 'fs';
|
|
import { resolve } from 'path';
|
|
import {
|
|
PDFArray,
|
|
PDFDict,
|
|
PDFDocument,
|
|
PDFName,
|
|
PDFRawStream,
|
|
decodePDFRawStream,
|
|
} from 'pdf-lib';
|
|
|
|
vi.mock('../js/utils/helpers.js', async () => {
|
|
const { default: createModule } = await import('@neslinesli93/qpdf-wasm');
|
|
const wasmPath = resolve(
|
|
'node_modules/@neslinesli93/qpdf-wasm/dist/qpdf.wasm'
|
|
);
|
|
const options = {
|
|
locateFile: () => wasmPath,
|
|
wasmBinary: readFileSync(wasmPath),
|
|
print: () => {},
|
|
printErr: () => {},
|
|
};
|
|
const qpdf = await createModule(options);
|
|
return { initializeQpdf: async () => qpdf };
|
|
});
|
|
|
|
vi.mock('rete', () => ({
|
|
ClassicPreset: {
|
|
Input: class {},
|
|
Output: class {},
|
|
},
|
|
}));
|
|
|
|
vi.mock('@/js/workflow/sockets', () => ({ pdfSocket: {} }));
|
|
|
|
vi.mock('@/js/workflow/nodes/base-node', () => ({
|
|
BaseWorkflowNode: class {
|
|
addInput() {}
|
|
addOutput() {}
|
|
},
|
|
}));
|
|
|
|
const { flattenAnnotations, flattenFormsInDoc } =
|
|
await import('../js/utils/flatten-annotations.js');
|
|
const { removeUnreferencedObjects } =
|
|
await import('../js/utils/load-pdf-document.js');
|
|
const { sanitizePdf } = await import('../js/utils/sanitize.js');
|
|
const { FlattenNode } = await import('../js/workflow/nodes/flatten-node.js');
|
|
|
|
const SECRETS = [
|
|
'HIDDEN-VALUE',
|
|
'NOVIEW-VALUE',
|
|
'PASSWORD-SECRET',
|
|
'GROUP-SECRET',
|
|
];
|
|
|
|
async function buildFormDocument(): Promise<Uint8Array> {
|
|
const doc = await PDFDocument.create();
|
|
const page = doc.addPage([400, 400]);
|
|
const form = doc.getForm();
|
|
let y = 10;
|
|
const addField = (name: string, value: string, flags?: number) => {
|
|
const field = form.createTextField(name);
|
|
field.setText(value);
|
|
field.addToPage(page, { x: 10, y, width: 200, height: 20 });
|
|
y += 30;
|
|
if (flags !== undefined) {
|
|
const widgets = field.acroField.getWidgets();
|
|
widgets[widgets.length - 1].setFlags(flags);
|
|
}
|
|
return field;
|
|
};
|
|
|
|
addField('visible', 'VISIBLE-VALUE');
|
|
addField('hiddenField', 'HIDDEN-VALUE', 2);
|
|
addField('noViewField', 'NOVIEW-VALUE', 32 | 4);
|
|
addField('group.secret', 'GROUP-SECRET', 2);
|
|
|
|
const password = addField('pw', 'PASSWORD-SECRET');
|
|
password.enablePassword();
|
|
password.acroField.getWidgets()[0].dict.delete(PDFName.of('AP'));
|
|
|
|
const multi = addField('multi', 'MULTI-VALUE');
|
|
multi.addToPage(page, { x: 250, y: 10, width: 120, height: 20 });
|
|
multi.acroField.getWidgets()[1].setFlags(2);
|
|
|
|
const ctx = doc.context;
|
|
const appearance = (text: string) =>
|
|
ctx.register(
|
|
ctx.flateStream(`BT /Helv 10 Tf (${text}) Tj ET`, {
|
|
Type: 'XObject',
|
|
Subtype: 'Form',
|
|
BBox: [0, 0, 100, 20],
|
|
})
|
|
);
|
|
const annots = page.node.Annots() as PDFArray;
|
|
annots.push(
|
|
ctx.register(
|
|
ctx.obj({
|
|
Type: 'Annot',
|
|
Subtype: 'Square',
|
|
Rect: [10, 300, 110, 320],
|
|
F: 32 | 4,
|
|
AP: { N: appearance('NOVIEW-ANNOT') },
|
|
})
|
|
)
|
|
);
|
|
annots.push(
|
|
ctx.register(
|
|
ctx.obj({
|
|
Type: 'Annot',
|
|
Subtype: 'Square',
|
|
Rect: [10, 330, 110, 350],
|
|
AP: { N: appearance('SHOWN-ANNOT') },
|
|
})
|
|
)
|
|
);
|
|
|
|
return doc.save();
|
|
}
|
|
|
|
function hex(text: string): string {
|
|
return Buffer.from(text).toString('hex');
|
|
}
|
|
|
|
async function inspect(bytes: Uint8Array) {
|
|
const doc = await PDFDocument.load(bytes, { updateMetadata: false });
|
|
const streams: string[] = [];
|
|
for (const [, object] of doc.context.enumerateIndirectObjects()) {
|
|
if (object instanceof PDFRawStream) {
|
|
streams.push(
|
|
Buffer.from(decodePDFRawStream(object).decode()).toString('latin1')
|
|
);
|
|
}
|
|
}
|
|
const text = [Buffer.from(bytes).toString('latin1'), ...streams]
|
|
.join('\n')
|
|
.toLowerCase();
|
|
const count = (value: string) =>
|
|
text.split(value.toLowerCase()).length -
|
|
1 +
|
|
text.split(hex(value)).length -
|
|
1;
|
|
return { doc, count };
|
|
}
|
|
|
|
async function expectHiddenFieldsGone(bytes: Uint8Array) {
|
|
const { doc, count } = await inspect(bytes);
|
|
for (const secret of SECRETS) expect(count(secret)).toBe(0);
|
|
expect(count('VISIBLE-VALUE')).toBe(1);
|
|
expect(count('MULTI-VALUE')).toBe(1);
|
|
const acroForm = doc.catalog.lookup(PDFName.of('AcroForm'));
|
|
const fields =
|
|
acroForm instanceof PDFDict
|
|
? acroForm.lookup(PDFName.of('Fields'), PDFArray)
|
|
: undefined;
|
|
expect(fields?.size() ?? 0).toBe(0);
|
|
return count;
|
|
}
|
|
|
|
describe('flattenFormsInDoc', () => {
|
|
it('does not draw hidden, no-view or password fields', async () => {
|
|
const input = await buildFormDocument();
|
|
const before = await inspect(input);
|
|
for (const secret of SECRETS)
|
|
expect(before.count(secret)).toBeGreaterThan(0);
|
|
|
|
const doc = await PDFDocument.load(input);
|
|
flattenFormsInDoc(doc);
|
|
flattenAnnotations(doc);
|
|
const bytes = await removeUnreferencedObjects(await doc.save());
|
|
|
|
const count = await expectHiddenFieldsGone(bytes);
|
|
expect(count('NOVIEW-ANNOT')).toBe(0);
|
|
expect(count('SHOWN-ANNOT')).toBe(1);
|
|
});
|
|
|
|
it('is used by the sanitize flatten option', async () => {
|
|
const { bytes } = await sanitizePdf(await buildFormDocument(), {
|
|
flattenForms: true,
|
|
removeMetadata: false,
|
|
removeAnnotations: false,
|
|
removeJavascript: false,
|
|
removeEmbeddedFiles: false,
|
|
removeLayers: false,
|
|
removeLinks: false,
|
|
removeStructureTree: false,
|
|
removeMarkInfo: false,
|
|
removeFonts: false,
|
|
});
|
|
await expectHiddenFieldsGone(bytes);
|
|
});
|
|
|
|
it('is used by the workflow flatten node', async () => {
|
|
const node = new FlattenNode();
|
|
const result = await node.data({
|
|
pdf: [
|
|
{
|
|
type: 'pdf',
|
|
document: await PDFDocument.create(),
|
|
bytes: await buildFormDocument(),
|
|
filename: 'form.pdf',
|
|
},
|
|
],
|
|
});
|
|
const output = result.pdf;
|
|
if (output.type !== 'pdf') throw new Error('Expected a single PDF');
|
|
const count = await expectHiddenFieldsGone(output.bytes);
|
|
expect(count('NOVIEW-ANNOT')).toBe(0);
|
|
});
|
|
});
|