Skip to main content Skip to footer

Programmatically Extract Signatures from PDF Using a JavaScript Viewer

Imagine this: you’ve spent weeks, or even months, looking for the perfect home for you and your family. You have finally found the house that is the dream home for your entire family! It’s been a long road, but you finally put in your offer for the home, waited another week or two, and got an answer: THE HOUSE IS YOURS! These are exciting times, but it’s just the beginning of the process, and a ton of paperwork is headed your way.

In years past, it was necessary to drive to your realtor's office or a lawyer's office, sit in a windowless room, wait for the realtor and/or the lawyer to show up, then plod through reams of paper, to initial every page, and signing a multitude of other pages with your full signature. If you miss a page or pages get stuck together, you used to end up driving back to the office and signing the missed page(s).

Enter the brilliance of technology and all its benefits for automating and making life easier for everyone. Now it is not only possible but also probable that you would be asked to go through this process in the comfort of your home or office through an online portal or, at a minimum, with the paperwork emailed to your inbox!

Although this process has been around for quite some time, GrapeCity has tools to make this process even easier! With this blog, you will learn how to utilize GrapeCity Document's JavaScript PDF Viewer (GcPdfViewer) and associated components to implement this process quickly and easily with a few simple lines of code!

Ready to Try it Out? Download GrapeCity Documents for PDF to get the GcPdfViewer Today!

The Setup of the Application

Your verbal offer has been accepted, and it’s time to put in the official paperwork, including the offer and some Earnest money. A sample is shown below (and we will work with this document throughout this process).

Extract Signatures PDF

This document has several pages, each requiring the buyer and seller's signature. We will now set up the interface through JavaScript to gather the appropriate signatures on this document, display it in the GrapeCity GcPdfViewer, collect the required signatures, and then download the signed document for the record. IMPORTANT NOTE: Download the full demonstration. It will set up our SupportAPI, a required component to modify and save PDF files.

Step 1 - Walk Through

If the desired outcome is for a quick testing option, I highly recommend downloading the complete project from here - then click on the icon representing a zip file:

Zip Logo

This will download the complete code. Suppose you want to do this from scratch and get a profound understanding of the tools. In that case, we suggest setting up your own environment by following along on this documentation: Configure the PDF Editor. Once you do either of these, see the explanations below for how/why we set up the source files for collecting signatures.

Step 2 - Setting up the Signature Blocks

After configuring an environment that will load the sample file, you can now examine how to set up this file to add signatures and collect them in order (bringing the user to each signature block in order) by making these changes.

Within the config.js file, the first thing we do is create an array of locations for the signatures:

 // Graphical signature locations.
    // Note that the { x, y } origin is at the bottom left.
    var graphicalSignatureLocationsBottomLeft = {
        // The key is the name of the PDF file in lower case without extension.
        "sign-pdf-in-order": [
            { pageIndex: 0, x: 187, y: 73, w: 80, h: 20, title: "Buyer" },
            { pageIndex: 0, x: 420, y: 73, w: 80, h: 20, title: "Seller", color: "#1234dd" },
            { pageIndex: 1, x: 187, y: 73, w: 80, h: 20, title: "Buyer" },
            { pageIndex: 1, x: 420, y: 73, w: 80, h: 20, title: "Seller", color: "#1234dd" },
            { pageIndex: 2, x: 187, y: 73, w: 80, h: 20, title: "Buyer" },
            { pageIndex: 2, x: 420, y: 73, w: 80, h: 20, title: "Seller", color: "#1234dd" },
            { pageIndex: 3, x: 187, y: 73, w: 80, h: 20, title: "Buyer" },
            { pageIndex: 3, x: 420, y: 73, w: 80, h: 20, title: "Seller", color: "#1234dd" },
            { pageIndex: 4, x: 187, y: 73, w: 80, h: 20, title: "Buyer" },
            { pageIndex: 4, x: 420, y: 73, w: 80, h: 20, title: "Seller", color: "#1234dd" },
            { pageIndex: 5, x: 187, y: 73, w: 80, h: 20, title: "Buyer" },
            { pageIndex: 5, x: 420, y: 73, w: 80, h: 20, title: "Seller", color: "#1234dd" }
        ]
    };

