Hopding / pdf-lib

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

How to get/set Annot or Widget from pdf #39

Closed ssleptsov closed 6 years ago

ssleptsov commented 6 years ago

Hey, is it possible to get/set Annot or Widget for existing pdf file? I'm trying to replace this pdfkit code with your library

const signature = pdf.ref({
  Type: 'Sig',
  Filter: 'Adobe.PPKLite',
  SubFilter: 'adbe.pkcs7.detached',
  ByteRange: [
    0,
    DEFAULT_BYTE_RANGE_PLACEHOLDER,
    DEFAULT_BYTE_RANGE_PLACEHOLDER,
    DEFAULT_BYTE_RANGE_PLACEHOLDER,
  ],
  Contents: Buffer.from(String.fromCharCode(0).repeat(signatureLength)),
  Reason: new String(reason), // eslint-disable-line no-new-wrappers
  M: new Date(),
});

// Generate signature annotation widget
const widget = pdf.ref({
  Type: 'Annot',
  Subtype: 'Widget',
  FT: 'Sig',
  Rect: [0, 0, 0, 0],
  V: signature,
  T: new String('Signature1'), // eslint-disable-line no-new-wrappers
  F: 4,
  P: pdf._root.data.Pages.data.Kids[0], // eslint-disable-line no-underscore-dangle
});

// Include the widget in a page
pdf._root.data.Pages.data.Kids[0].data.Annots = [widget];

// Create a form (with the widget) and link in the _root
const form = pdf.ref({
  SigFlags: 3,
  Fields: [widget],
});
pdf._root.data.AcroForm = form;

Thanks!

Hopding commented 6 years ago

Hello @ssleptsov. This is certainly possible, as pdf-lib provides low level access to pdf objects. However, there is not currently a nice high level API for manipulating annotations or acroform objects, so you'll have to put up with some verbose boilerplate for now :smile:

Here's how to do it:

const pdfBytes = /* A Uint8Array containing the PDF you wish to modify */
const pdfDoc = PDFDocumentFactory.load(pdfBytes);

const [FontHelvetica] = pdfDoc.embedStandardFont('Helvetica');

// Define the signature dictionary (Detailed in Table 252 of PDF specification)
// Note that the dict below isn't actually valid. I just made up its `ByteRange`
// and `Contents` entries. It's just an example.
const signatureDict = PDFDictionary.from({
  Type: PDFName.from('Sig'),
  Filter: PDFName.from('Adobe.PPKLite'),
  SubFilter: PDFName.from('adbe.pkcs7.detached'),
  ByteRange: PDFArray.fromArray([
    PDFNumber.fromNumber(0),
    PDFNumber.fromNumber(0),
    PDFNumber.fromNumber(0),
    PDFNumber.fromNumber(0),
  ], pdfDoc.index),
  Contents: PDFHexString.fromString('00000000000'),
  Reason: PDFString.fromString('We need your signature for reasons...'),
  // M: PDFString.fromString('D:YYYYMMDDHHmmSSOHH'mm')
  // See section "7.9.4 Dates" of the PDF specification for details on the format of date strings
}, pdfDoc.index);

// Define a content stream that defines how the signature field should appear
// on the PDF.
//
// Note: This isn't necessary if you define the signature widget's "Rect" entry
// as [0, 0, 0, 0] like in the snippet you provided. But I've implemented it 
// just in case you (or others) might find it useful.
const sigAppearanceStream = PDFContentStream.of(
  PDFDictionary.from({
    Type: PDFName.from('XObject'),
    Subtype: PDFName.from('Form'),
    BBox: PDFArray.fromArray([
      PDFNumber.fromNumber(0),
      PDFNumber.fromNumber(0),
      PDFNumber.fromNumber(200),
      PDFNumber.fromNumber(50),
    ], pdfDoc.index),
    Resources: PDFDictionary.from({
      Font: PDFDictionary.from({
        Helvetica: FontHelvetica
      }, pdfDoc.index)
    }, pdfDoc.index),
  }, pdfDoc.index),
  drawRectangle({
    x: 0,
    y: 0,
    width: 200,
    height: 50,
    colorRgb: [0.95, 0.95, 0.95],
    borderWidth: 3,
    borderColorRgb: [0, 0, 0],
  }),
  drawText('Sign Here', {
    x: 10,
    y: 15,
    font: 'Helvetica',
    size: 30,
    colorRgb: [0.5, 0.5, 0.5],
  }),
  drawRectangle({
    x: 4,
    y: 4,
    width: 192,
    height: 2,
    colorRgb: [0.5, 0.5, 0.5],
  }),
);
const sigAppearanceStreamRef = pdfDoc.register(sigAppearanceStream);

