question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

acro form fields blank in either downloaded or streamed PDF

See original GitHub issue

Hey, we are using your great lib to fill form field in a pdf and sending it back to the user in two ways. First we are directly streaming the PDF to update a PDF Preview on the client and after finished the edit we persist the PDF and just deliver the PDF via GET Url.

We built a small helper to generate the filled PDF and return the bytes after we performed pdfDoc.save(). We use the exact same method in both cases but depending on wether we stream it directly or save the file we get either blank fields or filled fields. We had an old implementation – found somewhere in this repo – which only showed the filled fields when we persisted the PDF but not in preview mode. We then updated this implementation as stated in the Update to V1.X.X guide in the readme, this made the preview work but broke the persisted PDF. After that we started from scratch with the solution provided in https://github.com/Hopding/pdf-lib/issues/205 which seemed promising but again, only the preview worked, not the persisted PDF.

So we are kinda out of ideas here, maybe you have any further suggestions?

pdfhelper.js (only working as persisted PDF)

/* eslint-disable no-await-in-loop */
/** PDF helper function module
 * @module pdfhelper
 */

'use strict';

const { PDFName, PDFNumber, PDFString, PDFDict, PDFContentStream, drawLinesOfText, degrees, rgb } = require('pdf-lib');

const pdfhelper = {

    getAcroFields: function (pdfDoc) {
        if (!pdfDoc.catalog.get(PDFName.of('AcroForm'))) { return []; }
        const acroForm = pdfDoc.context.lookup(pdfDoc.catalog.get(PDFName.of('AcroForm')));
        if (!acroForm.get(PDFName.of('Fields'))) { return []; }
        const acroFields = acroForm.context.lookup(acroForm.get(PDFName.of('Fields')));
        return acroFields.array.map(ref => acroForm.context.lookup(ref));
    },

    findAcroFieldByName: function (pdfDoc, name) {
        const acroFields = this.getAcroFields(pdfDoc);
        return acroFields.find((acroField) => {
            const fieldName = acroField.get(PDFName.of('T'));
            return !!fieldName && fieldName.value === name;
        });
    },

    fillAcroTextField: function (
        pdfDoc,
        acroField,
        fontObject,
        text,
        fontSize = 15,
    ) {
        const fieldRect = acroField.get(PDFName.of('Rect'));
        const fieldWidth = fieldRect.get(2).number - fieldRect.get(0).number;
        const fieldHeight = fieldRect.get(3).number - fieldRect.get(1).number;

        const dict = fontObject.doc.context.obj({
            Type: 'XObject',
            Subtype: 'Form',
            FormType: 1,
            BBox: [0, 0, fieldWidth, fieldHeight],
            Resources: { Font: { F0: fontObject.ref } },
        });

        const appearanceStream = fontObject.doc.context.register(
            PDFContentStream.of(
                dict,
                drawLinesOfText(text.split('\n'), {
                    color: rgb(0, 0, 0),
                    font: fontObject.name,
                    size: fontSize,
                    rotate: degrees(0),
                    xSkew: degrees(0),
                    ySkew: degrees(0),
                    x: 0,
                    y: 0,
                    lineHeight: fontSize + 2,
                })
            ),
        );

        acroField.set(PDFName.of('V'), PDFString.of(text));
        acroField.set(PDFName.of('Ff'), PDFNumber.of(1 /* Read Only */));
        acroField.set(PDFName.of('AP'), acroField.context.obj({ N: appearanceStream }));
    },

    lockField: function (acroField) {
        acroField.set(PDFName.of('Ff'), PDFNumber.of(1 << 0 /* Read Only */));
    }
};

module.exports = pdfhelper;

helper.js (only working as persisted PDF)

const generatePDF = async function (language) {
    const pdfDoc = await PDFDocument.load(fs.readFileSync('./some/path/pdf_' + language + '.pdf'));
    pdfDoc.registerFontkit(fontkit);
    const fontObject = await pdfDoc.embedFont(StandardFonts.Helvetica);

    const fillInField = (fieldName, text, fontSize = 12) => {
        const field = pdfhelper.findAcroFieldByName(pdfDoc, fieldName);
        if (!field) {
            throw new Error(`Missing AcroField: ${fieldName}`);
        };
        pdfhelper.fillAcroTextField(pdfDoc, field, fontObject, text, fontSize);
    };

    const date = moment(new Date()).locale(util.convertLanguageForDate(language)).format(util.getDateFormat(language));
    fillInField('Principal_Place, Date', date);
    fillInField('Agent_Place, Date', date);

    fillInField('Principal', 'John Doe');
    fillInField('PrincipalRole', 'CTO');

    fillInField('Agent', 'Jane Doe');
    fillInField('AgentRole', 'Managing Director');

    fillInField(
        'Company',
        'SomeStreet 123' + '\n'
        + '54321 SomeCity' + '\n'
        + 'SomeCountry'
    );

    const pdfBytes = await pdfDoc.save();
    return pdfBytes;
}

