Get Started

play_arrow

Learn more

play_arrow

Common use cases

play_arrow

Open a document

play_arrow

Save a document

play_arrow

Viewer

play_arrow

UI Customization

play_arrow

Annotations

play_arrow

Collaboration

play_arrow

Forms

play_arrow

Signature

play_arrow

Measurement

play_arrow

PDF.js Express REST API

play_arrow

Add custom annotations

PDF.js Express Web Viewer allows you to create your own annotations that can be customized in several different ways. You can change the appearance and behaviors of the annotation, selection box and control handles.

There are two main parts of an annotation, the annotation tool, which is responsible for drawing the annotation and selecting properties such as color, fill, size, etc, and the annotation itself, which is what you see displayed on the PDF while you are viewing it.

View samples on github

Creating custom annotations

In most scenerios, extending or changing the functionality of existing annotations is the easiest way to create your own custom annotations.

Most annotations extends the markup annotation, which is a basic annotation with stroke color, fill color, opacity, etc.

The most commonly overriden function is the draw function, which is the function that is called every time the annotation needs to be rendered. It is passed a canvas context that can be used to draw the annotation. Overriding this draw function lets us choose exactly how the annotation is rendered.

First let's create a basic triangle annotation class that extends the MarkupAnnotation class. We'll also set the elementName to triangle. elementName is name of the annotation when in XFDF form.

WebViewer(
  // ...
).then((instance) => {
  const { Annotations } = instance;

  const TriangleAnnotation = function() {
    Annotations.MarkupAnnotation.call(this);
    this.Subject = 'Triangle';
  };

  TriangleAnnotation.prototype = new Annotations.MarkupAnnotation();

  TriangleAnnotation.prototype.elementName = 'triangle';
});

Now we are going to override the draw function to make it draw a triangle on the canvas. We are also going to call setStyles, which sets style properties for us like stroke, fill, etc.

TriangleAnnotation.prototype.draw = function(ctx, pageMatrix) {
  this.setStyles(ctx, pageMatrix);

  // first we need to translate to the annotation's x/y coordinates so that it's
  // drawn in the correct location
  ctx.translate(this.X, this.Y);
  ctx.beginPath();

  // Width and Height are properties that exist on the base Annotation class
  ctx.moveTo(this.Width / 2, 0);
  ctx.lineTo(this.Width, this.Height);
  ctx.lineTo(0, this.Height);

  ctx.closePath();
  ctx.fill();
  ctx.stroke();
};

Creating custom tools

To allow a user to actually add the annotation to a document we'll need to create a tool. Our triangle just depends on two mouse points so we can inherit from the GenericAnnotationCreateTool, which is just a tool that creates an annotation from two points.

// we also need to access the Tools namespace from the instance
const { Annotations, Tools } = instance;

const TriangleCreateTool = function(docViewer){
  // TriangleAnnotation is the constructor function for our annotation we defined previously
  Tools.GenericAnnotationCreateTool.call(this, docViewer, TriangleAnnotation);
};

TriangleCreateTool.prototype = new Tools.GenericAnnotationCreateTool();

Adding the tool to the UI

Now that our tool and annotation are created, we can add it to the UI so that the user can select it and create it.

// access annotManager and docViewer objects from the instance
const { Annotations, Tools, annotManager, docViewer } = instance;

const triangleToolName = 'AnnotationCreateTriangle';

// register the annotation type so that it can be saved to XFDF files
annotManager.registerAnnotationType(TriangleAnnotation.prototype.elementName, TriangleAnnotation);

const triangleTool = new TriangleCreateTool(docViewer);
instance.registerTool({
  toolName: triangleToolName,
  toolObject: triangleTool,
  buttonImage: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">' +
    '<path d="M12 7.77L18.39 18H5.61L12 7.77M12 4L2 20h20L12 4z"/>' +
    '<path fill="none" d="M0 0h24v24H0V0z"/>' +
  '</svg>',
  buttonName: 'triangleToolButton',
  tooltip: 'Triangle'
}, TriangleAnnotation);

