Skip to content

Commit

Permalink
✨ [placeholder-pdf-lib]
Browse files Browse the repository at this point in the history
* Use pdf-lib 1.17 types;
* Use the newly introduced FLAG enums;
* Reuse existing AcroForm (refs #198);
* Reuse existing Annots (refs #198);
* Fix mistaken signature length;
  • Loading branch information
Valeri Buchinski authored and vbuch committed Nov 21, 2023
1 parent 03e9c28 commit 92c1a0c
Showing 1 changed file with 63 additions and 29 deletions.
92 changes: 63 additions & 29 deletions packages/placeholder-pdf-lib/src/pdflibAddPlaceholder.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,40 @@
import {DEFAULT_BYTE_RANGE_PLACEHOLDER, DEFAULT_SIGNATURE_LENGTH, SUBFILTER_ADOBE_PKCS7_DETACHED} from '@signpdf/utils';
import {
PDFArray, PDFHexString, PDFName, PDFNumber, PDFString, toHexStringOfMinLength,
ANNOTATION_FLAGS,
DEFAULT_BYTE_RANGE_PLACEHOLDER,
DEFAULT_SIGNATURE_LENGTH,
SIG_FLAGS,
SUBFILTER_ADOBE_PKCS7_DETACHED,
} from '@signpdf/utils';
import {
PDFArray, PDFDict, PDFHexString, PDFName, PDFNumber, PDFString,
} from 'pdf-lib';

/**
* @typedef {import('pdf-lib').PDFDocumentFactory} PDFDocumentFactory
* @typedef {import('pdf-lib').PDFDocument} PDFDocument
*/

/**
* @typedef {object} InputType
* @property {PDFDocumentFactory} pdfDoc
* @property {PDFDocument} pdfDoc
* @property {string} reason
* @property {string} contactInfo
* @property {string} name
* @property {string} location
* @property {number} [signatureLength]
* @property {string} [byteRangePlaceholder]
* @property {string} [subFilter] One of SUBFILTER_* from @signpdf/utils
* @property {string} [subFilter] One of SUBFILTER_* from \@signpdf/utils
* @property {number[]} [widgetRect] [x1, y1, x2, y2] widget rectangle
*/

/**
* Adds a signature placeholder to a PDF-LIB PDFDocument.
*
* Alters the passed pdfDoc and returns void.
*
* @param {InputType}
* @returns {Buffer}
* @returns {void}
*/
export const pdflibAddPlaceholder = async ({
export const pdflibAddPlaceholder = ({
pdfDoc,
reason,
contactInfo,
Expand All @@ -35,17 +45,20 @@ export const pdflibAddPlaceholder = async ({
subFilter = SUBFILTER_ADOBE_PKCS7_DETACHED,
widgetRect = [0, 0, 0, 0],
}) => {
const pages = pdfDoc.getPages();
const page = pdfDoc.getPage(0);

// Create a placeholder where the the last 3 parameters of the
// actual range will be replaced when signing is done.
const byteRange = PDFArray.withContext(pdfDoc.context);
byteRange.push(PDFNumber.of(0));
byteRange.push(PDFString.of(byteRangePlaceholder));
byteRange.push(PDFString.of(byteRangePlaceholder));
byteRange.push(PDFString.of(byteRangePlaceholder));
byteRange.push(PDFName.of(byteRangePlaceholder));
byteRange.push(PDFName.of(byteRangePlaceholder));
byteRange.push(PDFName.of(byteRangePlaceholder));

const hexNull = toHexStringOfMinLength(0, 4);
const placeholder = PDFHexString.of(hexNull.repeat(signatureLength));
// Fill the contents of the placeholder with 00s.
const placeholder = PDFHexString.of(String.fromCharCode(0).repeat(signatureLength));

// Create a signature dictionary to be referenced in the signature widget.
const signatureDict = pdfDoc.context.obj({
Type: 'Sig',
Filter: 'Adobe.PPKLite',
Expand All @@ -58,34 +71,55 @@ export const pdflibAddPlaceholder = async ({
Name: PDFString.of(name),
Location: PDFString.of(location),
}, pdfDoc.index);
const signatureDictRef = pdfDoc.context.register(signatureDict);

// Create the signature widget
const rect = PDFArray.withContext(pdfDoc.context);
widgetRect.forEach((c) => rect.push(PDFNumber.of(c)));
const widgetDict = pdfDoc.context.obj({
Type: 'Annot',
Subtype: 'Widget',
FT: 'Sig',
Rect: rect,
V: signatureDict,
V: signatureDictRef,
T: PDFString.of('Signature1'),
F: 4,
P: pages[0].ref,
F: ANNOTATION_FLAGS.PRINT,
P: page.ref,
}, pdfDoc.index);
const widgetDictRef = pdfDoc.context.register(widgetDict);

pages[0].node.set(
PDFName.of('Annots'),
pdfDoc.context.obj([widgetDictRef]),
);
// Annotate the widget on the first page
let annotations = page.node.lookupMaybe(PDFName.of('Annots'), PDFArray);
if (typeof annotations === 'undefined') {
annotations = pdfDoc.context.obj([]);
}
annotations.push(widgetDictRef);
page.node.set(PDFName.of('Annots'), annotations);

pdfDoc.catalog.set(
PDFName.of('AcroForm'),
pdfDoc.context.obj({
SigFlags: 3,
Fields: [widgetDictRef],
}),
);
// Add an AcroForm or update the existing one
let acroForm = pdfDoc.catalog.lookupMaybe(PDFName.of('AcroForm'), PDFDict);
if (typeof acroForm === 'undefined') {
// Need to create a new AcroForm
acroForm = pdfDoc.context.obj({Fields: []});
const acroFormRef = pdfDoc.context.register(acroForm);
pdfDoc.catalog.set(PDFName.of('AcroForm'), acroFormRef);
}

const pdfBytes = await pdfDoc.save({useObjectStreams: false});
return Buffer.from(pdfBytes);
/**
* @type {PDFNumber}
*/
let sigFlags;
if (acroForm.has(PDFName.of('SigFlags'))) {
// Already has some flags, will merge
sigFlags = acroForm.get(PDFName.of('SigFlags'));
} else {
// Create blank flags
sigFlags = PDFNumber.of(0);
}
const updatedFlags = PDFNumber.of(
sigFlags.asNumber() | SIG_FLAGS.SIGNATURES_EXIST | SIG_FLAGS.APPEND_ONLY,
);
acroForm.set(PDFName.of('SigFlags'), updatedFlags);
const fields = acroForm.get(PDFName.of('Fields'));
fields.push(widgetDictRef);
};

0 comments on commit 92c1a0c

Please sign in to comment.