This code places a buyer and seller signature block on each page at specific locations and color-codes the block assigned to the “seller”.

Next, we set up where the date will appear concerning the signatures, as well as configured the viewer options so that the file can be saved and the signatures collected:

  // Parameters for the "sign date" labels
    const signDateLabelParams = {
        // x offset from the position of the graphical signature
        offset_x: 101,
        // y offset from the position of the graphical signature
        offset_y: 0,
        // label width
        w: 58,
        // label height
        h: 14
    }

//ensure we can reach the SupportAPI so we can sign and save the document
    getViewerOptions = function getViewerOptions() {
        return {
            supportApi: {
                apiUrl: "http://localhost:5004/api/pdf-viewer",  // e.g. "http://localhost:5004/api/pdf-viewer";
                token: "support-api-demo-net-core-token-2021", // e.g. "support-api-demo-net-core-token";
                webSocketUrl: false
            },
            restoreViewStateOnLoad: false
        };
    }

//setup the viewer so we have some panels available for editing, then register and populate the 
//signature blocks.
    configureViewerUI = function (viewer) {

        viewer.addDefaultPanels();
        viewer.addAnnotationEditorPanel();
        //now bring document focus to the first signature on the page and scroll through
        //all signatures.
        viewer.onAfterOpen.register(function () {
            populateSignature(viewer);
        });
    }

Next, we need to add a few more functions for this to work as designed:

  • populateSignature() - The main function to drive the collection of the signatures
  • onSignatureLinkEvent() - triggered when signature blocks are clicked, then triggers the scrolling through remaining signatures - scrolToNextSignature().
  • scrollToNextSignature() - changes focus to subsequent signature blocks in the order of the array.

The populateSignature() function sets up all the signature blocks in the appropriate locations using the graphicalSignatureLocationsBottom array, along with labels, colors, and the event driver (onSignatureLinkEvent). It then repaints the document and waits for users to sign (call the onSignatureLinkEvent).