instance.setHeaderItems(header => {
  const triangleButton = {
    type: 'toolButton',
    toolName: triangleToolName
  };
  header.get('freeHandToolGroupButton').insertBefore(triangleButton);
});

docViewer.on('documentLoaded', () => {
  // set the tool mode to our tool so that we can start using it right away
  instance.setToolMode(triangleToolName);
});

The code above does a few things. First we call registerAnnotationType, which makes sure the annotation will be serialized and saved when the XFDF is exported.

We then register the tool with registerTool, which lets the UI know of its existence.

Next, we add the tool to the header using setHeaderItems, which lets the user click on and select the tool.

Custom control handles

Control handles are the little dots that appear when the annotation selected, and they let you resize and change the shape of annotations.

After creating some triangles you might notice that the selection box is a rectangle and has eight control handles. This isn't terrible but we could probably make it better by having a control handle for each corner and drawing the selection box around the edge of the annotation.

First let's change the control points so that there is one for each vertex. To do this we'll add an array of vertices on the annotation and then define a new selection model and control handles to resize the annotation.

We'll add the array to the annotation constructor:

const TriangleAnnotation = function () {
  Annotations.MarkupAnnotation.call(this);
  this.Subject = 'Triangle';
  this.vertices = [];
  const numVertices = 3;
  for (let i = 0; i < numVertices; ++i) {
    this.vertices.push({
      x: 0,
      y: 0
    });
  }
};

Then we'll update the draw function on the annotation to use the vertices for drawing. We do this so that we can have control over each point of the triangle rather than relying on the width and height of the annotation.

TriangleAnnotation.prototype.draw = function (ctx, pageMatrix) {
  this.setStyles(ctx, pageMatrix);

  ctx.beginPath();
  ctx.moveTo(this.vertices[0].x, this.vertices[0].y);
  ctx.lineTo(this.vertices[1].x, this.vertices[1].y);
  ctx.lineTo(this.vertices[2].x, this.vertices[2].y);
  ctx.closePath();
  ctx.fill();
  ctx.stroke();
};

Then for the tool we'll override the mouseMove function to set the vertices:

TriangleCreateTool.prototype.mouseMove = function(e) {
  // call the parent mouseMove first
  Tools.GenericAnnotationCreateTool.prototype.mouseMove.call(this, e);
  if (this.annotation) {
    this.annotation.vertices[0].x = this.annotation.X + this.annotation.Width / 2;
    this.annotation.vertices[0].y = this.annotation.Y;
    this.annotation.vertices[1].x = this.annotation.X + this.annotation.Width;
    this.annotation.vertices[1].y = this.annotation.Y + this.annotation.Height;
    this.annotation.vertices[2].x = this.annotation.X;
    this.annotation.vertices[2].y = this.annotation.Y + this.annotation.Height;

    // update the annotation appearance
    this.docViewer.getAnnotationManager().redrawAnnotation(this.annotation);
  }
};

At this point the drawing of the annotation should look the same as before, however you won't be able to move the annotation. To fix this let's create the custom selection model and control handles. Note that the code snippet below must be above the TriangleAnnotation definition.

const TriangleControlHandle = function (annotation, index) {
  this.annotation = annotation;
  // set the index of this control handle so that we know which vertex it corresponds to
  this.index = index;
};

TriangleControlHandle.prototype = new Annotations.ControlHandle();

Now we'll overwrite the getDimensions function which is used to calculate the control handles size and position.

// returns a rect that should represent the control handle's position and size
TriangleControlHandle.prototype.getDimensions = function (annotation, selectionBox, zoom) {
  let x = annotation.vertices[this.index].x;
  let y = annotation.vertices[this.index].y;

  // Use the default width and height
  const width = Annotations.ControlHandle.handleWidth / zoom;
  const height = Annotations.ControlHandle.handleHeight / zoom;

  // adjust for the control handle's own width and height
  x -= width * 0.5;
  y -= height * 0.5;
  return new Annotations.Rect(x, y, x + width, y + height);
};

