diff --git a/packages/placeholder-pdf-lib/src/pdflibAddPlaceholder.js b/packages/placeholder-pdf-lib/src/pdflibAddPlaceholder.js index a5fced8d..67954aaf 100644 --- a/packages/placeholder-pdf-lib/src/pdflibAddPlaceholder.js +++ b/packages/placeholder-pdf-lib/src/pdflibAddPlaceholder.js @@ -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, @@ -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', @@ -58,7 +71,9 @@ 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({ @@ -66,26 +81,45 @@ export const pdflibAddPlaceholder = async ({ 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); };