Scheduled Maintenance
-
February 10, 2026, from 8:00 AM to 9:00 AM PST. Account-related operations might be temporarily unavailable.

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