New: Create and edit Word documents with DOCX Editor in WebViewer

How to Convert a Scanned PDF to a Fillable Form Online

By Andrey Safonov | 2019 Apr 12

Sanity Image
Read time

5 min

This is a guide on how to leverage the Apryse WebViewer and the WebViewer Full API to embed a fast and user-friendly, pure client-side form builder into your web application.

You can watch how easy our form builder is to use in this YouTube demo I put together. Or try it out yourself in the form builder demo.


There’s no questioning PDF’s theoretical advantages when it comes to data capture from forms. PDF documents are vendor- and session-independent, and support a Swiss-Army-knife worth of interactive form elements, on top of digital signatures, encryption, and a host of the features.

But actually filling PDF forms in the real world is often frustrating. Hospitals and so on produce many static scanned or malformed PDFs. And all too often, these force users to retreat to old-fashioned printing, filling and signing by hand — a waste of time and trees.

At Apryse, we wanted to help our customers by making form building from static documents as simple and painless as possible, preferably, in a browser, where the feature could be enjoyed by the maximum number of users (from virtually anywhere). So we went and built a sample to show how we solve the problem using our PDF SDK.

Getting Started

You will first need to download the WebViewer SDK.

Next, unzip and navigate to the right directory from the command line.

When that’s done, run the following command:

npm install

This will start up a local server and open the WebViewer samples. (For more on how to integrate WebViewer into any JavaScript framework, such as React and Angular — check out our in-depth guide).

Navigate to http://localhost:3000 in your favourite browser. The form building sample is located under Forms/form-builder. You can now create your interactive form by either creating a new PDF or opening an existing document.

Using WebViewer to Create Form Fields via Config.js

The first step is to let the user place text annotations anywhere inside a PDF to delineate form fields — which users can easily move and resize accordingly. You are also able to easily customize the appearance of these annotations. You can choose from different colors, borders, font families, and more. (Feel free to read the detailed customization guide.)

Function addFormFieldAnnot allows you to add a text annotation — with custom parameters: type, name, value and flag.

  • Type determines whether the annotation is a signature field, a text field or a checkbox.
  • Name is useful later as a reference while iterating over fields — and when either programmatically filling or extracting the Value of that form field.
  • Value should be entered into the form field right away upon its creation.
  • Flag (a boolean) determines whether the field will be fillable by a user.
 const addFormFieldAnnot = (type, name, value, flag) => {
   var docViewer = readerControl.docViewer;
   var annotManager = docViewer.getAnnotationManager();

   var textAnnot = new Annotations.FreeTextAnnotation();
   textAnnot.PageNumber = 1;
   if (type === 'CHECK') {
     textAnnot.Width = 50;
     textAnnot.Height = 50;
   } else {
     textAnnot.Width = 200;
     textAnnot.Height = 50;
   textAnnot.X = 100;
   textAnnot.Y = 150;

   textAnnot.setPadding(new Annotations.Rect(0, 0, 0, 0));
   textAnnot.custom = {

   // set the type of annot
   textAnnot.FontSize = '20px';

   textAnnot.Author = annotManager.getCurrentUser();


Using the WebViewer Full API to Create PDF Form Fields

Next, function convertAnnotToFormField takes all the text annotations that were placed and sized accordingly and converts them into actual form fields. The next step — converting text annotations created in WebViewer into interactive PDF form fields — uses the Full API.

(Here is the full WebViewer guide if you need to access further advanced functionality.)

The sample is as follows:

const convertAnnotToFormField = async() => {
   // initialize
   const docViewer = readerControl.docViewer;
   const annotManager = docViewer.getAnnotationManager();
   const annotationsList = annotManager.getAnnotationsList();
   const currentDocument = docViewer.getDocument();

   await PDFNet.initialize();
   const pdfDoc = await currentDocument.getPDFDoc();

   await Promise.all( => {
     let field;

     if (typeof annot.custom !== 'undefined') {

       // create a form field based on the type of annotation
       if (annot.custom.type === 'TEXT') {
         field = await pdfDoc.fieldCreateFromStrings(annot.getContents(), PDFNet.Field.Type.e_text, annot.custom.value, '');
       } else if (annot.custom.type === 'SIGNATURE') {
         field = await pdfDoc.fieldCreateFromStrings(annot.getContents(), PDFNet.Field.Type.e_signature, '', '');
       } else if (annot.custom.type === 'CHECK') {
         field = await pdfDoc.fieldCreateFromStrings(annot.getContents(), PDFNet.Field.Type.e_check, '', '');
       } else {
         // exit early for other annotations

       // check if there is a flag
       if (annot.custom.flag === true) {
         field.setFlag(PDFNet.Field.Flag.e_read_only, true);
     } else {
       // exit early for other annotations

     // translate coordinates
     const annotRect = await annot.getRect();
     const setTopLeft = currentDocument.getPDFCoordinates(annot.getPageNumber() - 1, annotRect.x1, annotRect.y1);
     const setBottomRight = currentDocument.getPDFCoordinates(annot.getPageNumber() - 1, annotRect.x2, annotRect.y2);

     // create an annotation with a form field created
     const pageNumber = annot.getPageNumber();
     const newAnnot = await PDFNet.WidgetAnnot.create(pdfDoc, (await PDFNet.Rect.init(setTopLeft.x, setTopLeft.y, setBottomRight.x, setBottomRight.y)), field);

     // delete original annotation
     annotManager.deleteAnnotation(annot, false, true);

     // customize styles of the form field
     Annotations.WidgetAnnotation.getCustomStyles = function(widget) {
       if (widget instanceof Annotations.TextWidgetAnnotation) {
         return {
           'background-color': '#a5c7ff',
           color: 'white',
           'font-size': '20px'
       } else if (widget instanceof Annotations.SignatureWidgetAnnotation) {
         return {
           border: '1px solid #a5c7ff'

     // draw the annotation the viewer
     const page = await pdfDoc.getPage(pageNumber);
     await pdfDoc.refreshFieldAppearances();

   // import newly created form fields
   const fdfDoc = await pdfDoc.fdfExtract(PDFNet.PDFDoc.ExtractFlag.e_both);
   const xfdf = await fdfDoc.saveAsXFDFAsString();

   // refresh viewer

If you’re interested, this is how the sample works:

  1. It copies the annotated document inside of WebViewer and creates an instance of a PDF doc.
  2. It fetches all the annotations inside the marked up WebViewer document, namely, those whose custom attribute is either text, a signature, or a checkbox.
  3. It creates a type of interactive PDF form field matching the custom attribute.
  4. It figures out the correct coordinates for the new PDF field. <Canvas> annotation coordinates differ from coordinates stored in PDF; so a simple conversion is required. (To learn more about this conversion, refer to the PDF coordinate guide.)
  5. After converting coordinates, it inserts the interactive form field into the PDF doc. (You can customize embedded form field appearances by using CSS inside of our code.)

The final steps are to delete the original text annotations, save the document, and import new form fields into WebViewer so the user can see the changes.

Next Steps

We’re ultimately just scratching the surface of what can be done with Apryse SDK’s hundreds of unique features to extend the above sample. For example: integrating a feature that would snap annotations to a grid would make it a lot easier for users to align fields.

If you hit any stuck points, want to share how this solution worked out for you, or are just curious about the other capabilities of our SDKs — don’t hesitate to get in touch.

You can reach out to me here via email.

Sanity Image

Andrey Safonov

Director of Product

LinkedIn link

Share this post