Some test text!

Loading...
Guides

Freeform rotation for custom rectangular annotations

WebViewer introduced freeform rotation for 8 annotation types: Line, Polygon, Polyline, Freehand, Ellipse, Rectangle, Stamp, FreeText. This guide will walk you through the steps to enable freeform rotation on a custom rectangular annotation.

First let's define what's a rectangular annotation: If the annotation can be drawn inside a rectangle based on their bounding box coordinates, then it's a rectangular annotation.

The base custom annotation

It's not the purpose of this guide to thoroughly talk about the steps to create a custom annotation. That said, we will just post the base code for the custom annotation.

If you are looking for a more in-depth explanation on how to create a custom annotation, please check this guide.

Webviewer(...).then((instance) => {
  const { Core, UI } = instance;
  const { Annotations, annotationManager, Tools, documentViewer } = Core;

  // Beginning Custom Annotation Class
  class Rotatable extends Annotations.CustomAnnotation {
    constructor() {
      super('rotatable');
      this.Subject = 'Rotatable';
      this.selectionModel = Annotations.BoxSelectionModel;
    }

    draw(ctx, pageMatrix) {
      this.setStyles(ctx, pageMatrix);

      const x = this.X;
      const y = this.Y;
      const width = this.Width;
      const height = this.Height;

      ctx.translate(x, y);
      ctx.beginPath();
      ctx.moveTo(0, 0);
      ctx.lineTo(width, 0);
      ctx.lineTo(width, height);
      ctx.lineTo(0, height);
      ctx.lineTo(0, 0);
      ctx.lineTo(width, height);
      ctx.moveTo(width, 0);
      ctx.lineTo(0, height);
      ctx.stroke();
    }
  }

  Rotatable.prototype.elementName = 'rotatable';

  annotationManager.registerAnnotationType(Rotatable.prototype.elementName, Rotatable);
  // End Custom Annotation Class

  // Beginning Tool
  class RotatableCreateTool extends Tools.GenericAnnotationCreateTool {
    constructor(documentViewer) {
      super(documentViewer, Rotatable);
    }
  }

  const rotatableToolName = 'AnnotationCreateRotatable';

  const rotatableTool = new RotatableCreateTool(documentViewer);
  UI.registerTool({
    toolName: rotatableToolName,
    toolObject: rotatableTool,
    buttonImage: '<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 64 64">' +
      '<line x1="9.37" x2="54.63" y1="9.37" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
      '<line x1="9.37" x2="9.37" y1="54.63" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
      '<line x1="54.63" x2="54.63" y1="9.37" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
      '<line x1="9.37" x2="54.63" y1="54.63" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
      '<line x1="9.37" x2="54.63" y1="9.37" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
      '<line x1="9.37" x2="54.63" y1="54.63" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
      '</svg>',
    buttonName: 'rotatableToolButton',
    tooltip: 'Rotatable'
  }, Rotatable);

  UI.setHeaderItems((header) => {
    header.getHeader('toolbarGroup-Shapes').get('freeHandToolGroupButton').insertBefore({
      type: 'toolButton',
      toolName: rotatableToolName
    });
  });
  // End Tool
});

The convenient mixin

WebViewer provides a convenient mixin out of the box for you to plug into the custom annotation class. The RectangularCustomAnnotationRotationMixin will add the following methods:

  • rotate: Changes the Rotation property of the annotation and updates the bounding box.
  • getUnrotatedDimensions: Calculates the correct dimension for drawing.
  • getRotatedAnnotationBoundingBoxRect: Calculates the bounding box dimensions.
  • serialize / deserialize: Make sure the annotation gets correctly saved into the PDF when downloading the document and that it will load fine.
// Beginning Custom Annotation Class
  class Rotatable extends Annotations.CustomAnnotation {
    ...
  }
  
  Object.assign(Rotatable.prototype, Annotations.RotationUtils.RectangularCustomAnnotationRotationMixin);

  Rotatable.prototype.elementName = 'rotatable';

  annotationManager.registerAnnotationType(Rotatable.prototype.elementName, Rotatable);
  // End Custom Annotation Class
});

Drawing the rotated annotation

With all those methods already available within the class, it's time to use getUnrotatedDimensions and the angle to draw the annotation. Not only that, but the canvas need to be rotated as well.

class Rotatable extends Annotations.CustomAnnotation {
    ...
    draw(ctx, pageMatrix) {
      this.setStyles(ctx, pageMatrix);

      const { x, y, width, height } = this.getUnrotatedDimensions();

      ctx.translate(x + width / 2, y + height / 2);
      ctx.rotate(-Annotations.RotationUtils.getRotationAngleInRadiansByDegrees(this['Rotation']));
      ctx.translate(-x - width / 2, -y - height / 2);

      ctx.translate(x, y);
      ctx.beginPath();
      ctx.moveTo(0, 0);
      ctx.lineTo(width, 0);
      ctx.lineTo(width, height);
      ctx.lineTo(0, height);
      ctx.lineTo(0, 0);
      ctx.lineTo(width, height);
      ctx.moveTo(width, 0);
      ctx.lineTo(0, height);
      ctx.stroke();
    }
    ...
  }

Adding the rotation control handle

In order to be able to actually rotate the annotation with the mouse movement, you will need to add a rotation control handle. In WebViewer, this is done by a selection model.

In this custom selection model, you have only to check if the rotation control handle is enabled and add the control handle itself to the list of control handles.

// Beginning Custom Annotation Class
  ...
  // End Custom Annotation Class

  // Beginning Selection Model
  class RotatableSelectionModel extends Annotations.BoxSelectionModel {
    constructor(annotation, canModify, isSelected, documentViewer) {
      super(annotation, canModify, isSelected, documentViewer);

      const controlHandles = this.getControlHandles();

      if (canModify && documentViewer.getAnnotationManager().isFreeformRotationEnabled() && annotation.hasRotationControlEnabled()) {
        controlHandles.push(new Annotations.RotationControlHandle(Annotations.ControlHandle['rotationHandleWidth'], Annotations.ControlHandle['rotationHandleHeight'], 40, annotation, documentViewer));
      }
    }
  }
  // End Selection Model

  // Beginning Tool
  ...
  // End Tool

The complete code

Webviewer(...).then((instance) => {
  const { Core, UI } = instance;
  const { Annotations, annotationManager, Tools, documentViewer } = Core;

  // Beginning Custom Annotation Class
  class Rotatable extends Annotations.CustomAnnotation {
    constructor() {
      super('rotatable');
      this.Subject = 'Rotatable';
      this.selectionModel = RotatableSelectionModel;
    }

    draw(ctx, pageMatrix) {
      this.setStyles(ctx, pageMatrix);

      const { x, y, width, height } = this.getUnrotatedDimensions();

      ctx.translate(x + width / 2, y + height / 2);
      ctx.rotate(-Annotations.RotationUtils.getRotationAngleInRadiansByDegrees(this['Rotation']));
      ctx.translate(-x - width / 2, -y - height / 2);

      ctx.translate(x, y);
      ctx.beginPath();
      ctx.moveTo(0, 0);
      ctx.lineTo(width, 0);
      ctx.lineTo(width, height);
      ctx.lineTo(0, height);
      ctx.lineTo(0, 0);
      ctx.lineTo(width, height);
      ctx.moveTo(width, 0);
      ctx.lineTo(0, height);
      ctx.stroke();
    }
  }

  Object.assign(Rotatable.prototype, Annotations.RotationUtils.RectangularCustomAnnotationRotationMixin);

  Rotatable.prototype.elementName = 'rotatable';

  annotationManager.registerAnnotationType(Rotatable.prototype.elementName, Rotatable);
  // End Custom Annotation Class

  // Beginning Selection Model
  class RotatableSelectionModel extends Annotations.BoxSelectionModel {
    constructor(annotation, canModify, isSelected, documentViewer) {
      super(annotation, canModify, isSelected, documentViewer);

      const controlHandles = this.getControlHandles();

      if (canModify && documentViewer.getAnnotationManager().isFreeformRotationEnabled() && annotation.hasRotationControlEnabled()) {
        controlHandles.push(new Annotations.RotationControlHandle(Annotations.ControlHandle['rotationHandleWidth'], Annotations.ControlHandle['rotationHandleHeight'], 40, annotation, documentViewer));
      }
    }
  }
  // End Selection Model

  // Beginning Tool
  class RotatableCreateTool extends Tools.GenericAnnotationCreateTool {
    constructor(documentViewer) {
      super(documentViewer, Rotatable);
    }
  }

  const rotatableToolName = 'AnnotationCreateRotatable';

  const rotatableTool = new RotatableCreateTool(documentViewer);
  UI.registerTool({
    toolName: rotatableToolName,
    toolObject: rotatableTool,
    buttonImage: '<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 64 64">' +
      '<line x1="9.37" x2="54.63" y1="9.37" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
      '<line x1="9.37" x2="9.37" y1="54.63" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
      '<line x1="54.63" x2="54.63" y1="9.37" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
      '<line x1="9.37" x2="54.63" y1="54.63" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
      '<line x1="9.37" x2="54.63" y1="9.37" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
      '<line x1="9.37" x2="54.63" y1="54.63" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
      '</svg>',
    buttonName: 'rotatableToolButton',
    tooltip: 'Rotatable'
  }, Rotatable);

  UI.setHeaderItems((header) => {
    header.getHeader('toolbarGroup-Shapes').get('freeHandToolGroupButton').insertBefore({
      type: 'toolButton',
      toolName: rotatableToolName
    });
  });
  // End Tool
});

Get the answers you need: Support

UPCOMING WEBINAR: Live tech update and run-through. October 21 @ 11am PDT