// Define the signature widget annotation
const widgetDict = PDFDictionary.from({
  Type: PDFName.from('Annot'),
  Subtype: PDFName.from('Widget'),
  FT: PDFName.from('Sig'),
  Rect: PDFArray.fromArray([
    PDFNumber.fromNumber(50),
    PDFNumber.fromNumber(50),
    PDFNumber.fromNumber(300),
    PDFNumber.fromNumber(100),
  ], pdfDoc.index),

  // Uncomment this if you've created a valid signatureDict
  // V: signatureDict,  

  T: PDFString.fromString('Signature1'),
  F: PDFNumber.fromNumber(4),
  P: (pdfDoc.catalog.Pages.get('Kids') as PDFArray).get(0),
  AP: PDFDictionary.from({
    N: sigAppearanceStreamRef,
  }, pdfDoc.index),
}, pdfDoc.index);
const widgetDictRef = pdfDoc.register(widgetDict);

// Add our signature widget to the first page
const pages = pdfDoc.getPages();
pages[0].set(
  'Annots',
  PDFArray.fromArray([widgetDictRef], pdfDoc.index),
);

// Create an AcroForm object containing our signature widget
const formDict = PDFDictionary.from({
  SigFlags: PDFNumber.fromNumber(3),
  Fields: PDFArray.fromArray([widgetDictRef], pdfDoc.index),
}, pdfDoc.index);

pdfDoc.catalog.set('AcroForm', formDict);

const modifiedPdfBytes = PDFDocumentWriter.saveToBytes(pdfDoc);

Here's a sample PDF that I modified using the script above (notice the "Sign Here" field at the bottom of the first page): pdf-with-signature-field-unsigned.pdf.

And here's the original pdf.

Please let me know if this is what you're looking for, or if you need any additional help with pdf-lib!

ssleptsov commented 6 years ago

wow, that's awesome! Thanks for your help and the great library!

Hopding commented 6 years ago

You bet!

(I updated the pdf-with-signature-field-unsigned.pdf file - I noticed I had attached the wrong one).

pspeter3 commented 5 years ago

Does this work for filling form fields? I think I do not know PDFs well enough yet.

Hopding commented 5 years ago

Yes, it should be able to fill form fields. If you can share a document that you'd like to fill out, I can provide some sample code that does so. If not, I can provide an example of filling out a form field with one of the repo's test PDFs, if you let me know which types of form fields you're working with.

Hopding commented 5 years ago

@pspeter3 Sorry I haven’t had a chance to tackle this the past couple of days. I’ll try to write up an example script this evening. If you’re able to submit a PR, that’d be great!

Thanks! As an example, thinking about how to form fill these http://dnd.wizards.com/articles/features/character_sheets. I would be happy to submit a PR with an example of a form fillable document as a thank you for your help. This project is great, thank you for building it.

— You are receiving this because you modified the open/close state.

Reply to this email directly, view it on GitHub https://github.com/Hopding/pdf-lib/issues/39#issuecomment-437608424, or mute the thread https://github.com/notifications/unsubscribe-auth/AJ301KMDCv1QQMgDXaBYZAsXIryyNOe4ks5utx5TgaJpZM4Xx1vf .

pspeter3 commented 5 years ago

No, worries. I think this project is great!

On Tue, Nov 13, 2018, 5:57 AM Andrew Dillon <notifications@github.com wrote:

Sorry I haven’t had a chance to tackle this the past couple of days. I’ll try to write up an example script this evening. If you’re able to submit a PR, that’d be great!

Thanks! As an example, thinking about how to form fill these http://dnd.wizards.com/articles/features/character_sheets. I would be happy to submit a PR with an example of a form fillable document as a thank you for your help. This project is great, thank you for building it.

— You are receiving this because you modified the open/close state.

Reply to this email directly, view it on GitHub https://github.com/Hopding/pdf-lib/issues/39#issuecomment-437608424, or mute the thread < https://github.com/notifications/unsubscribe-auth/AJ301KMDCv1QQMgDXaBYZAsXIryyNOe4ks5utx5TgaJpZM4Xx1vf

.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/Hopding/pdf-lib/issues/39#issuecomment-438274647, or mute the thread https://github.com/notifications/unsubscribe-auth/AAlF1fAgu_gGvcE9JX6piABrLfZJk4A3ks5uus_cgaJpZM4Xx1vf .

Hopding commented 5 years ago

@pspeter3 I created an example script that fills out a DoD character sheet here: https://github.com/Hopding/pdf-lib/issues/48.

