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.

JavaScript (SDK v8.0+)

1Webviewer(...).then((instance) => {
2 const { Core, UI } = instance;
3 const { Annotations, annotationManager, Tools, documentViewer } = Core;
4
5 // Beginning Custom Annotation Class
6 class Rotatable extends Annotations.CustomAnnotation {
7 constructor() {
8 super('rotatable');
9 this.Subject = 'Rotatable';
10 this.selectionModel = Annotations.BoxSelectionModel;
11 }
12
13 draw(ctx, pageMatrix) {
14 this.setStyles(ctx, pageMatrix);
15
16 const x = this.X;
17 const y = this.Y;
18 const width = this.Width;
19 const height = this.Height;
20
21 ctx.translate(x, y);
22 ctx.beginPath();
23 ctx.moveTo(0, 0);
24 ctx.lineTo(width, 0);
25 ctx.lineTo(width, height);
26 ctx.lineTo(0, height);
27 ctx.lineTo(0, 0);
28 ctx.lineTo(width, height);
29 ctx.moveTo(width, 0);
30 ctx.lineTo(0, height);
31 ctx.stroke();
32 }
33 }
34
35 Rotatable.prototype.elementName = 'rotatable';
36
37 annotationManager.registerAnnotationType(Rotatable.prototype.elementName, Rotatable);
38 // End Custom Annotation Class
39
40 // Beginning Tool
41 class RotatableCreateTool extends Tools.GenericAnnotationCreateTool {
42 constructor(documentViewer) {
43 super(documentViewer, Rotatable);
44 }
45 }
46
47 const rotatableToolName = 'AnnotationCreateRotatable';
48
49 const rotatableTool = new RotatableCreateTool(documentViewer);
50 UI.registerTool({
51 toolName: rotatableToolName,
52 toolObject: rotatableTool,
53 buttonImage: '<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 64 64">' +
54 '<line x1="9.37" x2="54.63" y1="9.37" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
55 '<line x1="9.37" x2="9.37" y1="54.63" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
56 '<line x1="54.63" x2="54.63" y1="9.37" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
57 '<line x1="9.37" x2="54.63" y1="54.63" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
58 '<line x1="9.37" x2="54.63" y1="9.37" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
59 '<line x1="9.37" x2="54.63" y1="54.63" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
60 '</svg>',
61 buttonName: 'rotatableToolButton',
62 tooltip: 'Rotatable'
63 }, Rotatable);
64
65 UI.setHeaderItems((header) => {
66 header.getHeader('toolbarGroup-Shapes').get('freeHandToolGroupButton').insertBefore({
67 type: 'toolButton',
68 toolName: rotatableToolName
69 });
70 });
71 // End Tool
72});

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.

JavaScript (SDK v8.0+)

1// Beginning Custom Annotation Class
2 class Rotatable extends Annotations.CustomAnnotation {
3 ...
4 }
5
6 Object.assign(Rotatable.prototype, Annotations.RotationUtils.RectangularCustomAnnotationRotationMixin);
7
8 Rotatable.prototype.elementName = 'rotatable';
9
10 annotationManager.registerAnnotationType(Rotatable.prototype.elementName, Rotatable);
11 // End Custom Annotation Class
12});

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.

JavaScript (SDK v8.0+)

1class Rotatable extends Annotations.CustomAnnotation {
2 ...
3 draw(ctx, pageMatrix) {
4 this.setStyles(ctx, pageMatrix);
5
6 const { x, y, width, height } = this.getUnrotatedDimensions();
7
8 ctx.translate(x + width / 2, y + height / 2);
9 ctx.rotate(-Annotations.RotationUtils.getRotationAngleInRadiansByDegrees(this['Rotation']));
10 ctx.translate(-x - width / 2, -y - height / 2);
11
12 ctx.translate(x, y);
13 ctx.beginPath();
14 ctx.moveTo(0, 0);
15 ctx.lineTo(width, 0);
16 ctx.lineTo(width, height);
17 ctx.lineTo(0, height);
18 ctx.lineTo(0, 0);
19 ctx.lineTo(width, height);
20 ctx.moveTo(width, 0);
21 ctx.lineTo(0, height);
22 ctx.stroke();
23 }
24 ...
25 }

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.

JavaScript (SDK v8.0+)

1// Beginning Custom Annotation Class
2 ...
3 // End Custom Annotation Class
4
5 // Beginning Selection Model
6 class RotatableSelectionModel extends Annotations.BoxSelectionModel {
7 constructor(annotation, canModify, isSelected, documentViewer) {
8 super(annotation, canModify, isSelected, documentViewer);
9
10 const controlHandles = this.getControlHandles();
11
12 if (canModify && documentViewer.getAnnotationManager().isFreeformRotationEnabled() && annotation.hasRotationControlEnabled()) {
13 controlHandles.push(new Annotations.RotationControlHandle(Annotations.ControlHandle['rotationHandleWidth'], Annotations.ControlHandle['rotationHandleHeight'], 40, annotation, documentViewer));
14 }
15 }
16 }
17 // End Selection Model
18
19 // Beginning Tool
20 ...
21 // End Tool

The complete code

JavaScript (SDK v8.0+)

1Webviewer(...).then((instance) => {
2 const { Core, UI } = instance;
3 const { Annotations, annotationManager, Tools, documentViewer } = Core;
4
5 // Beginning Custom Annotation Class
6 class Rotatable extends Annotations.CustomAnnotation {
7 constructor() {
8 super('rotatable');
9 this.Subject = 'Rotatable';
10 this.selectionModel = RotatableSelectionModel;
11 }
12
13 draw(ctx, pageMatrix) {
14 this.setStyles(ctx, pageMatrix);
15
16 const { x, y, width, height } = this.getUnrotatedDimensions();
17
18 ctx.translate(x + width / 2, y + height / 2);
19 ctx.rotate(-Annotations.RotationUtils.getRotationAngleInRadiansByDegrees(this['Rotation']));
20 ctx.translate(-x - width / 2, -y - height / 2);
21
22 ctx.translate(x, y);
23 ctx.beginPath();
24 ctx.moveTo(0, 0);
25 ctx.lineTo(width, 0);
26 ctx.lineTo(width, height);
27 ctx.lineTo(0, height);
28 ctx.lineTo(0, 0);
29 ctx.lineTo(width, height);
30 ctx.moveTo(width, 0);
31 ctx.lineTo(0, height);
32 ctx.stroke();
33 }
34 }
35
36 Object.assign(Rotatable.prototype, Annotations.RotationUtils.RectangularCustomAnnotationRotationMixin);
37
38 Rotatable.prototype.elementName = 'rotatable';
39
40 annotationManager.registerAnnotationType(Rotatable.prototype.elementName, Rotatable);
41 // End Custom Annotation Class
42
43 // Beginning Selection Model
44 class RotatableSelectionModel extends Annotations.BoxSelectionModel {
45 constructor(annotation, canModify, isSelected, documentViewer) {
46 super(annotation, canModify, isSelected, documentViewer);
47
48 const controlHandles = this.getControlHandles();
49
50 if (canModify && documentViewer.getAnnotationManager().isFreeformRotationEnabled() && annotation.hasRotationControlEnabled()) {
51 controlHandles.push(new Annotations.RotationControlHandle(Annotations.ControlHandle['rotationHandleWidth'], Annotations.ControlHandle['rotationHandleHeight'], 40, annotation, documentViewer));
52 }
53 }
54 }
55 // End Selection Model
56
57 // Beginning Tool
58 class RotatableCreateTool extends Tools.GenericAnnotationCreateTool {
59 constructor(documentViewer) {
60 super(documentViewer, Rotatable);
61 }
62 }
63
64 const rotatableToolName = 'AnnotationCreateRotatable';
65
66 const rotatableTool = new RotatableCreateTool(documentViewer);
67 UI.registerTool({
68 toolName: rotatableToolName,
69 toolObject: rotatableTool,
70 buttonImage: '<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 64 64">' +
71 '<line x1="9.37" x2="54.63" y1="9.37" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
72 '<line x1="9.37" x2="9.37" y1="54.63" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
73 '<line x1="54.63" x2="54.63" y1="9.37" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
74 '<line x1="9.37" x2="54.63" y1="54.63" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
75 '<line x1="9.37" x2="54.63" y1="9.37" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
76 '<line x1="9.37" x2="54.63" y1="54.63" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
77 '</svg>',
78 buttonName: 'rotatableToolButton',
79 tooltip: 'Rotatable'
80 }, Rotatable);
81
82 UI.setHeaderItems((header) => {
83 header.getHeader('toolbarGroup-Shapes').get('freeHandToolGroupButton').insertBefore({
84 type: 'toolButton',
85 toolName: rotatableToolName
86 });
87 });
88 // End Tool
89});

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales