Digitally sign a PDF using JavaScript
Digitally sign any PDF even if it doesn’t come with a visible signature form field. Get additional resources by visiting our JavaScript PDF signature library or by reading our guide about invisibly signing any PDF using JavaScript.
// Since implementations are different depending on the deployment// we splitted the specific implementation details of each backend// on its own module. We load them both even though we only need// the one from the current backend to avoid waiting to dynamically// fetch and load the relevant implementation.import standalone from "./standalone.js";import server from "./server.js";
export async function load(defaultConfiguration) { // PSPDFKit.Configuration#authPayload is a Server only property. // We check for its presence to determine which module to use const deployment = defaultConfiguration.authPayload ? server : standalone;
return deployment(defaultConfiguration);}
export async function attachListeners(instance, buttons) { instance.addEventListener("document.change", async () => { // Enable the "Reset" button if the document is signed const annotations = await instance.getAnnotations(0);
updateToolbarItems(annotations, true, instance, buttons); });
instance.addEventListener( "annotations.create", async (createdAnnotations) => { const signatures = await instance.getSignaturesInfo(); const isDocumentSigned = signatures.status !== "not_signed";
if (isDocumentSigned) { // Bailing out since we just need to handle the scenario before a digital signature // has been placed. return; }
const annotation = createdAnnotations.get(0);
if (annotation.isSignature) { // Position the created signature over signature form field // and enable the "Finish Signing" button. positionSignature(annotation, instance); await instance.ensureChangesSaved(annotation); updateToolbarItems(createdAnnotations, true, instance, buttons); } } );
instance.addEventListener("annotations.delete", (deletedAnnotations) => { // Disable the "Finish Signing" button if a signature has been deleted updateToolbarItems(deletedAnnotations, false, instance, buttons); });
const annotations = await instance.getAnnotations(0);
updateToolbarItems(annotations, true, instance, buttons);
return instance;}
// Checks what toolbar items need to be enabled/disabled at a given time.// When the document doesn't have any electronic signature yet, we want to// disable the "Finish signing" button. Once a signature is placed, it// the electronic signature button should be disabled and the "Finish signing"// should be enabled instead.// Once the document has been signed, we show the "Reset" button.async function updateToolbarItems( annotations, disableFinishIfNoAnnotations, instance, buttons) { const signatures = await instance.getSignaturesInfo(); const { resetButton, saveButton, finishButton } = buttons; const hasSignatureAnnotation = annotations.some( (annotation) => annotation.isSignature ); // When the document is loaded and when a signature annotation is // created or deleted, we need to enable or disable the signing custom // toolbar item accordingly. The "disableFinishIfNoAnnotations" boolean // will determine which disable state we'll update the toolbar button with. const shouldDisableFinishBtn = disableFinishIfNoAnnotations ? !hasSignatureAnnotation : hasSignatureAnnotation; const additionalButtons = signatures.status === "not_signed" ? [ { type: "signature", disabled: !shouldDisableFinishBtn, }, { ...finishButton, disabled: shouldDisableFinishBtn, }, saveButton, ] : [{ type: "signature", disabled: true }, resetButton, saveButton];
instance.setToolbarItems([...initialToolbarItems, ...additionalButtons]);}
// Helper function to properly place the signature annotation// added by the user to the corresponding spot on the document.// Based on https://www.nutrient.io/guides/web/knowledge-base/override-ink-signature-dialog/function positionSignature(annotation, instance) { // appropiate rect for the space to fit the annotation in const signingSpace = new PSPDFKit.Geometry.Rect({ width: 150, height: 40, left: 375, top: 690, });
const newSize = fitIn( { width: annotation.boundingBox.width, height: annotation.boundingBox.height, }, { width: signingSpace.width, height: signingSpace.height, } ); const resizeRatio = newSize.width / annotation.boundingBox.width; const newLeft = signingSpace.left + signingSpace.width / 2 - newSize.width / 2; const newTop = signingSpace.top + signingSpace.height / 2 - newSize.height / 2;
const newBoundingBox = new PSPDFKit.Geometry.Rect({ left: newLeft, top: newTop, width: newSize.width, height: newSize.height, });
if (annotation.lines) { const newLines = annotation.lines.map((line) => { return line.map((point) => { return new PSPDFKit.Geometry.DrawingPoint({ x: newLeft + (point.x - annotation.boundingBox.left) * resizeRatio, y: newTop + (point.y - annotation.boundingBox.top) * resizeRatio, }); }); });
instance.update( annotation .set("boundingBox", newBoundingBox) .set("lines", newLines) .set("lineWidth", annotation.lineWidth * resizeRatio) ); } else { instance.update(annotation.set("boundingBox", newBoundingBox)); }}
function fitIn(size, containerSize) { const { width, height } = size;
const widthRatio = containerSize.width / width; const heightRatio = containerSize.height / height;
const ratio = Math.min(widthRatio, heightRatio);
return { width: width * ratio, height: height * ratio, };}
export const initialToolbarItems = [ { type: "sidebar-thumbnails" }, { type: "sidebar-bookmarks" }, { type: "zoom-in" }, { type: "zoom-out" }, { type: "spacer" },];
import PSPDFKit from "@nutrient-sdk/viewer";import { attachListeners, initialToolbarItems } from "./index";
let instance = null;
const buttons = { saveButton: null, finishButton: { type: "custom", title: "Finish Signing", className: "finish-signing", name: "sign", async onPress() { // When "Finish Signing" is pressed, after the user // has added an ink signature, we proceed to apply // a digital signature to the document. From this // point on the integrity of the file is guaranteed. try { await instance.signDocument(null, { // The example signing microservice we are using // expects the "user-1-with-rights" token when // invoking its endpoint. Nutrient Document Engine forwards // any value specified in "signingToken" to it. signingToken: "user-1-with-rights", }); console.log("New signature added to the document!"); } catch (error) { console.error(error); } }, }, resetButton: { type: "custom", title: "Reset", name: "reset", async onPress() { localStorage.removeItem( "examples/digital-signatures-sign/lastUsedServerDocumentId" ); location.href = "/digital-signatures-sign"; }, },};
export default function load(defaultConfiguration) { const { toolbarItems } = defaultConfiguration;
// split the rest of the toolbar items from the save button so that // later we can keep it as the last item while adding the sign button buttons.saveButton = toolbarItems[toolbarItems.length - 1];
return PSPDFKit.load({ ...defaultConfiguration, toolbarItems: initialToolbarItems, styleSheets: ["/digital-signatures-sign/static/styles.css"], initialViewState: new PSPDFKit.ViewState({ showSignatureValidationStatus: PSPDFKit.ShowSignatureValidationStatusMode.IF_SIGNED, }), }).then(async (_instance) => { instance = _instance; console.log("Nutrient Web SDK successfully loaded!!", instance); attachListeners(instance, buttons);
return instance; });}
import PSPDFKit from "@nutrient-sdk/viewer";import { attachListeners, initialToolbarItems } from "./index";
let forge = null;let instance = null;
const buttons = { saveButton: null, finishButton: { type: "custom", title: "Finish Signing", className: "finish-signing", name: "sign", async onPress() { // When "Finish Signing" is pressed, after the user // has added an ink signature, we proceed to apply // a digital signature to the document. From this // point on the integrity of the file is guaranteed. try { await instance.signDocument(null, generatePKCS7); console.log("New signature added to the document!"); } catch (error) { console.error(error); } }, }, resetButton: { type: "custom", title: "Reset", name: "reset", async onPress() { location.href = "/digital-signatures-sign"; }, },};
export default function load(defaultConfiguration) { const { toolbarItems } = defaultConfiguration;
import("./static/forge.min.js").then(({ default: _forge }) => { forge = _forge; });
// split the rest of the toolbar items from the save button so that // later we can keep it as the last item while adding the sign button buttons.saveButton = toolbarItems[toolbarItems.length - 1];
return PSPDFKit.load({ ...defaultConfiguration, toolbarItems: initialToolbarItems, styleSheets: ["/digital-signatures-sign/static/styles.css"], initialViewState: new PSPDFKit.ViewState({ showSignatureValidationStatus: PSPDFKit.ShowSignatureValidationStatusMode.IF_SIGNED, }), async trustedCAsCallback() { // The particular certificate + private key that we are going to use // for signing this example were issued by this CA that we are going // to use for validation after signing. const response = await fetch("/digital-signatures-sign/static/ca.pem"); const cert = await response.text();
return [cert]; }, }).then(async (_instance) => { instance = _instance; console.log("Nutrient Web SDK successfully loaded!!", instance); attachListeners(instance, buttons);
return instance; });}
// Naive implementation that fetches the private key over the network.// Do not use it for a production deployment.async function generatePKCS7({ fileContents }) { const certificatePromise = fetch( "/digital-signatures-sign/static/certificate.pem" ).then((response) => response.text()); const privateKeyPromise = fetch( "/digital-signatures-sign/static/private-key.pem" ).then((response) => response.text()); const [certificatePem, privateKeyPem] = await Promise.all([ certificatePromise, privateKeyPromise, ]); const certificate = forge.pki.certificateFromPem(certificatePem); const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
const p7 = forge.pkcs7.createSignedData();
p7.content = new forge.util.ByteBuffer(fileContents); p7.addCertificate(certificate); p7.addSigner({ key: privateKey, certificate: certificate, digestAlgorithm: forge.pki.oids.sha256, authenticatedAttributes: [ { type: forge.pki.oids.contentType, value: forge.pki.oids.data, }, { type: forge.pki.oids.messageDigest, }, { type: forge.pki.oids.signingTime, value: new Date(), }, ], });
p7.sign({ detached: true });
return stringToArrayBuffer(forge.asn1.toDer(p7.toAsn1()).getBytes());}
// https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-Stringfunction stringToArrayBuffer(binaryString) { const buffer = new ArrayBuffer(binaryString.length); let bufferView = new Uint8Array(buffer);
for (let i = 0, len = binaryString.length; i < len; i++) { bufferView[i] = binaryString.charCodeAt(i); }
return buffer;}
This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.