routes.js (excerpt)

router.route('/:id/legal')
    .get(/* ... */, (async (req, res, next) => {
       
        /* ... */

        const pdfBytes = await helper.generatePDF(login, customer);
        res.attachment('some.pdf');
        res.contentType('application/pdf');
        res.send(Buffer.from(pdfBytes));
    }))

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:2
  • Comments:6 (1 by maintainers)

github_iconTop GitHub Comments

2reactions
Hopdingcommented, Sep 20, 2020

Hello @faxemaxee @lalaman @freirg! pdf-lib has a new forms API that should solve this problem. See the README and API docs for details.

(Note that the new forms API does not allow forms to be copied from other documents. That’s still an issue. See https://github.com/Hopding/pdf-lib/issues/218 and related issues for details.)

1reaction
lalamancommented, Jul 14, 2020

Unfortunately, this issue has been brought up many times in this repository as copyPages does not copy over acrofields from the original pages.

What I ended up doing was using a combination of pdf-lib to remove pages (since that maintains the acrofields) and then using pdf-merge to join the documents together. This way, the pdfs are merged and retain the acroform fields. After you join, you still have to use PDFDocument.load() to set needAppearances to true in order to see the text inside the acroform fields.

Here is an example:

const fs = require('fs');
const { PDFBool, PDFDocument, PDFName } = require('pdf-lib');
const PDFMerge = require('pdf-merge');

// Helper function for removing pages in PDF
const removePDFPages = async (doc, start, end) => {
  for (let i = end; i >= start; i -= 1) {
    doc.removePage(i);
  }
  const docBytes = await doc.save();
  const filepath = `${Date.now()}.pdf`;
  fs.writeFileSync(filepath, docBytes);
  return filepath;
};

// Join pages 1-10 of doc1 and 1-5 of doc2
const combinePDFs = async (doc1filepath, doc2filepath) => {
  let tempFilepaths = [];

  try {
    const doc1 = await PDFDocument.load(fs.readFileSync(doc1filepath));
    const doc1pages = doc1.getPages();

    const doc2 = await PDFDocument.load(fs.readFileSync(doc2filepath));
    const doc2pages = doc2.getPages();

    const temp1filepath = await removePDFPages(doc1, 10, doc1pages - 1);
    tempFilepaths.push(temp1filepath);

    const temp2filepath = await removePDFPages(doc2, 5, doc2pages - 1);
    tempFilepaths.push(temp2filepath);

    const mergedBuffer = await PDFMerge([
      temp1filepath,
      temp2filepath
    ]);

    // Set appearances to true so that your form fields will not
    // appear as white text
    const mergedDoc = await PDFDocument.load(mergedBuffer);
    const acroForm = mergedDoc.context.lookup(
      mergedDoc.catalog.get(PDFName.of('AcroForm')),
    );
    acroForm.set(PDFName.of('NeedAppearances'), PDFBool.True);

    const mergedDocBytes = await mergedDoc.save();
    const destination = `${Date.now()}-final.pdf`;
    fs.writeFileSync(destination, mergedDocBytes);

    return destination;
  } catch (err) {
    console.log('Unable to combine PDFs', err);
    throw new Error(err);
  } finally {
    // Delete temp files
    for (const path of tempFilepaths) {
      if (fs.existsSync(path)) {
        fs.unlinkSync(path);
      }
    }
  }
};
Read more comments on GitHub >

github_iconTop Results From Across the Web

Fix the common issues in fillable forms in Acrobat or Reader
Make sure that the PDF includes interactive, or fillable, form fields. Sometimes form creators forget to convert their PDFs to interactive forms ......
Read more >
How to fix a fillable PDF that shows blank fields.
1. Make sure the PDF you need to print is saved onto your computer. 2. Right click on the PDF. Note: your menu...
Read more >
Solving the mystery of the empty PDF form - Macworld
Starting in Mountain Lion, entering data in PDF forms got even easier; Preview can now automatically detect where form fields are, so blanks...
Read more >
Working with PDF Forms - Syncfusion
An interactive form, sometimes referred to as an AcroForm is a collection of fields for gathering information. A PDF document can contain any...
Read more >
Create Fillable Forms Using Adobe® Acrobat® DC - YouTube
Learn how to create a fillable PDF form in Adobe Acrobat DC in under 20 minutes. Loaded with Pro Tips to save you...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found