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.

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 RotatablePath extends Annotations.CustomAnnotation {
7 constructor() {
8 super('rotatable-path');
9 this.Subject = 'RotatablePath';
10 this.path = [
11 new Annotations.Point(this.X, this.Y),
12 new Annotations.Point(this.X + this.Width, this.Y),
13 new Annotations.Point(this.X + this.Width, this.Y + this.Height),
14 new Annotations.Point(this.X, this.Y + this.Height),
15 ];
16 this.selectionModel = RotatableSelectionModel;
17 }
18
19 getPath() {
20 return this.path;
21 }
22
23 setPath(newPath) {
24 this.path = newPath;
25 }
26
27 draw(ctx, pageMatrix) {
28 this.setStyles(ctx, pageMatrix);
29
30 ctx.beginPath();
31 ctx.moveTo(this.path[0].x, this.path[0].y);
32 ctx.lineTo(this.path[1].x, this.path[1].y);
33 ctx.lineTo(this.path[2].x, this.path[2].y);
34 ctx.lineTo(this.path[3].x, this.path[3].y);
35 ctx.lineTo(this.path[0].x, this.path[0].y);
36 ctx.lineTo(this.path[2].x, this.path[2].y);
37 ctx.moveTo(this.path[1].x, this.path[1].y);
38 ctx.lineTo(this.path[3].x, this.path[3].y);
39 ctx.stroke();
40 }
41
42 resize(rect) {
43 const { x1, y1 } = rect;
44 const deltaX = x1 - this['X'];
45 const deltaY = y1 - this['Y'];
46 if (deltaX === 0 && deltaY === 0) {
47 return;
48 }
49 for (let i = 0; i < this.path.length; i++) {
50 this.path[i].translate(deltaX, deltaY);
51 }
52
53 this.adjustRect();
54 }
55
56 adjustRect() {
57 let minX = this.path[0]['x'];
58 let minY = this.path[0]['y'];
59 let maxX = this.path[0]['x'];
60 let maxY = this.path[0]['y'];
61
62 this.path.forEach((point) => {
63 if (point.x < minX) {
64 minX = point.x;
65 }
66 if (point.x > maxX) {
67 maxX = point.x;
68 }
69 if (point.y < minY) {
70 minY = point.y;
71 }
72 if (point.y > maxY) {
73 maxY = point.y;
74 }
75 });
76
77 this['X'] = minX;
78 this['Y'] = minY;
79 this['Width'] = maxX - minX;
80 this['Height'] = maxY - minY;
81 }
82
83 serialize(element, pageMatrix) {
84 this.setCustomData('trn-path', JSON.stringify(this.path));
85
86 return Annotations.CustomAnnotation.prototype.serialize.apply(this, arguments);
87 }
88
89 deserialize(element) {
90 Annotations.CustomAnnotation.prototype.deserialize.apply(this, arguments);
91
92 this.path = JSON.parse(this.getCustomData('trn-path'));
93 }
94 }
95
96 RotatablePath.prototype.elementName = 'rotatable-path';
97
98 annotationManager.registerAnnotationType(RotatablePath.prototype.elementName, RotatablePath);
99 // End Custom Annotation Class
100
101 // Beginning Control Handle
102 class RotatablePathControlHandle extends Annotations.PathControlHandle {
103 constructor(x, y, width, height, pathIndex) {
104 super(x, y, width, height, pathIndex);
105
106 this.pathIndex = pathIndex;
107 }
108
109 move(annotation, deltaX, deltaY) {
110 annotation.path[this.pathIndex] = new Annotations.Point(annotation.path[this.pathIndex].x + deltaX, annotation.path[this.pathIndex].y + deltaY);
111
112 annotation.adjustRect();
113
114 return true;
115 }
116 }
117 // End Control Handle
118
119 // Beginning Selection Model
120 class RotatableSelectionModel extends Annotations.SelectionModel {
121 constructor(annotation, canModify, isSelected, documentViewer) {
122 super(annotation, canModify, isSelected, documentViewer);
123
124 const controlHandles = this.getControlHandles();
125
126 if (canModify) {
127 for (let i = 0; i < annotation.path.length; i++) {
128 controlHandles.push(new RotatablePathControlHandle(annotation.path[i].x, annotation.path[i].y, Annotations.ControlHandle['handleWidth'], Annotations.ControlHandle['handleWidth'], i));
129 }
130 }
131 }
132 }
133 // End Selection Model
134
135 // Beginning Tool
136 class RotatableCreateTool extends Tools.GenericAnnotationCreateTool {
137 constructor(documentViewer) {
138 super(documentViewer, RotatablePath);
139 }
140
141 mouseMove(e) {
142 super.mouseMove(e);
143
144 if (this.annotation) {
145 this.annotation.path[0].x = this.annotation.X;
146 this.annotation.path[0].y = this.annotation.Y;
147 this.annotation.path[1].x = this.annotation.X + this.annotation.Width;
148 this.annotation.path[1].y = this.annotation.Y;
149 this.annotation.path[2].x = this.annotation.X + this.annotation.Width;
150 this.annotation.path[2].y = this.annotation.Y + this.annotation.Height;
151 this.annotation.path[3].x = this.annotation.X;
152 this.annotation.path[3].y = this.annotation.Y + this.annotation.Height;
153
154 annotationManager.redrawAnnotation(this.annotation);
155 }
156 }
157 }
158
159 const rotatableToolName = 'AnnotationCreateRotatable';
160
161 const rotatableTool = new RotatableCreateTool(documentViewer);
162 UI.registerTool({
163 toolName: rotatableToolName,
164 toolObject: rotatableTool,
165 buttonImage: '<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 64 64">' +
166 '<line x1="9.37" x2="54.63" y1="9.37" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
167 '<line x1="9.37" x2="9.37" y1="54.63" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
168 '<line x1="54.63" x2="54.63" y1="9.37" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
169 '<line x1="9.37" x2="54.63" y1="54.63" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
170 '<line x1="9.37" x2="54.63" y1="9.37" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
171 '<line x1="9.37" x2="54.63" y1="54.63" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
172 '</svg>',
173 buttonName: 'rotatableToolButton',
174 tooltip: 'Rotatable'
175 }, RotatablePath);
176
177 UI.setHeaderItems((header) => {
178 header.getHeader('toolbarGroup-Shapes').get('freeHandToolGroupButton').insertBefore({
179 type: 'toolButton',
180 toolName: rotatableToolName
181 });
182 });
183 // End Tool
184});

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)

