Custom annotations

Create custom PDF, Word, Excel, or PowerPoint annotations with this JavaScript sample (no servers or other external dependencies required). Annotations can be customized in several different ways – you can change appearances and behaviors, selection box and control handles. Change one of our pre-built annotations or create your own custom annotations that integrate directly into your workflow. A common example of a custom annotation would be creating a 'complete' annotation stamp that triggers a back-end workflow to mark a task item as complete. This sample works on all browsers (including IE11) and mobile devices without using plug-ins. To see an example visit our custom annotation demo. Learn more about our Web SDK.

1// eslint-disable-next-line no-undef
2const WebViewerConstructor = isWebComponent() ? WebViewer.WebComponent : WebViewer;
3
4(function(exports) {
5 const TRIANGLE_TOOL_NAME = 'AnnotationCreateTriangle';
6 WebViewerConstructor(
7 {
8 path: '../../../lib',
9 initialDoc: 'https://pdftron.s3.amazonaws.com/downloads/pl/demo-annotated.pdf',
10 },
11 document.getElementById('viewer')
12 ).then(instance => {
13 samplesSetup(instance);
14 const { registerTool, setHeaderItems, setToolMode, unregisterTool, iframeWindow } = instance.UI;
15 const { documentViewer, annotationManager, Annotations, Tools, Math } = instance.Core;
16 // stamp.js
17 const customStampTool = window.createStampTool(instance);
18 const TriangleAnnotation = exports.TriangleAnnotationFactory.initialize(Annotations, Math);
19 const TriangleCreateTool = exports.TriangleCreateToolFactory.initialize(Tools, TriangleAnnotation);
20
21 // If we are using the WebComponent we need to change the window context
22 // eslint-disable-next-line no-undef
23 const context = isWebComponent() ? window : iframeWindow;
24
25 // register the annotation type so that it can be saved to XFDF files
26 documentViewer.getAnnotationManager().registerAnnotationType(TriangleAnnotation.prototype.elementName, TriangleAnnotation);
27 // function to check if an annotation is a triangle annotation. allows WebViewer UI to be able to style a selected custom triangle annotation
28 const isTriangleAnnot = annotation =>
29 annotation && annotation[exports.TriangleAnnotationFactory.ANNOT_TYPE] && annotation[exports.TriangleAnnotationFactory.ANNOT_TYPE] === exports.TriangleAnnotationFactory.TRIANGLE_ANNOT_ID;
30 const addTriangleTool = () => {
31 registerTool(
32 {
33 toolName: TRIANGLE_TOOL_NAME,
34 toolObject: new TriangleCreateTool(documentViewer),
35 buttonImage:
36 '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">' +
37 '<path d="M12 7.77L18.39 18H5.61L12 7.77M12 4L2 20h20L12 4z"/>' +
38 '<path fill="none" d="M0 0h24v24H0V0z"/>' +
39 '</svg>',
40 buttonName: 'triangleToolButton',
41 tooltip: 'Triangle',
42 },
43 TriangleAnnotation,
44 annotation => isTriangleAnnot(annotation)
45 );
46
47 const triangleButton = {
48 type: 'toolButton',
49 toolName: TRIANGLE_TOOL_NAME,
50 };
51
52 setHeaderItems(header => {
53 header
54 .getHeader('toolbarGroup-Annotate')
55 .get('highlightToolGroupButton')
56 .insertBefore(triangleButton);
57 });
58 setToolMode(TRIANGLE_TOOL_NAME);
59 };
60
61 const addCustomStampTool = () => {
62 // Register tool
63 registerTool({
64 toolName: 'CustomStampTool',
65 toolObject: customStampTool,
66 buttonImage: '../../../samples/annotation/custom-annotations/stamp.png',
67 buttonName: 'customStampToolButton',
68 tooltip: 'Approved Stamp Tool',
69 });
70
71 // Add tool button in header
72 setHeaderItems(header => {
73 header
74 .getHeader('toolbarGroup-Annotate')
75 .get('highlightToolGroupButton')
76 .insertBefore({
77 type: 'toolButton',
78 toolName: 'CustomStampTool',
79 });
80 });
81 setToolMode('CustomStampTool');
82 };
83
84 const removeCustomStampTool = () => {
85 unregisterTool('CustomStampTool');
86 setToolMode('AnnotationEdit');
87 };
88
89 const removeTriangleTool = () => {
90 unregisterTool(TRIANGLE_TOOL_NAME);
91 setToolMode('AnnotationEdit');
92 };
93
94 document.getElementById('custom-stamp').onchange = e => {
95 if (e.target.checked) {
96 addCustomStampTool();
97 } else {
98 removeCustomStampTool();
99 }
100 };
101
102 document.getElementById('custom-triangle-tool').onchange = e => {
103 if (e.target.checked) {
104 addTriangleTool();
105 } else {
106 removeTriangleTool();
107 }
108 };
109
110 context.document.body.ondragover = e => {
111 e.preventDefault();
112 return false;
113 };
114
115 let dropPoint = {};
116 context.document.body.ondrop = e => {
117 const scrollElement = documentViewer.getScrollViewElement();
118 const scrollLeft = scrollElement.scrollLeft || 0;
119 const scrollTop = scrollElement.scrollTop || 0;
120 dropPoint = { x: e.pageX + scrollLeft, y: e.pageY + scrollTop };
121 e.preventDefault();
122 return false;
123 };
124
125 const addStamp = (imgData, point, rect) => {
126 point = point || {};
127 rect = rect || {};
128 const { documentViewer } = instance.Core;
129 const doc = documentViewer.getDocument();
130 const displayMode = documentViewer.getDisplayModeManager().getDisplayMode();
131 const page = displayMode.getSelectedPages(point, point);
132 if (!!point.x && page.first == null) {
133 return; // don't add to an invalid page location
134 }
135 const pageNumber = page.first !== null ? page.first : documentViewer.getCurrentPage();
136 const pageInfo = doc.getPageInfo(pageNumber);
137 const pagePoint = displayMode.windowToPage(point, pageNumber);
138 const zoom = documentViewer.getZoomLevel();
139
140 const stampAnnot = new Annotations.StampAnnotation();
141 stampAnnot.PageNumber = pageNumber;
142 const rotation = documentViewer.getCompleteRotation(pageNumber) * 90;
143 stampAnnot.Rotation = rotation;
144 if (rotation === 270 || rotation === 90) {
145 stampAnnot.Width = rect.height / zoom;
146 stampAnnot.Height = rect.width / zoom;
147 } else {
148 stampAnnot.Width = rect.width / zoom;
149 stampAnnot.Height = rect.height / zoom;
150 }
151 stampAnnot.X = (pagePoint.x || pageInfo.width / 2) - stampAnnot.Width / 2;
152 stampAnnot.Y = (pagePoint.y || pageInfo.height / 2) - stampAnnot.Height / 2;
153
154 stampAnnot.setImageData(imgData);
155 stampAnnot.Author = annotationManager.getCurrentUser();
156
157 annotationManager.deselectAllAnnotations();
158 annotationManager.addAnnotation(stampAnnot);
159 annotationManager.redrawAnnotation(stampAnnot);
160 annotationManager.selectAnnotation(stampAnnot);
161 };
162
163 // create a stamp image copy for drag and drop
164 const sampleImg = document.getElementById('sample-image');
165 const div = document.createElement('div');
166 const img = document.createElement('img');
167 img.id = 'sample-image-copy';
168 div.appendChild(img);
169 div.style.position = 'absolute';
170 div.style.top = '-500px';
171 div.style.left = '-500px';
172 document.body.appendChild(div);
173 const el = sampleImg;
174 img.src = el.src;
175 const height = el.height;
176 const width = (height / img.height) * img.width;
177 img.style.width = `${width}px`;
178 img.style.height = `${height}px`;
179 const c = document.createElement('canvas');
180 const ctx = c.getContext('2d');
181 c.width = width;
182 c.height = height;
183 ctx.drawImage(img, 0, 0, width, height);
184 img.src = c.toDataURL();
185
186 sampleImg.ondragstart = e => {
187 e.target.style.opacity = 0.5;
188 const copy = e.target.cloneNode(true);
189 copy.id = 'stamp-image-drag-copy';
190 const el = document.getElementById('sample-image-copy');
191 copy.src = el.src;
192 copy.style.width = el.width;
193 copy.style.height = el.height;
194 copy.style.padding = 0;
195 copy.style.position = 'absolute';
196 copy.style.top = '-1000px';
197 copy.style.left = '-1000px';
198 document.body.appendChild(copy);
199 e.dataTransfer.setDragImage(copy, copy.width * 0.5, copy.height * 0.5);
200 e.dataTransfer.setData('text', '');
201 };
202
203 sampleImg.ondragend = e => {
204 const el = document.getElementById('stamp-image-drag-copy');
205 addStamp(e.target.src, dropPoint, el.getBoundingClientRect());
206 e.target.style.opacity = 1;
207 document.body.removeChild(document.getElementById('stamp-image-drag-copy'));
208 e.preventDefault();
209 };
210
211 sampleImg.onclick = e => {
212 addStamp(e.target.src, {}, document.getElementById('sample-image-copy'));
213 };
214
215 document.getElementById('file-open').onchange = e => {
216 const fileReader = new FileReader();
217 fileReader.onload = () => {
218 const result = fileReader.result;
219 const uploadImg = new Image();
220 uploadImg.onload = () => {
221 const imgWidth = 250;
222 addStamp(result, {}, { width: imgWidth, height: (uploadImg.height / uploadImg.width) * imgWidth });
223 };
224 uploadImg.src = result;
225 };
226 if (e.target.files && e.target.files.length > 0) {
227 fileReader.readAsDataURL(e.target.files[0]);
228 }
229 };
230 });
231})(window);

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales