@pdfme/pdf-lib
Version:
Create and modify PDF files with JavaScript
271 lines β’ 15 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const index_1 = require("../../../src/index");
const getWidgets = (pdfDoc) => pdfDoc.context
.enumerateIndirectObjects()
.map(([, obj]) => obj)
.filter((obj) => obj instanceof index_1.PDFDict &&
obj.get(index_1.PDFName.of('Type')) === index_1.PDFName.of('Annot') &&
obj.get(index_1.PDFName.of('Subtype')) === index_1.PDFName.of('Widget'))
.map((obj) => obj);
const getRefs = (pdfDoc) => pdfDoc.context.enumerateIndirectObjects().map(([ref]) => ref);
const getApRefs = (widget) => {
const onValue = widget.getOnValue() ?? index_1.PDFName.of('Yes');
const aps = widget.getAppearances();
return [
(aps?.normal).get(onValue),
aps?.rollover?.get(onValue),
aps?.down?.get(onValue),
(aps?.normal).get(index_1.PDFName.of('Off')),
aps?.rollover?.get(index_1.PDFName.of('Off')),
aps?.down?.get(index_1.PDFName.of('Off')),
].filter(Boolean);
};
const flatten = (arr) => arr.reduce((curr, acc) => [...acc, ...curr], []);
const fancyFieldsPdfBytes = fs_1.default.readFileSync('assets/pdfs/fancy_fields.pdf');
// const sampleFormPdfBytes = fs.readFileSync('assets/pdfs/sample_form.pdf');
// const combedPdfBytes = fs.readFileSync('assets/pdfs/with_combed_fields.pdf');
// const dodPdfBytes = fs.readFileSync('assets/pdfs/dod_character.pdf');
const xfaPdfBytes = fs_1.default.readFileSync('assets/pdfs/with_xfa_fields.pdf');
const signaturePdfBytes = fs_1.default.readFileSync('assets/pdfs/with_signature.pdf');
describe(`PDFForm`, () => {
const origConsoleWarn = console.warn;
beforeAll(() => {
const ignoredWarnings = [
'Removing XFA form data as pdf-lib does not support reading or writing XFA',
];
console.warn = jest.fn((...args) => {
const isIgnored = ignoredWarnings.find((iw) => args[0].includes(iw));
if (!isIgnored)
origConsoleWarn(...args);
});
});
beforeEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
console.warn = origConsoleWarn;
});
// prettier-ignore
it(`provides access to all terminal fields in an AcroForm`, async () => {
const pdfDoc = await index_1.PDFDocument.load(fancyFieldsPdfBytes);
const form = pdfDoc.getForm();
const fields = form.getFields();
expect(fields.length).toBe(15);
expect(form.getField('Prefix β½οΈ')).toBeInstanceOf(index_1.PDFTextField);
expect(form.getField('First Name π')).toBeInstanceOf(index_1.PDFTextField);
expect(form.getField('MiddleInitial π³')).toBeInstanceOf(index_1.PDFTextField);
expect(form.getField('LastName π©')).toBeInstanceOf(index_1.PDFTextField);
expect(form.getField('Are You A Fairy? πΏ')).toBeInstanceOf(index_1.PDFCheckBox);
expect(form.getField('Is Your Power Level Over 9000? πͺ')).toBeInstanceOf(index_1.PDFCheckBox);
expect(form.getField('Can You Defeat Enemies In One Punch? π')).toBeInstanceOf(index_1.PDFCheckBox);
expect(form.getField('Will You Ever Let Me Down? βοΈ')).toBeInstanceOf(index_1.PDFCheckBox);
expect(form.getField('Eject πΌ')).toBeInstanceOf(index_1.PDFButton);
expect(form.getField('Submit π')).toBeInstanceOf(index_1.PDFButton);
expect(form.getField('Play βΆοΈ')).toBeInstanceOf(index_1.PDFButton);
expect(form.getField('Launch π')).toBeInstanceOf(index_1.PDFButton);
expect(form.getField('Historical Figures πΊ')).toBeInstanceOf(index_1.PDFRadioGroup);
expect(form.getField('Which Are Planets? π')).toBeInstanceOf(index_1.PDFOptionList);
expect(form.getField('Choose A Gundam π€')).toBeInstanceOf(index_1.PDFDropdown);
const fieldDicts = fields.map(f => f.acroField.dict);
const getFieldDict = (name) => form.getField(name)?.acroField.dict;
expect(fieldDicts).toContain(getFieldDict('Prefix β½οΈ'));
expect(fieldDicts).toContain(getFieldDict('First Name π'));
expect(fieldDicts).toContain(getFieldDict('MiddleInitial π³'));
expect(fieldDicts).toContain(getFieldDict('LastName π©'));
expect(fieldDicts).toContain(getFieldDict('Are You A Fairy? πΏ'));
expect(fieldDicts).toContain(getFieldDict('Is Your Power Level Over 9000? πͺ'));
expect(fieldDicts).toContain(getFieldDict('Can You Defeat Enemies In One Punch? π'));
expect(fieldDicts).toContain(getFieldDict('Will You Ever Let Me Down? βοΈ'));
expect(fieldDicts).toContain(getFieldDict('Eject πΌ'));
expect(fieldDicts).toContain(getFieldDict('Submit π'));
expect(fieldDicts).toContain(getFieldDict('Play βΆοΈ'));
expect(fieldDicts).toContain(getFieldDict('Launch π'));
expect(fieldDicts).toContain(getFieldDict('Historical Figures πΊ'));
expect(fieldDicts).toContain(getFieldDict('Which Are Planets? π'));
expect(fieldDicts).toContain(getFieldDict('Choose A Gundam π€'));
});
// Need to also run this test with assets/pdfs/with_xfa_fields.pdf as it has "partial/50%" APs for checkboxes (is only missing the /Off APs)
it(`does not override existing appearance streams for check boxes and radio groups if they already exist`, async () => {
const pdfDoc = await index_1.PDFDocument.load(fancyFieldsPdfBytes);
const form = pdfDoc.getForm();
// Get fields
const cb1 = form.getCheckBox('Are You A Fairy? πΏ');
const cb2 = form.getCheckBox('Is Your Power Level Over 9000? πͺ');
const cb3 = form.getCheckBox('Can You Defeat Enemies In One Punch? π');
const cb4 = form.getCheckBox('Will You Ever Let Me Down? βοΈ');
const rg1 = form.getRadioGroup('Historical Figures πΊ');
// Assert preconditions
expect(cb1.isChecked()).toBe(true);
expect(cb2.isChecked()).toBe(false);
expect(cb3.isChecked()).toBe(true);
expect(cb4.isChecked()).toBe(false);
expect(rg1.getSelected()).toEqual('Marcus Aurelius ποΈ');
// Collect all existing appearance streams
const fields = [cb1, cb2, cb3, cb4, rg1];
const widgets = flatten(fields.map((f) => f.acroField.getWidgets()));
const originalAps = flatten(widgets.map(getApRefs));
// (1) Run appearance update
form.updateFieldAppearances();
// (1) Make sure no new appearance streams were created
expect(flatten(widgets.map(getApRefs))).toEqual(originalAps);
// (2) Flip check box values
cb1.uncheck();
cb2.check();
cb3.uncheck();
cb4.check();
// (2) un appearance update
form.updateFieldAppearances();
// (2) Make sure no new appearance streams were created
expect(flatten(widgets.map(getApRefs))).toEqual(originalAps);
// (3) Change radio group value
rg1.select('Alexander Hamilton πΊπΈ');
// (3) Run appearance update
form.updateFieldAppearances();
// (3) Make sure no new appearance streams were created
expect(flatten(widgets.map(getApRefs))).toEqual(originalAps);
});
it(`creates appearance streams for widgets that do not have any`, async () => {
const pdfDoc = await index_1.PDFDocument.create();
const page = pdfDoc.addPage();
const form = pdfDoc.getForm();
const btn = form.createButton('a.button.field');
const cb = form.createCheckBox('a.checkbox.field');
const dd = form.createDropdown('a.dropdown.field');
const ol = form.createOptionList('a.optionlist.field');
const tf = form.createTextField('a.text.field');
// Skipping Radio Groups for this test as they _must_ have APs or else the
// value represented by each radio button is undefined.
// const rg = form.createRadioGroup('a.radiogroup.field');
btn.addToPage('foo', page);
cb.addToPage(page);
dd.addToPage(page);
ol.addToPage(page);
tf.addToPage(page);
// rg.addOptionToPage('bar', page);
const widgets = getWidgets(pdfDoc);
expect(widgets.length).toBe(5);
const aps = () => widgets.filter((w) => w.has(index_1.PDFName.of('AP'))).length;
expect(aps()).toBe(5);
widgets.forEach((w) => w.delete(index_1.PDFName.of('AP')));
expect(aps()).toBe(0);
form.updateFieldAppearances();
expect(aps()).toBe(5);
});
it(`removes XFA entries when it is accessed`, async () => {
const pdfDoc = await index_1.PDFDocument.load(xfaPdfBytes);
const acroForm = pdfDoc.catalog.getOrCreateAcroForm();
expect(acroForm.dict.has(index_1.PDFName.of('XFA'))).toBe(true);
expect(pdfDoc.getForm()).toBeInstanceOf(index_1.PDFForm);
expect(acroForm.dict.has(index_1.PDFName.of('XFA'))).toBe(false);
});
it(`is only created if it is accessed`, async () => {
const pdfDoc = await index_1.PDFDocument.create();
expect(pdfDoc.catalog.getAcroForm()).toBe(undefined);
expect(pdfDoc.getForm()).toBeInstanceOf(index_1.PDFForm);
expect(pdfDoc.catalog.getAcroForm()).toBeInstanceOf(index_1.PDFAcroForm);
});
it(`does not update appearance streams if "updateFieldAppearances" is true, but no fields are dirty`, async () => {
const pdfDoc = await index_1.PDFDocument.load(fancyFieldsPdfBytes);
const widgets = getWidgets(pdfDoc);
expect(widgets.length).toBe(24);
const aps = () => widgets.filter((w) => w.has(index_1.PDFName.of('AP'))).length;
expect(aps()).toBe(24);
widgets.forEach((w) => w.delete(index_1.PDFName.of('AP')));
expect(aps()).toBe(0);
await pdfDoc.save({ updateFieldAppearances: true });
expect(aps()).toBe(0);
});
it(`does not update appearance streams if "updateFieldAppearances" is false, even if fields are dirty`, async () => {
const pdfDoc = await index_1.PDFDocument.load(fancyFieldsPdfBytes);
const widgets = getWidgets(pdfDoc);
expect(widgets.length).toBe(24);
const aps = () => widgets.filter((w) => w.has(index_1.PDFName.of('AP'))).length;
expect(aps()).toBe(24);
widgets.forEach((w) => w.delete(index_1.PDFName.of('AP')));
expect(aps()).toBe(0);
const form = pdfDoc.getForm();
form.getFields().forEach((f) => form.markFieldAsDirty(f.ref));
await pdfDoc.save({ updateFieldAppearances: false });
expect(aps()).toBe(0);
});
it(`does update appearance streams if "updateFieldAppearances" is true, and fields are dirty`, async () => {
const pdfDoc = await index_1.PDFDocument.load(fancyFieldsPdfBytes);
const widgets = getWidgets(pdfDoc);
expect(widgets.length).toBe(24);
const aps = () => widgets.filter((w) => w.has(index_1.PDFName.of('AP'))).length;
expect(aps()).toBe(24);
widgets.forEach((w) => w.delete(index_1.PDFName.of('AP')));
expect(aps()).toBe(0);
const form = pdfDoc.getForm();
form.getFields().forEach((f) => form.markFieldAsDirty(f.ref));
await pdfDoc.save({ updateFieldAppearances: true });
expect(aps()).toBe(20);
});
it(`does not throw errors for PDFSignature fields`, async () => {
const pdfDoc = await index_1.PDFDocument.load(signaturePdfBytes);
const widgets = getWidgets(pdfDoc);
expect(widgets.length).toBe(1);
const form = pdfDoc.getForm();
expect(() => form.updateFieldAppearances()).not.toThrow();
expect(pdfDoc.save({ updateFieldAppearances: true })).resolves.toBeInstanceOf(Uint8Array);
});
it(`it cleans references of removed fields and their widgets`, async () => {
const pdfDoc = await index_1.PDFDocument.load(fancyFieldsPdfBytes);
const form = pdfDoc.getForm();
const refs1 = getRefs(pdfDoc);
const cb = form.getCheckBox('Will You Ever Let Me Down? βοΈ');
const rg = form.getRadioGroup('Historical Figures πΊ');
const cbWidgetRefs = cb.acroField.normalizedEntries().Kids.asArray();
const rgWidgetRefs = cb.acroField.normalizedEntries().Kids.asArray();
expect(cbWidgetRefs.length).toBeGreaterThan(0);
expect(rgWidgetRefs.length).toBeGreaterThan(0);
// Assert that refs are present before their fields have been removed
expect(refs1.includes(cb.ref)).toBe(true);
expect(refs1.includes(rg.ref)).toBe(true);
cbWidgetRefs.forEach((ref) => expect(refs1).toContain(ref));
rgWidgetRefs.forEach((ref) => expect(refs1).toContain(ref));
form.removeField(cb);
form.removeField(rg);
const refs2 = getRefs(pdfDoc);
// Assert that refs are not present after their fields have been removed
expect(refs2.includes(cb.ref)).toBe(false);
expect(refs2.includes(rg.ref)).toBe(false);
cbWidgetRefs.forEach((ref) => expect(refs2).not.toContain(ref));
rgWidgetRefs.forEach((ref) => expect(refs2).not.toContain(ref));
});
it(`it cleans references of removed fields and their widgets when created with pdf-lib`, async () => {
const pdfDoc = await index_1.PDFDocument.create();
const page = pdfDoc.addPage();
const form = pdfDoc.getForm();
const cb = form.createCheckBox('a.new.check.box');
const tf = form.createTextField('a.new.text.field');
cb.addToPage(page);
tf.addToPage(page);
const refs1 = getRefs(pdfDoc);
const cbWidgetRefs = cb.acroField.normalizedEntries().Kids.asArray();
const tfWidgetRefs = cb.acroField.normalizedEntries().Kids.asArray();
expect(cbWidgetRefs.length).toBeGreaterThan(0);
expect(tfWidgetRefs.length).toBeGreaterThan(0);
// Assert that refs are present before their fields have been removed
expect(refs1.includes(cb.ref)).toBe(true);
expect(refs1.includes(tf.ref)).toBe(true);
cbWidgetRefs.forEach((ref) => expect(refs1).toContain(ref));
tfWidgetRefs.forEach((ref) => expect(refs1).toContain(ref));
form.removeField(cb);
form.removeField(tf);
const refs2 = getRefs(pdfDoc);
// Assert that refs are not present after their fields have been removed
expect(refs2.includes(cb.ref)).toBe(false);
expect(refs2.includes(tf.ref)).toBe(false);
cbWidgetRefs.forEach((ref) => expect(refs2).not.toContain(ref));
tfWidgetRefs.forEach((ref) => expect(refs2).not.toContain(ref));
});
// TODO: Add method to remove APs and use `NeedsAppearances`? How would this
// work with RadioGroups? Just set the APs to `null`but keep the keys?
});
//# sourceMappingURL=PDFForm.spec.js.map