JavaScript (SDK v8.0+)

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

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.SelectionModel {
7 constructor(annotation, canModify, isSelected, documentViewer) {
8 super(annotation, canModify, isSelected, documentViewer);
9
10 const controlHandles = this.getControlHandles();
11
12 if (canModify) {
13 for (let i = 0; i < annotation.path.length; i++) {
14 controlHandles.push(new RotatablePathControlHandle(annotation.path[i].x, annotation.path[i].y, Annotations.ControlHandle['handleWidth'], Annotations.ControlHandle['handleWidth'], i));
15 }
16
17 if (documentViewer.getAnnotationManager().isFreeformRotationEnabled() && annotation.hasRotationControlEnabled()) {
18 controlHandles.push(new Annotations.RotationControlHandle(Annotations.ControlHandle['rotationHandleWidth'], Annotations.ControlHandle['rotationHandleHeight'], 40, annotation, documentViewer));
19 }
20 }
21 }
22 }
23 // End Selection Model
24
25 // Beginning Tool
26 ...
27 // 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 RotatablePath extends Annotations.CustomAnnotation {
7 constructor() {
8 super('rotatable-path');
9 this.Subject = 'RotatablePath';
10 this.path = [
11 new Annotations.Point(this.X, this.Y),
12 new Annotations.Point(this.X + this.Width, this.Y),
13 new Annotations.Point(this.X + this.Width, this.Y + this.Height),
14 new Annotations.Point(this.X, this.Y + this.Height),
15 ];
16 this.selectionModel = RotatableSelectionModel;
17 }
18
19 getPath() {
20 return this.path;
21 }
22
23 setPath(newPath) {
24 this.path = newPath;
25 }
26
27 draw(ctx, pageMatrix) {
28 this.setStyles(ctx, pageMatrix);
29
30 ctx.beginPath();
31 ctx.moveTo(this.path[0].x, this.path[0].y);
32 ctx.lineTo(this.path[1].x, this.path[1].y);
33 ctx.lineTo(this.path[2].x, this.path[2].y);
34 ctx.lineTo(this.path[3].x, this.path[3].y);
35 ctx.lineTo(this.path[0].x, this.path[0].y);
36 ctx.lineTo(this.path[2].x, this.path[2].y);
37 ctx.moveTo(this.path[1].x, this.path[1].y);
38 ctx.lineTo(this.path[3].x, this.path[3].y);
39 ctx.stroke();
40 }
41
42 resize(rect) {
43 const { x1, y1 } = rect;
44 const deltaX = x1 - this['X'];
45 const deltaY = y1 - this['Y'];
46 if (deltaX === 0 && deltaY === 0) {
47 return;
48 }
49 for (let i = 0; i < this.path.length; i++) {
50 this.path[i].translate(deltaX, deltaY);
51 }
52
53 this.adjustRect();
54 }
55 }
56
57 Object.assign(RotatablePath.prototype, Annotations.RotationUtils.PathCustomAnnotationRotationMixin);
58
59 RotatablePath.prototype.elementName = 'rotatable-path';
60
61 annotationManager.registerAnnotationType(RotatablePath.prototype.elementName, RotatablePath);
62 // End Custom Annotation Class
63
64 // Beginning Control Handle
65 class RotatablePathControlHandle extends Annotations.PathControlHandle {
66 constructor(x, y, width, height, pathIndex) {
67 super(x, y, width, height, pathIndex);
68
69 this.pathIndex = pathIndex;
70 }
71
72 move(annotation, deltaX, deltaY) {
73 annotation.getPath()[this.pathIndex] = new Annotations.Point(annotation.getPath()[this.pathIndex].x + deltaX, annotation.getPath()[this.pathIndex].y + deltaY);
74
75 annotation.adjustRect();
76
77 return true;
78 }
79 }
80 // End Control Handle
81
82 // Beginning Selection Model
83 class RotatableSelectionModel extends Annotations.SelectionModel {
84 constructor(annotation, canModify, isSelected, documentViewer) {
85 super(annotation, canModify, isSelected, documentViewer);
86
87 const controlHandles = this.getControlHandles();
88
89 if (canModify) {
90 for (let i = 0; i < annotation.path.length; i++) {
91 controlHandles.push(new RotatablePathControlHandle(annotation.path[i].x, annotation.path[i].y, Annotations.ControlHandle['handleWidth'], Annotations.ControlHandle['handleWidth'], i));
92 }
93
94 if (documentViewer.getAnnotationManager().isFreeformRotationEnabled() && annotation.hasRotationControlEnabled()) {
95 controlHandles.push(new Annotations.RotationControlHandle(Annotations.ControlHandle['rotationHandleWidth'], Annotations.ControlHandle['rotationHandleHeight'], 40, annotation, documentViewer));
96 }
97 }
98 }
99 }
100 // End Selection Model
101
102 // Beginning Tool
103 class RotatableCreateTool extends Tools.GenericAnnotationCreateTool {
104 constructor(documentViewer) {
105 super(documentViewer, RotatablePath);
106 }
107
108 mouseMove(e) {
109 super.mouseMove(e);
110
111 if (this.annotation) {
112 this.annotation.path[0].x = this.annotation.X;
113 this.annotation.path[0].y = this.annotation.Y;
114 this.annotation.path[1].x = this.annotation.X + this.annotation.Width;
115 this.annotation.path[1].y = this.annotation.Y;
116 this.annotation.path[2].x = this.annotation.X + this.annotation.Width;
117 this.annotation.path[2].y = this.annotation.Y + this.annotation.Height;
118 this.annotation.path[3].x = this.annotation.X;
119 this.annotation.path[3].y = this.annotation.Y + this.annotation.Height;
120
121 annotationManager.redrawAnnotation(this.annotation);
122 }
123 }
124 }
125
126 const rotatableToolName = 'AnnotationCreateRotatable';
127
128 const rotatableTool = new RotatableCreateTool(documentViewer);
129 UI.registerTool({
130 toolName: rotatableToolName,
131 toolObject: rotatableTool,
132 buttonImage: '<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 64 64">' +
133 '<line x1="9.37" x2="54.63" y1="9.37" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
134 '<line x1="9.37" x2="9.37" y1="54.63" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
135 '<line x1="54.63" x2="54.63" y1="9.37" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
136 '<line x1="9.37" x2="54.63" y1="54.63" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
137 '<line x1="9.37" x2="54.63" y1="9.37" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
138 '<line x1="9.37" x2="54.63" y1="54.63" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
139 '</svg>',
140 buttonName: 'rotatableToolButton',
141 tooltip: 'Rotatable'
142 }, RotatablePath);
143
144 UI.setHeaderItems((header) => {
145 header.getHeader('toolbarGroup-Shapes').get('freeHandToolGroupButton').insertBefore({
146 type: 'toolButton',
147 toolName: rotatableToolName
148 });
149 });
150 // End Tool
151});

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales