Some test text!

Loading...
Guides

Freeform rotation for custom path-based 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 path-based annotation.

First let's define what's a path-based annotation: If the annotation can be drawn based on an array of points alone, then it's a path-based annnotation.

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 RotatablePath extends Annotations.CustomAnnotation {
    constructor() {
      super('rotatable-path');
      this.Subject = 'RotatablePath';
      this.path = [
        new Annotations.Point(this.X, this.Y),
        new Annotations.Point(this.X + this.Width, this.Y),
        new Annotations.Point(this.X + this.Width, this.Y + this.Height),
        new Annotations.Point(this.X, this.Y + this.Height),
      ];
      this.selectionModel = RotatableSelectionModel;
    }

    getPath() {
      return this.path;
    }

    setPath(newPath) {
      this.path = newPath;
    }

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

      ctx.beginPath();
      ctx.moveTo(this.path[0].x, this.path[0].y);
      ctx.lineTo(this.path[1].x, this.path[1].y);
      ctx.lineTo(this.path[2].x, this.path[2].y);
      ctx.lineTo(this.path[3].x, this.path[3].y);
      ctx.lineTo(this.path[0].x, this.path[0].y);
      ctx.lineTo(this.path[2].x, this.path[2].y);
      ctx.moveTo(this.path[1].x, this.path[1].y);
      ctx.lineTo(this.path[3].x, this.path[3].y);
      ctx.stroke();
    }

    resize(rect) {
      const { x1, y1 } = rect;
      const deltaX = x1 - this['X'];
      const deltaY = y1 - this['Y'];
      if (deltaX === 0 && deltaY === 0) {
        return;
      }
      for (let i = 0; i < this.path.length; i++) {
        this.path[i].translate(deltaX, deltaY);
      }

      this.adjustRect();
    }

    adjustRect() {
      let minX = this.path[0]['x'];
      let minY = this.path[0]['y'];
      let maxX = this.path[0]['x'];
      let maxY = this.path[0]['y'];

      this.path.forEach((point) => {
        if (point.x < minX) {
          minX = point.x;
        }
        if (point.x > maxX) {
          maxX = point.x;
        }
        if (point.y < minY) {
          minY = point.y;
        }
        if (point.y > maxY) {
          maxY = point.y;
        }
      });

      this['X'] = minX;
      this['Y'] = minY;
      this['Width'] = maxX - minX;
      this['Height'] = maxY - minY;
    }

    serialize(element, pageMatrix) {
      this.setCustomData('trn-path', JSON.stringify(this.path));
      
      return Annotations.CustomAnnotation.prototype.serialize.apply(this, arguments);
    }

    deserialize(element) {
      Annotations.CustomAnnotation.prototype.deserialize.apply(this, arguments);

      this.path = JSON.parse(this.getCustomData('trn-path'));
    }
  }

  RotatablePath.prototype.elementName = 'rotatable-path';

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

  // Beginning Control Handle
  class RotatablePathControlHandle extends Annotations.PathControlHandle {
    constructor(x, y, width, height, pathIndex) {
      super(x, y, width, height, pathIndex);

      this.pathIndex = pathIndex;
    }

    move(annotation, deltaX, deltaY) {
      annotation.path[this.pathIndex] = new Annotations.Point(annotation.path[this.pathIndex].x + deltaX, annotation.path[this.pathIndex].y + deltaY);

      annotation.adjustRect();

      return true;
    }
  }
  // End Control Handle

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

      const controlHandles = this.getControlHandles();

      if (canModify) {
        for (let i = 0; i < annotation.path.length; i++) {
          controlHandles.push(new RotatablePathControlHandle(annotation.path[i].x, annotation.path[i].y, Annotations.ControlHandle['handleWidth'], Annotations.ControlHandle['handleWidth'], i));
        }
      }
    }
  }
  // End Selection Model

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

    mouseMove(e) {
      super.mouseMove(e);

      if (this.annotation) {
        this.annotation.path[0].x = this.annotation.X;
        this.annotation.path[0].y = this.annotation.Y;
        this.annotation.path[1].x = this.annotation.X + this.annotation.Width;
        this.annotation.path[1].y = this.annotation.Y;
        this.annotation.path[2].x = this.annotation.X + this.annotation.Width;
        this.annotation.path[2].y = this.annotation.Y + this.annotation.Height;
        this.annotation.path[3].x = this.annotation.X;
        this.annotation.path[3].y = this.annotation.Y + this.annotation.Height;

        annotationManager.redrawAnnotation(this.annotation);
      }
    }
  }

  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'
  }, RotatablePath);

  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 path-based annotation class. The PathCustomAnnotationRotationMixin will add the following methods:

  • rotate: Changes the Rotation property of the annotation, updates the annotation path and updates the bounding box.
  • adjustRect: Adjusts the annotation's bounding box
  • serialize / deserialize: Make sure the annotation gets correctly saved into the PDF when downloading the document and that it will load fine.

Notice that this mixin already includes adjustRect and serialize / deserialize methods, so you can remove them from the custom annotation class body.

Lastly, for this mixin to work properly, the annotation class must implement two methods:

  • setPath: Updates the annotation's path (Array of points)
  • getPath: returns the annotation's path (Array of points)
// Beginning Custom Annotation Class
  class RotatablePath extends Annotations.CustomAnnotation {
    ...
  }
  
  Object.assign(RotatablePath.prototype, Annotations.RotationUtils.PathCustomAnnotationRotationMixin);

  RotatablePath.prototype.elementName = 'rotatable-path';

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

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.SelectionModel {
    constructor(annotation, canModify, isSelected, documentViewer) {
      super(annotation, canModify, isSelected, documentViewer);

      const controlHandles = this.getControlHandles();

      if (canModify) {
        for (let i = 0; i < annotation.path.length; i++) {
          controlHandles.push(new RotatablePathControlHandle(annotation.path[i].x, annotation.path[i].y, Annotations.ControlHandle['handleWidth'], Annotations.ControlHandle['handleWidth'], i));
        }

        if (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 RotatablePath extends Annotations.CustomAnnotation {
    constructor() {
      super('rotatable-path');
      this.Subject = 'RotatablePath';
      this.path = [
        new Annotations.Point(this.X, this.Y),
        new Annotations.Point(this.X + this.Width, this.Y),
        new Annotations.Point(this.X + this.Width, this.Y + this.Height),
        new Annotations.Point(this.X, this.Y + this.Height),
      ];
      this.selectionModel = RotatableSelectionModel;
    }

    getPath() {
      return this.path;
    }

    setPath(newPath) {
      this.path = newPath;
    }

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

      ctx.beginPath();
      ctx.moveTo(this.path[0].x, this.path[0].y);
      ctx.lineTo(this.path[1].x, this.path[1].y);
      ctx.lineTo(this.path[2].x, this.path[2].y);
      ctx.lineTo(this.path[3].x, this.path[3].y);
      ctx.lineTo(this.path[0].x, this.path[0].y);
      ctx.lineTo(this.path[2].x, this.path[2].y);
      ctx.moveTo(this.path[1].x, this.path[1].y);
      ctx.lineTo(this.path[3].x, this.path[3].y);
      ctx.stroke();
    }

    resize(rect) {
      const { x1, y1 } = rect;
      const deltaX = x1 - this['X'];
      const deltaY = y1 - this['Y'];
      if (deltaX === 0 && deltaY === 0) {
        return;
      }
      for (let i = 0; i < this.path.length; i++) {
        this.path[i].translate(deltaX, deltaY);
      }

      this.adjustRect();
    }
  }

  Object.assign(RotatablePath.prototype, Annotations.RotationUtils.PathCustomAnnotationRotationMixin);

  RotatablePath.prototype.elementName = 'rotatable-path';

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

  // Beginning Control Handle
  class RotatablePathControlHandle extends Annotations.PathControlHandle {
    constructor(x, y, width, height, pathIndex) {
      super(x, y, width, height, pathIndex);

      this.pathIndex = pathIndex;
    }

    move(annotation, deltaX, deltaY) {
      annotation.getPath()[this.pathIndex] = new Annotations.Point(annotation.getPath()[this.pathIndex].x + deltaX, annotation.getPath()[this.pathIndex].y + deltaY);

      annotation.adjustRect();

      return true;
    }
  }
  // End Control Handle

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

      const controlHandles = this.getControlHandles();

      if (canModify) {
        for (let i = 0; i < annotation.path.length; i++) {
          controlHandles.push(new RotatablePathControlHandle(annotation.path[i].x, annotation.path[i].y, Annotations.ControlHandle['handleWidth'], Annotations.ControlHandle['handleWidth'], i));
        }

        if (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, RotatablePath);
    }

    mouseMove(e) {
      super.mouseMove(e);

      if (this.annotation) {
        this.annotation.path[0].x = this.annotation.X;
        this.annotation.path[0].y = this.annotation.Y;
        this.annotation.path[1].x = this.annotation.X + this.annotation.Width;
        this.annotation.path[1].y = this.annotation.Y;
        this.annotation.path[2].x = this.annotation.X + this.annotation.Width;
        this.annotation.path[2].y = this.annotation.Y + this.annotation.Height;
        this.annotation.path[3].x = this.annotation.X;
        this.annotation.path[3].y = this.annotation.Y + this.annotation.Height;

        annotationManager.redrawAnnotation(this.annotation);
      }
    }
  }

  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'
  }, RotatablePath);

  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