async function populateSignature(viewer) {
        viewer.__onSignatureLinkEvent = onSignatureLinkEvent;
        const locationsBottomLeft = graphicalSignatureLocationsBottomLeft[viewer.fileName.toLowerCase().replace(".pdf", "")];
        if (locationsBottomLeft) {
            for (let i = 0; i < locationsBottomLeft.length; i++) {
                const locationInfo = locationsBottomLeft[i];
                const pageIndex = parseInt(locationInfo.pageIndex);
                const signTitle = locationInfo.title;

                const rect = [locationInfo.x, locationInfo.y, locationInfo.x + locationInfo.w, locationInfo.y + locationInfo.h];
                const signUiId = "signui_" + i;

                const freeTextLabel = {
                    annotationType: 3, // AnnotationTypeCode.FREETEXT
                    subject: signUiId,
                    borderStyle: { width: 1, style: 2 },
                    fontSize: 6,
                    appearanceColor: "#fff59d",
                    color: locationInfo.color || "#f44336",
                    contents: "Sign Here",
                    textAlignment: 1,
                    rect: [rect[0], rect[3] - 12, rect[0] + 35, rect[3]]
                };
                await viewer.addAnnotation(pageIndex, freeTextLabel, { skipPageRefresh: true });
                const viewerSelector = "#" + viewer.hostElement.id;
                const linkAnnotation = {
                    annotationType: 2, // AnnotationTypeCode.LINK
                    subject: signUiId,
                    linkType: "js",
                    borderStyle: { width: 0, style: 5 },
                    color: "#2196f3",
                    jsAction: `if(app.viewerType == 'GcPdfViewer') {
					var viewer = GcPdfViewer.findControl("${viewerSelector}");
					viewer.__onSignatureLinkEvent(viewer, ${pageIndex}, "${signTitle}", "${signUiId}", ${rect[0]}, ${rect[1]}, ${(rect[2] - rect[0])}, ${(rect[3] - rect[1])});
				}`,
                    rect: rect,
                    title: signTitle
                };
                await viewer.addAnnotation(pageIndex, linkAnnotation, { skipPageRefresh: true });

            }
            viewer.repaint();
        }

When users begin signing the document the onSignatureLinkEvent() function is called, which essentially calls up the signature tool in GcPdfViewer (.showSignTool()), then removes the placeholder for the signature block and replaces it with the users signature. The function then calls the “scrollToNextSignature()” function to get to the next appropriate signature block for signing.

function onSignatureLinkEvent(viewer, pageIndex, signTitle, signId, x, y, w, h) {
        viewer.showSignTool({
            subject: "AddGraphicalSignature", tabs: ["Type", "Draw"],
            dialogLocation: "center",
            pageIndex: pageIndex,
            title: signTitle,
            location: { x: x, y: y },
            canvasSize: { width: w * 5, height: h * 5 },
            destinationScale: 1 / 5,
            convertToContent: true,
            afterAdd: function (result) {
                // remove current Sign UI and scroll to next one:
                removeSignature(viewer, signId).then(function () {
                    viewer.addAnnotation(pageIndex, {
                        annotationType: 3, borderStyle: { width: 0 },
                        fontSize: 10, contents: new Date().toLocaleDateString("en-US"),
                        rect: [x + signDateLabelParams.offset_x, y + signDateLabelParams.offset_y, x + signDateLabelParams.offset_x + signDateLabelParams.w, y + signDateLabelParams.offset_y + signDateLabelParams.h],
                        convertToContent: true
                    },
                        { skipPageRefresh: false }).then(function () {
                            scrollToNextSignature(viewer, signTitle);
                        });
                });
            }
        });

The last two functions in our application setup are the removeSignature() and scrollToNextSignature() functions which are called from the above.

async function removeSignature(viewer, signId) {
        const annotations = await viewer.findAnnotations(signId, { findField: "subject", findAll: true });
        for (let data of annotations) {
            await viewer.removeAnnotation(data.pageIndex, data.annotation.id);
        }
    };

    async function scrollToNextSignature(viewer, signTitle) {
        let linkAnnotations = await viewer.findAnnotations(signTitle, { findField: "title", findAll: false });
        if (linkAnnotations.length === 0)
            linkAnnotations = await viewer.findAnnotations(2, { findField: "annotationType", findAll: false });
        if (linkAnnotations.length > 0) {
            var linkAnnotationInfo = linkAnnotations[0].annotation.title !== signTitle && linkAnnotations[1] && linkAnnotations[1].annotation.title === signTitle ? linkAnnotations[1] : linkAnnotations[0];
            let annotationElement = viewer.scrollView.querySelector(`section.linkAnnotation[data-annotation-id='${linkAnnotationInfo.id}']`);
            if (annotationElement) {
                annotationElement.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
            } else {
                pageElement = viewer.scrollView.querySelector(`div.page[data-index='${linkAnnotationInfo.pageIndex}']`);
                if (pageElement) {
                    pageElement.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });
                    setTimeout(function () { viewer.repaint([linkAnnotationInfo.pageIndex]); }, 400);
                }
            }
            if (linkAnnotationInfo.pageIndex < viewer.pageCount - 1) { viewer.repaint([linkAnnotationInfo.pageIndex + 1]); }
        } else {
            alert("The document is fully signed.");
        }
    }

Once you have completed these changes to the config.js file, be sure to check your app.js and index.html files to ensure you have them set up correctly, as noted below:

app.js

window.onload = function() {	
	const viewer = new GcPdfViewer("#viewer", getViewerOptions());
	configureViewerUI(viewer);
	viewer.open("assets/pdf/sign-pdf-in-order.pdf");
}

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Sign PDF in order</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
	
    <link rel="stylesheet" href="./src/styles.css">	
	<script src="node_modules/@grapecity/gcpdfviewer/gcpdfviewer.js"></script>
    <script src="./src/app.js"></script>
	<script src="./src/config.js"></script>
</head>

<body>
	<div id="viewer"></div>
</body>

</html>

After completing these steps, you should have a good understanding of the processes and functions and be able to modify this to meet the needs of your PDF signing application!

Tune in in the coming months for a second edition of this which will explain how to collect signatures in a document in an order and enforce the order—in other words, having a buyer or seller sign before one or the other.

Until then, happy coding, and don’t hesitate to contact us for help with this or other examples!

Ready to Try it Out? Download GrapeCity Documents for PDF to get the GcPdfViewer Today!

Tags:

comments powered by Disqus