My apologies for not getting around to this sooner. Had to deal with some fiascos at work this past week.

john-attrium-204 commented 5 years ago

wow, that's awesome! Thanks for your help and the great library!

Hey ! I am trying to signed my existing pdf file which i have generated using filling form with hummusjs. But i was not able to set default ByteRange for signature placeholder. I am getting this error

Could not find ByteRange placeholder: /ByteRange [0 /** /** /**]

Can you please help me get this done.

Thanks in advance.

Hopding commented 5 years ago

Hello @john-attrium-204!

I need more information in order to help you with this. Can you please provide a code sample and a PDF that I can use to reproduce the issue?

therpobinski commented 4 years ago

HI, I have been trying to put a signature mark on the pdf, but using this library I modify the PDF with the drawText function, but I realize that it removes the trailer from the PDF Buffer. Then I want to sign and he won't let me, because he can't find the trailer. To better understand what I am saying, I provide the library with which I am signing and an pull-request where I am occupying pdf-lib. I hope you understand me and help me.

  1. node-signpdf
  2. pull-request, created by me for node-signpdf, here I explain a bit the problem that is happening to me with pdf-lib

According to the example you proposed, you occupy functions that do not exist in pdf-lib, such as:

And you talk about a 252 table of the PDF specification that I can't find.

vbuch commented 4 years ago

@therpobinski I am pretty sure the original snippet in this issue that @ssleptsov posted is a piece of code from the node-signpdf package. So he was trying to do the same thing you are. What @Hopding gave him is a PDF with a placeholder for signature, so the first part of the task is done. Since the signing code does not care about trailers I think it just worked from here on. @ssleptsov should confirm if that worked for him. But, yes, this is a great snippet that, if @Hopding allows, we can include as a pdflibAddPlaceholder helper in our lib.

I think @john-attrium-204 was dealing with the same thing.

Sorry @Hopding. Just saw we are commenting on a closed issue. And just because it's my first time to write in your lib, I want to congratulate you on it. I haven't had the time to play around with it too much but it looks awesome from what I see. Awesome work! Awesome share!

SnigBhaumik commented 4 years ago

@therpobinski I am pretty sure the original snippet in this issue that @ssleptsov posted is a piece of code from the node-signpdf package. So he was trying to do the same thing you are. What @Hopding gave him is a PDF with a placeholder for signature, so the first part of the task is done. Since the signing code does not care about trailers I think it just worked from here on. @ssleptsov should confirm if that worked for him. But, yes, this is a great snippet that, if @Hopding allows, we can include as a pdflibAddPlaceholder helper in our lib.

I think @john-attrium-204 was dealing with the same thing.

Sorry @Hopding. Just saw we are commenting on a closed issue. And just because it's my first time to write in your lib, I want to congratulate you on it. I haven't had the time to play around with it too much but it looks awesome from what I see. Awesome work! Awesome share!

It will be great if both libraries join hands - both pdf-lib and node-signpdf libraries would offer signing of new and existing pdfs. @Hopding and @vbuch - adding a pdflibAddPlaceholder in addition to pdfkit and plan one would surely resolve a major missing feature request.

therpobinski commented 4 years ago

wow, that's awesome! Thanks for your help and the great library!

Hey ! I am trying to signed my existing pdf file which i have generated using filling form with hummusjs. But i was not able to set default ByteRange for signature placeholder. I am getting this error

Could not find ByteRange placeholder: /ByteRange [0 /** /** /**]

Can you please help me get this done.

Thanks in advance.

Hi, Hello, I have the same problem and I think I know what it is, I am reviewing it and I see that the placeholder has never been embedded in the PDF, so signing the document does not find the ByteRange and the error occurs. Have you been able to find a solution? I doubt that the example that @Hopding gave us is working, I leave you my codecommit so you can review it and see if we can find a solution.

brasycad commented 4 years ago

Hello @ssleptsov. This is certainly possible, as pdf-lib provides low level access to pdf objects. However, there is not currently a nice high level API for manipulating annotations or acroform objects, so you'll have to put up with some verbose boilerplate for now 😄

Here's how to do it:

const pdfBytes = /* A Uint8Array containing the PDF you wish to modify */
const pdfDoc = PDFDocumentFactory.load(pdfBytes);

const [FontHelvetica] = pdfDoc.embedStandardFont('Helvetica');

// Define the signature dictionary (Detailed in Table 252 of PDF specification)
// Note that the dict below isn't actually valid. I just made up its `ByteRange`
// and `Contents` entries. It's just an example.
const signatureDict = PDFDictionary.from({
  Type: PDFName.from('Sig'),
  Filter: PDFName.from('Adobe.PPKLite'),
  SubFilter: PDFName.from('adbe.pkcs7.detached'),
  ByteRange: PDFArray.fromArray([
    PDFNumber.fromNumber(0),
    PDFNumber.fromNumber(0),
    PDFNumber.fromNumber(0),
    PDFNumber.fromNumber(0),
  ], pdfDoc.index),
  Contents: PDFHexString.fromString('00000000000'),
  Reason: PDFString.fromString('We need your signature for reasons...'),
  // M: PDFString.fromString('D:YYYYMMDDHHmmSSOHH'mm')
  // See section "7.9.4 Dates" of the PDF specification for details on the format of date strings
}, pdfDoc.index);

// Define a content stream that defines how the signature field should appear
// on the PDF.
//
// Note: This isn't necessary if you define the signature widget's "Rect" entry
// as [0, 0, 0, 0] like in the snippet you provided. But I've implemented it 
// just in case you (or others) might find it useful.
const sigAppearanceStream = PDFContentStream.of(
  PDFDictionary.from({
    Type: PDFName.from('XObject'),
    Subtype: PDFName.from('Form'),
    BBox: PDFArray.fromArray([
      PDFNumber.fromNumber(0),
      PDFNumber.fromNumber(0),
      PDFNumber.fromNumber(200),
      PDFNumber.fromNumber(50),
    ], pdfDoc.index),
    Resources: PDFDictionary.from({
      Font: PDFDictionary.from({
        Helvetica: FontHelvetica
      }, pdfDoc.index)
    }, pdfDoc.index),
  }, pdfDoc.index),
  drawRectangle({
    x: 0,
    y: 0,
    width: 200,
    height: 50,
    colorRgb: [0.95, 0.95, 0.95],
    borderWidth: 3,
    borderColorRgb: [0, 0, 0],
  }),
  drawText('Sign Here', {
    x: 10,
    y: 15,
    font: 'Helvetica',
    size: 30,
    colorRgb: [0.5, 0.5, 0.5],
  }),
  drawRectangle({
    x: 4,
    y: 4,
    width: 192,
    height: 2,
    colorRgb: [0.5, 0.5, 0.5],
  }),
);
const sigAppearanceStreamRef = pdfDoc.register(sigAppearanceStream);

// Define the signature widget annotation
const widgetDict = PDFDictionary.from({
  Type: PDFName.from('Annot'),
  Subtype: PDFName.from('Widget'),
  FT: PDFName.from('Sig'),
  Rect: PDFArray.fromArray([
    PDFNumber.fromNumber(50),
    PDFNumber.fromNumber(50),
    PDFNumber.fromNumber(300),
    PDFNumber.fromNumber(100),
  ], pdfDoc.index),

  // Uncomment this if you've created a valid signatureDict
  // V: signatureDict,  

  T: PDFString.fromString('Signature1'),
  F: PDFNumber.fromNumber(4),
  P: (pdfDoc.catalog.Pages.get('Kids') as PDFArray).get(0),
  AP: PDFDictionary.from({
    N: sigAppearanceStreamRef,
  }, pdfDoc.index),
}, pdfDoc.index);
const widgetDictRef = pdfDoc.register(widgetDict);

// Add our signature widget to the first page
const pages = pdfDoc.getPages();
pages[0].set(
  'Annots',
  PDFArray.fromArray([widgetDictRef], pdfDoc.index),
);

// Create an AcroForm object containing our signature widget
const formDict = PDFDictionary.from({
  SigFlags: PDFNumber.fromNumber(3),
  Fields: PDFArray.fromArray([widgetDictRef], pdfDoc.index),
}, pdfDoc.index);

pdfDoc.catalog.set('AcroForm', formDict);

const modifiedPdfBytes = PDFDocumentWriter.saveToBytes(pdfDoc);

Here's a sample PDF that I modified using the script above (notice the "Sign Here" field at the bottom of the first page): pdf-with-signature-field-unsigned.pdf.

And here's the original pdf.

Please let me know if this is what you're looking for, or if you need any additional help with pdf-lib!

Hi Andrew. Is it possible to have this code adapted to the latest version? Well, I try to use it and it gives me multiple errors. Thank you

vemundeldegard commented 4 years ago

@brasycad @Hopding Would also love to see this adapted to the latest version.

Hopding commented 4 years ago

@therpobinski @brasycad @vemundeldegard Please see https://github.com/Hopding/pdf-lib/issues/112#issuecomment-569085380. I've updated my example for version 1.3.0.