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 DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales