acro form fields blank in either downloaded or streamed PDF
See original GitHub issueHey, 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:
- Created 3 years ago
- Reactions:2
- Comments:6 (1 by maintainers)

Top Related StackOverflow Question
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.)
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: