Hopding / pdf-lib

Create and modify PDF documents in any JavaScript environment
https://pdf-lib.js.org
MIT License
6.76k stars 647 forks source link

Form fields deleted when I sign a pdf #860

Closed Geoov closed 2 years ago

Geoov commented 3 years ago

Hello. So, what I'm trying to achieve is completing a pdf with multiple fillable forms generated in Latex, and after that, sign it with node-signpdf. This is the code that I use to sign the document, (it is taken from an issue from node-signpdf). The code works fine, my document it's signed, but all my form fields are deleted, inclusive the text included in them. My forms were created with \TextField tag from Latex.

const fs = require("fs");
const signer = require("node-signpdf");

const {
  PDFDocument,
  PDFName,
  PDFNumber,
  PDFHexString,
  PDFString,
} = require("pdf-lib");

const PDFArrayCustom = require("./PDFArrayCustom");

const pdfBuffer = fs.readFileSync("./resources/myPdf.pdf");

const p12Buffer = fs.readFileSync("./resources/certificate.p12");

const SIGNATURE_LENGTH = 3390;

(async () => {
  const pdfDoc = await PDFDocument.load(pdfBuffer);
  const pages = pdfDoc.getPages();

  const ByteRange = PDFArrayCustom.withContext(pdfDoc.context);
  ByteRange.push(PDFNumber.of(0));
  ByteRange.push(PDFName.of(signer.DEFAULT_BYTE_RANGE_PLACEHOLDER));
  ByteRange.push(PDFName.of(signer.DEFAULT_BYTE_RANGE_PLACEHOLDER));
  ByteRange.push(PDFName.of(signer.DEFAULT_BYTE_RANGE_PLACEHOLDER));

  const signatureDict = pdfDoc.context.obj({
    Type: "Sig",
    Filter: "Adobe.PPKLite",
    SubFilter: "adbe.pkcs7.detached",
    ByteRange,
    Contents: PDFHexString.of("A".repeat(SIGNATURE_LENGTH)),
    Reason: PDFString.of("We need your signature for reasons..."),
    M: PDFString.fromDate(new Date()),
  });
  const signatureDictRef = pdfDoc.context.register(signatureDict);

  const widgetDict = pdfDoc.context.obj({
    Type: "Annot",
    Subtype: "Widget",
    FT: "Sig",
    Rect: [0, 0, 0, 0],
    V: signatureDictRef,
    T: PDFString.of("Test"),
    F: 4,
    P: pages[1].ref,
  });
  const widgetDictRef = pdfDoc.context.register(widgetDict);

  // Add our signature widget to the first page
  pages[1].node.set(PDFName.of("Annots"), pdfDoc.context.obj([widgetDictRef]));

  // Create an AcroForm object containing our signature widget
  pdfDoc.catalog.set(
    PDFName.of("AcroForm"),
    pdfDoc.context.obj({
      SigFlags: 3,
      Fields: [widgetDictRef],
    })
  );

  const modifiedPdfBytes = await pdfDoc.save({ useObjectStreams: false });
  const modifiedPdfBuffer = Buffer.from(modifiedPdfBytes);

  const signObj = new signer.SignPdf();
  const signedPdfBuffer = signObj.sign(modifiedPdfBuffer, p12Buffer);

  // Write the signed file
  fs.writeFileSync("./resources/signed1.pdf", signedPdfBuffer);
})();

The part of code that is creating this behavior is:

  pdfDoc.catalog.set(
    PDFName.of("AcroForm"),
    pdfDoc.context.obj({
      SigFlags: 3,
      Fields: [widgetDictRef],
    })
  );

but without it the signing doesn't work. So, what I think it's happening is that, this widget is overwriting all my form fields and, in this way my data it's gone.

I found a solution to this with the following lines:

  pdfDoc
  .getForm()
  .getFields()
  .forEach((field) => field.enableReadOnly());

but it just shows as form fields, they can't be completed anymore (I know this is what read only does, but maybe there is another solution for this).

btecu commented 3 years ago

Seems like this would be an issue for node-signpdf, however, that makes sense, no? If you sign a form, why would you leave it editable? Wouldn't that defeat the purpose of the signature?

Geoov commented 3 years ago

Yes, you are pretty right here. But I was interested in another 'solution' because when I set the fields on readonly the text box format I set with Latex is lost. Now, this is not something that 'critical' and that's why I asked if there is another solution besides field.enableReadOnly(), or better said, it would be perfect I can set readonly on text boxes without losing any of their format.

btecu commented 3 years ago

Why not flatten the form before you use node-signpdf? That should conserve the styling of your form.

Geoov commented 3 years ago

Ah, so there was a solution like this. Thank you, this should fix my problem.

kevinbror commented 3 years ago

Multiple signatures is a common use case. Anyone find a way to do this?

Trapfether commented 2 years ago

@Geoov The code you outlined as being problematic is actually overwriting the Acroform in the PDF. It is my understanding that PDFs only have a single form object that all fields are within. By setting the AcroForm to the newly created one, you are effectively removing your pre-existing form.

you might try the following instead

pdfDoc.getForm().acroForm.addField(widgetDictRef)

Also, if in the future you can provide a self-contained example that demonstrates the issue, it will make it far easier for someone to help you out. The maintainer of PDF-lib has provided a handy template for making sscce bug reports. here is a direct link for future reference Bug Report

Hopding commented 2 years ago

@Trapfether is spot on 👍