Skip to content

Commit

Permalink
Web and pdflib enhancements
Browse files Browse the repository at this point in the history
  • Loading branch information
dcbr committed Jan 6, 2024
1 parent 77ceb2f commit 632f3ef
Show file tree
Hide file tree
Showing 28 changed files with 403 additions and 113 deletions.
10 changes: 4 additions & 6 deletions packages/examples/src/pdf-lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,17 @@ function work() {
// Add a placeholder for a signature.
pdflibAddPlaceholder({
pdfDoc: pdfDoc,
reason: 'The user is decalaring consent through JavaScript.',
reason: 'The user is declaring consent through JavaScript.',
contactInfo: 'signpdf@example.com',
name: 'John Doe',
location: 'Free Text Str., Free World',
});

// Convert the PDF-LIB PDFDocument to Buffer
pdfDoc.save({useObjectStreams: false}).then(function (pdfBytes) {
var pdfWithPlaceholder = Buffer.from(pdfBytes);

// Get the modified PDFDocument bytes
pdfDoc.save().then(function (pdfWithPlaceholderBytes) {
// And finally sign the document.
signpdf
.sign(pdfWithPlaceholder, signer)
.sign(pdfWithPlaceholderBytes, signer)
.then(function (signedPdf) {
// signedPdf is a Buffer of an electronically signed PDF. Store it.
var targetPath = path.join(__dirname, '/../output/pdf-lib.pdf');
Expand Down
8 changes: 7 additions & 1 deletion packages/placeholder-pdf-lib/dist/pdflibAddPlaceholder.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
export function pdflibAddPlaceholder({ pdfDoc, reason, contactInfo, name, location, signatureLength, byteRangePlaceholder, subFilter, widgetRect, }: InputType): void;
export function pdflibAddPlaceholder({ pdfDoc, pdfPage, reason, contactInfo, name, location, signingTime, signatureLength, byteRangePlaceholder, subFilter, widgetRect, appName, }: InputType): void;
export type PDFDocument = import('pdf-lib').PDFDocument;
export type InputType = {
pdfDoc: PDFDocument;
pdfPage: PDFPage;
reason: string;
contactInfo: string;
name: string;
location: string;
signingTime?: Date;
signatureLength?: number;
byteRangePlaceholder?: string;
/**
Expand All @@ -16,5 +18,9 @@ export type InputType = {
* [x1, y1, x2, y2] widget rectangle
*/
widgetRect?: number[];
/**
* Name of the application generating the signature
*/
appName?: string;
};
//# sourceMappingURL=pdflibAddPlaceholder.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 55 additions & 20 deletions packages/placeholder-pdf-lib/dist/pdflibAddPlaceholder.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ var _pdfLib = require("pdf-lib");
/**
* @typedef {object} InputType
* @property {PDFDocument} pdfDoc
* @property {PDFPage} pdfPage
* @property {string} reason
* @property {string} contactInfo
* @property {string} name
* @property {string} location
* @property {Date} [signingTime]
* @property {number} [signatureLength]
* @property {string} [byteRangePlaceholder]
* @property {string} [subFilter] One of SUBFILTER_* from \@signpdf/utils
* @property {number[]} [widgetRect] [x1, y1, x2, y2] widget rectangle
* @property {string} [appName] Name of the application generating the signature
*/

/**
Expand All @@ -32,21 +35,28 @@ var _pdfLib = require("pdf-lib");
* @returns {void}
*/
const pdflibAddPlaceholder = ({
pdfDoc,
pdfDoc = undefined,
pdfPage = undefined,
reason,
contactInfo,
name,
location,
signingTime = undefined,
signatureLength = _utils.DEFAULT_SIGNATURE_LENGTH,
byteRangePlaceholder = _utils.DEFAULT_BYTE_RANGE_PLACEHOLDER,
subFilter = _utils.SUBFILTER_ADOBE_PKCS7_DETACHED,
widgetRect = [0, 0, 0, 0]
widgetRect = [0, 0, 0, 0],
appName = undefined
}) => {
const page = pdfDoc.getPage(0);
if (pdfDoc === undefined && pdfPage === undefined) {
throw new _utils.SignPdfError('PDFDoc or PDFPage must be set.', _utils.SignPdfError.TYPE_INPUT);
}
const doc = pdfDoc !== null && pdfDoc !== void 0 ? pdfDoc : pdfPage.doc;
const page = pdfPage !== null && pdfPage !== void 0 ? pdfPage : doc.getPages()[0];

// Create a placeholder where the the last 3 parameters of the
// actual range will be replaced when signing is done.
const byteRange = _pdfLib.PDFArray.withContext(pdfDoc.context);
const byteRange = _pdfLib.PDFArray.withContext(doc.context);
byteRange.push(_pdfLib.PDFNumber.of(0));
byteRange.push(_pdfLib.PDFName.of(byteRangePlaceholder));
byteRange.push(_pdfLib.PDFName.of(byteRangePlaceholder));
Expand All @@ -56,52 +66,77 @@ const pdflibAddPlaceholder = ({
const placeholder = _pdfLib.PDFHexString.of(String.fromCharCode(0).repeat(signatureLength));

// Create a signature dictionary to be referenced in the signature widget.
const signatureDict = pdfDoc.context.obj({
const appBuild = appName ? {
App: {
Name: appName
}
} : {};
const signatureDict = doc.context.obj({
Type: 'Sig',
Filter: 'Adobe.PPKLite',
SubFilter: subFilter,
ByteRange: byteRange,
Contents: placeholder,
Reason: _pdfLib.PDFString.of(reason),
M: _pdfLib.PDFString.fromDate(new Date()),
M: _pdfLib.PDFString.fromDate(signingTime !== null && signingTime !== void 0 ? signingTime : new Date()),
ContactInfo: _pdfLib.PDFString.of(contactInfo),
Name: _pdfLib.PDFString.of(name),
Location: _pdfLib.PDFString.of(location)
}, pdfDoc.index);
const signatureDictRef = pdfDoc.context.register(signatureDict);
Location: _pdfLib.PDFString.of(location),
Prop_Build: {
Filter: {
Name: 'Adobe.PPKLite'
},
...appBuild
}
});
// Register signatureDict as a PDFInvalidObject to prevent PDFLib from serializing it
// in an object stream.
const signatureBuffer = new Uint8Array(signatureDict.sizeInBytes());
signatureDict.copyBytesInto(signatureBuffer, 0);
const signatureObj = _pdfLib.PDFInvalidObject.of(signatureBuffer);
const signatureDictRef = doc.context.register(signatureObj);

// Create the signature widget
const rect = _pdfLib.PDFArray.withContext(pdfDoc.context);
const rect = _pdfLib.PDFArray.withContext(doc.context);
widgetRect.forEach(c => rect.push(_pdfLib.PDFNumber.of(c)));
const widgetDict = pdfDoc.context.obj({
const apStream = doc.context.formXObject([], {
BBox: widgetRect,
Resources: {} // Necessary to avoid Acrobat bug (see https://stackoverflow.com/a/73011571)
});

const widgetDict = doc.context.obj({
Type: 'Annot',
Subtype: 'Widget',
FT: 'Sig',
Rect: rect,
V: signatureDictRef,
T: _pdfLib.PDFString.of('Signature1'),
F: _utils.ANNOTATION_FLAGS.PRINT,
P: page.ref
}, pdfDoc.index);
const widgetDictRef = pdfDoc.context.register(widgetDict);
P: page.ref,
AP: {
N: doc.context.register(apStream)
} // Required for PDF/A compliance
});

const widgetDictRef = doc.context.register(widgetDict);

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

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

/**
Expand Down
65 changes: 47 additions & 18 deletions packages/placeholder-pdf-lib/src/pdflibAddPlaceholder.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import {
DEFAULT_SIGNATURE_LENGTH,
SIG_FLAGS,
SUBFILTER_ADOBE_PKCS7_DETACHED,
SignPdfError,
} from '@signpdf/utils';
import {
PDFArray, PDFDict, PDFHexString, PDFName, PDFNumber, PDFString,
PDFArray, PDFDict, PDFHexString, PDFName, PDFNumber, PDFInvalidObject, PDFString,
} from 'pdf-lib';

/**
Expand All @@ -16,14 +17,17 @@ import {
/**
* @typedef {object} InputType
* @property {PDFDocument} pdfDoc
* @property {PDFPage} pdfPage
* @property {string} reason
* @property {string} contactInfo
* @property {string} name
* @property {string} location
* @property {Date} [signingTime]
* @property {number} [signatureLength]
* @property {string} [byteRangePlaceholder]
* @property {string} [subFilter] One of SUBFILTER_* from \@signpdf/utils
* @property {number[]} [widgetRect] [x1, y1, x2, y2] widget rectangle
* @property {string} [appName] Name of the application generating the signature
*/

/**
Expand All @@ -35,21 +39,31 @@ import {
* @returns {void}
*/
export const pdflibAddPlaceholder = ({
pdfDoc,
pdfDoc = undefined,
pdfPage = undefined,
reason,
contactInfo,
name,
location,
signingTime = undefined,
signatureLength = DEFAULT_SIGNATURE_LENGTH,
byteRangePlaceholder = DEFAULT_BYTE_RANGE_PLACEHOLDER,
subFilter = SUBFILTER_ADOBE_PKCS7_DETACHED,
widgetRect = [0, 0, 0, 0],
appName = undefined,
}) => {
const page = pdfDoc.getPage(0);
if (pdfDoc === undefined && pdfPage === undefined) {
throw new SignPdfError(
'PDFDoc or PDFPage must be set.',
SignPdfError.TYPE_INPUT,
);
}
const doc = pdfDoc ?? pdfPage.doc;
const page = pdfPage ?? doc.getPages()[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);
const byteRange = PDFArray.withContext(doc.context);
byteRange.push(PDFNumber.of(0));
byteRange.push(PDFName.of(byteRangePlaceholder));
byteRange.push(PDFName.of(byteRangePlaceholder));
Expand All @@ -59,24 +73,38 @@ export const pdflibAddPlaceholder = ({
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({
const appBuild = appName ? {App: {Name: appName}} : {};
const signatureDict = doc.context.obj({
Type: 'Sig',
Filter: 'Adobe.PPKLite',
SubFilter: subFilter,
ByteRange: byteRange,
Contents: placeholder,
Reason: PDFString.of(reason),
M: PDFString.fromDate(new Date()),
M: PDFString.fromDate(signingTime ?? new Date()),
ContactInfo: PDFString.of(contactInfo),
Name: PDFString.of(name),
Location: PDFString.of(location),
}, pdfDoc.index);
const signatureDictRef = pdfDoc.context.register(signatureDict);
Prop_Build: {
Filter: {Name: 'Adobe.PPKLite'},
...appBuild,
},
});
// Register signatureDict as a PDFInvalidObject to prevent PDFLib from serializing it
// in an object stream.
const signatureBuffer = new Uint8Array(signatureDict.sizeInBytes());
signatureDict.copyBytesInto(signatureBuffer, 0);
const signatureObj = PDFInvalidObject.of(signatureBuffer);
const signatureDictRef = doc.context.register(signatureObj);

// Create the signature widget
const rect = PDFArray.withContext(pdfDoc.context);
const rect = PDFArray.withContext(doc.context);
widgetRect.forEach((c) => rect.push(PDFNumber.of(c)));
const widgetDict = pdfDoc.context.obj({
const apStream = doc.context.formXObject([], {
BBox: widgetRect,
Resources: {}, // Necessary to avoid Acrobat bug (see https://stackoverflow.com/a/73011571)
});
const widgetDict = doc.context.obj({
Type: 'Annot',
Subtype: 'Widget',
FT: 'Sig',
Expand All @@ -85,24 +113,25 @@ export const pdflibAddPlaceholder = ({
T: PDFString.of('Signature1'),
F: ANNOTATION_FLAGS.PRINT,
P: page.ref,
}, pdfDoc.index);
const widgetDictRef = pdfDoc.context.register(widgetDict);
AP: {N: doc.context.register(apStream)}, // Required for PDF/A compliance
});
const widgetDictRef = doc.context.register(widgetDict);

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

// Add an AcroForm or update the existing one
let acroForm = pdfDoc.catalog.lookupMaybe(PDFName.of('AcroForm'), PDFDict);
let acroForm = doc.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);
acroForm = doc.context.obj({Fields: []});
const acroFormRef = doc.context.register(acroForm);
doc.catalog.set(PDFName.of('AcroForm'), acroFormRef);
}

/**
Expand Down
Loading

0 comments on commit 632f3ef

Please sign in to comment.