Whenever a control handle is moved, we want to update the annotation vertices and redraw it. We also want to set the new size of the annotation using setRect. To do this we loop over all the annotations vertices and find the minimum and maximum values for x and y.

// this function is called when the control handle is dragged
TriangleControlHandle.prototype.move = function (annotation, deltaX, deltaY, fromPoint, toPoint) {
  // deltaX and deltaY represent how much the control handle moved
  annotation.vertices[this.index].x += deltaX;
  annotation.vertices[this.index].y += deltaY;

  // recalculate the X, Y, width and height of the annotation
  let minX = Number.MAX_VALUE;
  let maxX = -Number.MAX_VALUE;
  let minY = Number.MAX_VALUE;
  let maxY = -Number.MAX_VALUE;
  for (let i = 0; i < annotation.vertices.length; ++i) {
    const vertex = annotation.vertices[i];
    minX = Math.min(minX, vertex.x);
    maxX = Math.max(maxX, vertex.x);
    minY = Math.min(minY, vertex.y);
    maxY = Math.max(maxY, vertex.y);
  }

  const rect = new Annotations.Rect(minX, minY, maxX, maxY);
  annotation.setRect(rect);

  // return true if redraw is needed
  return true;
};

Now we can create the new selection model (extending the base selection model class) and add our control points to it.

// selection model creates the necessary control handles
const TriangleSelectionModel = function (annotation, canModify) {
  Annotations.SelectionModel.call(this, annotation, canModify);
  if (canModify) {
    const controlHandles = this.getControlHandles();
    // pass the vertex index to each control handle
    controlHandles.push(new TriangleControlHandle(annotation, 0));
    controlHandles.push(new TriangleControlHandle(annotation, 1));
    controlHandles.push(new TriangleControlHandle(annotation, 2));
  }
};

TriangleSelectionModel.prototype = new Annotations.SelectionModel();

Now we just have to assign the new TriangleSelectionModel as the TriangleAnnotation's selection model.

TriangleAnnotation.prototype.selectionModel = TriangleSelectionModel;

There should now be a control handle for each point of the triangle and if you drag them around you'll move that vertex of the triangle! However you may notice that if you try to drag and move the annotation it won't work. To fix this let's override the resize function on the annotation.

TriangleAnnotation.prototype.resize = function (rect) {
  // this function is only called when the annotation is dragged
  // since we handle the case where the control handles move
  const annotRect = this.getRect();
  const deltaX = rect.x1 - annotRect.x1;
  const deltaY = rect.y1 - annotRect.y1;

  // shift the vertices by the amount the rect has shifted
  this.vertices = this.vertices.map((vertex) => {
    vertex.x += deltaX;
    vertex.y += deltaY;
    return vertex;
  });
  this.setRect(rect);
};

Saving and loading custom annotations

If you tried to save and load this annotation you would notice that it isn't able to be reloaded. This is because PDF.js Express doesn't know that it needs to save the vertices array. To do this we can override the serialize and deserialize functions which are called when the annotation should be saved or loaded respectively.

TriangleAnnotation.prototype.serialize = function (element, pageMatrix) {
  const el = Annotations.MarkupAnnotation.prototype.serialize.call(this, element, pageMatrix);
  el.setAttribute('vertices', Annotations.XfdfUtils.serializePointArray(this.vertices, pageMatrix));
  return el;
};

TriangleAnnotation.prototype.deserialize = function (element, pageMatrix) {
  Annotations.MarkupAnnotation.prototype.deserialize.call(this, element, pageMatrix);
  this.vertices = Annotations.XfdfUtils.deserializePointArray(element.getAttribute('vertices'), pageMatrix);
}

After making this change you should be able to export XFDF and import the string back.