UNPKG

@pdfme/pdf-lib

Version:

Create and modify PDF files with JavaScript

266 lines β€’ 14.5 kB
import fs from 'fs'; import { PDFDocument, PDFTextField, PDFCheckBox, PDFButton, PDFRadioGroup, PDFOptionList, PDFDropdown, PDFDict, PDFName, PDFForm, PDFAcroForm, } from '../../../src/index'; const getWidgets = (pdfDoc) => pdfDoc.context .enumerateIndirectObjects() .map(([, obj]) => obj) .filter((obj) => obj instanceof PDFDict && obj.get(PDFName.of('Type')) === PDFName.of('Annot') && obj.get(PDFName.of('Subtype')) === PDFName.of('Widget')) .map((obj) => obj); const getRefs = (pdfDoc) => pdfDoc.context.enumerateIndirectObjects().map(([ref]) => ref); const getApRefs = (widget) => { const onValue = widget.getOnValue() ?? PDFName.of('Yes'); const aps = widget.getAppearances(); return [ (aps?.normal).get(onValue), aps?.rollover?.get(onValue), aps?.down?.get(onValue), (aps?.normal).get(PDFName.of('Off')), aps?.rollover?.get(PDFName.of('Off')), aps?.down?.get(PDFName.of('Off')), ].filter(Boolean); }; const flatten = (arr) => arr.reduce((curr, acc) => [...acc, ...curr], []); const fancyFieldsPdfBytes = fs.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.readFileSync('assets/pdfs/with_xfa_fields.pdf'); const signaturePdfBytes = fs.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 PDFDocument.load(fancyFieldsPdfBytes); const form = pdfDoc.getForm(); const fields = form.getFields(); expect(fields.length).toBe(15); expect(form.getField('Prefix ⚽️')).toBeInstanceOf(PDFTextField); expect(form.getField('First Name πŸš€')).toBeInstanceOf(PDFTextField); expect(form.getField('MiddleInitial 🎳')).toBeInstanceOf(PDFTextField); expect(form.getField('LastName πŸ›©')).toBeInstanceOf(PDFTextField); expect(form.getField('Are You A Fairy? 🌿')).toBeInstanceOf(PDFCheckBox); expect(form.getField('Is Your Power Level Over 9000? πŸ’ͺ')).toBeInstanceOf(PDFCheckBox); expect(form.getField('Can You Defeat Enemies In One Punch? πŸ‘Š')).toBeInstanceOf(PDFCheckBox); expect(form.getField('Will You Ever Let Me Down? β˜•οΈ')).toBeInstanceOf(PDFCheckBox); expect(form.getField('Eject πŸ“Ό')).toBeInstanceOf(PDFButton); expect(form.getField('Submit πŸ“')).toBeInstanceOf(PDFButton); expect(form.getField('Play ▢️')).toBeInstanceOf(PDFButton); expect(form.getField('Launch πŸš€')).toBeInstanceOf(PDFButton); expect(form.getField('Historical Figures 🐺')).toBeInstanceOf(PDFRadioGroup); expect(form.getField('Which Are Planets? 🌎')).toBeInstanceOf(PDFOptionList); expect(form.getField('Choose A Gundam πŸ€–')).toBeInstanceOf(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 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 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(PDFName.of('AP'))).length; expect(aps()).toBe(5); widgets.forEach((w) => w.delete(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 PDFDocument.load(xfaPdfBytes); const acroForm = pdfDoc.catalog.getOrCreateAcroForm(); expect(acroForm.dict.has(PDFName.of('XFA'))).toBe(true); expect(pdfDoc.getForm()).toBeInstanceOf(PDFForm); expect(acroForm.dict.has(PDFName.of('XFA'))).toBe(false); }); it(`is only created if it is accessed`, async () => { const pdfDoc = await PDFDocument.create(); expect(pdfDoc.catalog.getAcroForm()).toBe(undefined); expect(pdfDoc.getForm()).toBeInstanceOf(PDFForm); expect(pdfDoc.catalog.getAcroForm()).toBeInstanceOf(PDFAcroForm); }); it(`does not update appearance streams if "updateFieldAppearances" is true, but no fields are dirty`, async () => { const pdfDoc = await PDFDocument.load(fancyFieldsPdfBytes); const widgets = getWidgets(pdfDoc); expect(widgets.length).toBe(24); const aps = () => widgets.filter((w) => w.has(PDFName.of('AP'))).length; expect(aps()).toBe(24); widgets.forEach((w) => w.delete(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 PDFDocument.load(fancyFieldsPdfBytes); const widgets = getWidgets(pdfDoc); expect(widgets.length).toBe(24); const aps = () => widgets.filter((w) => w.has(PDFName.of('AP'))).length; expect(aps()).toBe(24); widgets.forEach((w) => w.delete(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 PDFDocument.load(fancyFieldsPdfBytes); const widgets = getWidgets(pdfDoc); expect(widgets.length).toBe(24); const aps = () => widgets.filter((w) => w.has(PDFName.of('AP'))).length; expect(aps()).toBe(24); widgets.forEach((w) => w.delete(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 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 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 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