Easily add stamps to your documents. The viewer includes several sample stamps, and you can also create custom ones using text or images.
This demo allows you to:
Implementation steps
To add stamping capability with WebViewer:
Step 1: Choose your preferred web stack
Step 2: Add the ES6 JavaScript sample code provided in this guide
1// ES6 Compliant Syntax
2// Copilot name: GitHub Copilot, version: 1.0.0, model: GPT-4, version: 2024-06, date: 2025-10-23
3// File: showcase-demos/edit-table-of-contents/index.js
4
5import WebViewer from '@pdftron/webviewer';
6
7// State variables for demo
8const dropPoint = {};
9const customStampDemoFile = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/sales-invoice.pdf';
10
11function initializeWebViewer() {
12 WebViewer(
13 {
14 path: '/lib',
15 initialDoc: customStampDemoFile,
16 enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File
17 licenseKey: 'YOUR_LICENSE_KEY', // Replace with your license key
18 },
19 document.getElementById('viewer')
20 ).then((instance) => {
21
22 createUIElements();
23
24 // Set toolbar group immediately after WebViewer loads
25 instance.UI.setToolbarGroup(['toolbarGroup-Insert']);
26
27 // Wait for document to load before setting tool mode (more reliable)
28 instance.Core.documentViewer.addEventListener('documentLoaded', () => {
29
30 // Set the rubber stamp tool mode
31 instance.UI.setToolMode('AnnotationCreateRubberStamp');
32
33 // Add a small delay before opening the panel
34 setTimeout(() => {
35 instance.UI.openElements(['rubberStampPanel']);
36 }, 150);
37
38 // Register events after the page is loaded
39 demoComplete();
40 registerDragEvents();
41 });
42 });
43};
44
45const getIframeWindow = () => {
46 return (
47 document.getElementById('viewer')?.getElementsByTagName('iframe')?.[0]?.contentWindow || window
48 );
49};
50
51const getIframeContext = () => {
52 return (
53 document.getElementById('viewer')?.getElementsByTagName('iframe')?.[0]?.contentWindow
54 .instance || window.WebViewer.getInstance()
55 );
56};
57
58// Drag and drop handlers
59const dragOver = (e) => {
60 e.preventDefault();
61 //const imgData = e.dataTransfer.getData('text/plain');
62 e.target.style.opacity = 1;
63 e.preventDefault();
64};
65
66// Update drop event to adjust dropPoint to fit document coordinates
67const drop = (e) => {
68 console.log('File dropped:', e);
69 e.preventDefault();
70 e.stopPropagation();
71 const documentViewer = getIframeContext().Core.documentViewer;
72 const scrollElement = documentViewer.getScrollViewElement();
73 const scrollLeft = scrollElement.scrollLeft || 0;
74 const scrollTop = scrollElement.scrollTop || 0;
75 const zoomLevel = documentViewer.getZoomLevel();
76 dropPoint.current = { x: (e.pageX + scrollLeft), y: (e.pageY + scrollTop) };
77 const displayMode = documentViewer.getDisplayModeManager().getDisplayMode();
78 const doc = documentViewer.getDocument();
79
80 const documentWidth = doc.getPageInfo(1).width;
81 const documentHeight = doc.getPageInfo(1).height;
82 dropPoint.current.x = Math.min(Math.max(dropPoint.current.x, 0), documentWidth);
83 dropPoint.current.y = Math.min(Math.max(dropPoint.current.y, 0), documentHeight);
84 const page = displayMode.getSelectedPages(dropPoint.current, dropPoint.current);
85
86 if (!page.first) {
87 console.error('128 Invalid page location for point:', dropPoint.current);
88
89 // Log dimensions of all pages for debugging
90 const pageCount = doc.getPageCount();
91 for (let i = 1; i <= pageCount; i++) {
92 const pageInfo = doc.getPageInfo(i);
93 console.log(`Page ${i} bounds:`, pageInfo);
94 }
95
96 return;
97 }
98
99 const pageInfo = doc.getPageInfo(page.first);
100 console.log('Selected page bounds:', pageInfo);
101
102 let pagePoint = displayMode.windowToPage(dropPoint.current, page.first);
103 console.log('Mapped pagePoint before clamping:', pagePoint);
104
105 if (!pagePoint || pagePoint.x < 0 || pagePoint.y < 0 || pagePoint.x > pageInfo.width || pagePoint.y > pageInfo.height) {
106 console.error('Invalid page coordinates after mapping:', pagePoint);
107 pagePoint = {
108 x: Math.min(Math.max(pagePoint.x || 0, 0), pageInfo.width),
109 y: Math.min(Math.max(pagePoint.y || 0, 0), pageInfo.height),
110 };
111 }
112
113 const imgData = e.dataTransfer.getData('text/plain');
114 if (!imgData) {
115 console.error('No imgData found in dataTransfer');
116 return;
117 }
118
119 console.log('Dimensions of image dataTransfer:', e.dataTransfer);
120 let stampSize = { width: 100, height: 25 }; //Default size
121 let img = new Image();
122 img.src = imgData; // Set the base64 string as the source
123
124 img.onload = () => {
125 document.body.appendChild(img); // Append the image to the DOM temporarily
126 const renderedWidth = img.offsetWidth; // Rendered width of the image
127 const renderedHeight = img.offsetHeight; // Rendered height of the image
128 document.body.removeChild(img); // Remove the image from the DOM
129
130 // You can now use these dimensions to set the stamp size
131 stampSize.width = renderedWidth;
132 stampSize.height = renderedHeight;
133
134 //find slider and get the value for width of image
135 const widthSlider = document.getElementById('width-slider');
136 if (widthSlider) {
137 const sliderValue = parseFloat(widthSlider.value);
138 if (!isNaN(sliderValue) && sliderValue > 0) {
139 stampSize.width = sliderValue;
140 const aspectRatio = renderedWidth / renderedHeight;
141 stampSize.height = sliderValue / aspectRatio;
142 }
143 }
144
145 const viewFactor = 0.66; // Adjust this factor as needed, used to scale the stamp appropriately
146 let stampRect = { width: stampSize.width * zoomLevel * viewFactor, height: stampSize.height * zoomLevel * viewFactor };
147
148 try {
149 addStamp(imgData, pagePoint, stampRect, page.first);
150 console.log('Stamp added at point:', pagePoint, 'with rect:', stampRect);
151 } catch (error) {
152 console.error('Error while adding stamp:', error);
153 console.error('Parameters passed to addStamp:', {
154 imgData,
155 point: pagePoint,
156 rect: stampRect,
157 pageNumber: page.first,
158 });
159 }
160 };
161
162 img.onerror = () => {
163 console.error('Failed to load image from data URL');
164 };
165};
166
167function demoComplete() {
168 //let defaultDocument = customStampDemoFile;
169 //console.log('Demo initialization complete');
170 const documentViewer = getIframeContext().Core.documentViewer;
171 documentViewer.addEventListener('toolModeUpdated', toolModeUpdated);
172 const iframeDoc = getIframeWindow().document.body;
173 iframeDoc.addEventListener('dragover', dragOver);
174 iframeDoc.addEventListener('drop', drop);
175
176 // Allow annotations outside page bounds
177 getIframeContext().Core.Tools.Tool.ALLOW_ANNOTS_OUTSIDE_PAGE = true;
178};
179
180const toolModeUpdated = (type) => {
181 const { id } = type;
182 if (id) {
183 // Check if stamps object exists and has the id
184 if (window.stamps && window.stamps[id]) {
185 data = window.stamps[id];
186 name = id;
187 } else {
188 data = null;
189 }
190 }
191};
192
193const stampPanelWidthOffset = 146;
194
195const addStamp = (
196 imgData,
197 point = {},
198 rect = { height: undefined, width: undefined }
199) => {
200 const { Annotations } = getIframeContext().Core;
201 const { documentViewer, annotationManager } = getIframeContext().Core;
202 const doc = documentViewer.getDocument();
203 const displayMode = documentViewer.getDisplayModeManager().getDisplayMode();
204
205 console.log('Adding stamp at point:', point, 'with rect:', rect);
206
207 //if stamp panel is open add points to x coordinate
208 if (getIframeContext().UI.isElementOpen('rubberStampPanel')) {
209 point.x += stampPanelWidthOffset + (rect.width < 50 ? rect.width + 50 : rect.width); // add width of stamp panel + width
210 console.log('326 Adjusted point for open stamp panel:', point);
211 }
212
213 const page = displayMode.getSelectedPages(point, point);
214 const page_num = page.first || documentViewer.getCurrentPage();
215 if (!page_num || typeof page_num !== 'number') {
216 console.error('Invalid page number:', page_num);
217 return;
218 }
219
220 const page_info = doc.getPageInfo(page_num);
221 const page_point = displayMode.windowToPage(point, page_num);
222 const zoom = documentViewer.getZoomLevel();
223 const stampAnnot = new Annotations.StampAnnotation();
224 stampAnnot.PageNumber = page_num;
225 const rotation = documentViewer.getCompleteRotation(page_num) * 90;
226 stampAnnot.Rotation = rotation;
227 if (rotation === 270 || rotation === 90) {
228 stampAnnot.Width = rect.height / zoom;
229 stampAnnot.Height = rect.width / zoom;
230 } else {
231 stampAnnot.Width = rect.width / zoom;
232 stampAnnot.Height = rect.height / zoom;
233 }
234
235 stampAnnot.X = (page_point.x || page_info.width / 2) - stampAnnot.Width / 2;
236 stampAnnot.X += (rect.width < 50 ? rect.width + 50 : rect.width); // Shift right by rect width to account for drag offset, stamp is shifted to the right
237 stampAnnot.Y = (page_point.y || page_info.height / 2) - stampAnnot.Height / 2;
238 stampAnnot.setImageData(imgData);
239 stampAnnot.Author = annotationManager.getCurrentUser();
240 annotationManager.deselectAllAnnotations();
241 annotationManager.addAnnotation(stampAnnot, true);
242 annotationManager.redrawAnnotation(stampAnnot);
243 annotationManager.selectAnnotation(stampAnnot);
244};
245
246// Register drag and drop events
247function registerDragEvents() {
248 // Required:
249 // target is 'viewer'
250 const dropTarget = document.getElementById('viewer');
251
252 // Just adds the '+' to show while dragging to the drop target
253 dropTarget.addEventListener('dragover', (e) => {
254 e.preventDefault();
255 e.dataTransfer.dropEffect = 'copy'; // with copy, shows '+' symbol during drag
256 });
257
258 dropTarget.addEventListener('drop', (e) => {
259 const imgData = e.dataTransfer.getData('text/plain'); // Retrieve using custom data type
260 if (!imgData) {
261 console.error('No imgData found in dataTransfer');
262 return;
263 }
264
265 const documentViewer = getIframeContext().Core.documentViewer;
266 const scrollElement = documentViewer.getScrollViewElement();
267 const scrollLeft = scrollElement.scrollLeft || 0;
268 const scrollTop = scrollElement.scrollTop || 0;
269 dropPoint.current = { x: e.pageX + scrollLeft, y: e.pageY + scrollTop };
270 const displayMode = documentViewer.getDisplayModeManager().getDisplayMode();
271 const page = displayMode.getSelectedPages(dropPoint.current, dropPoint.current);
272 if (!page.first) {
273 return;
274 }
275 });
276
277 // Custom annotation behavior checkbox handler
278 const setCustomAnnotationBehavior = () => {
279 const isChecked = document.getElementById('custom-annotation-behavior').checked;
280 getIframeContext().Core.Tools.Tool.ALLOW_ANNOTS_OUTSIDE_PAGE = isChecked;
281 };
282
283 // Register event listener for custom annotation behavior checkbox
284 const customAnnotationCheckbox = document.getElementById('custom-annotation-behavior');
285 if (customAnnotationCheckbox) {
286 customAnnotationCheckbox.addEventListener('click', setCustomAnnotationBehavior);
287 }
288
289 // Open custom stamp modal on button click
290 const setTextStamp = () => {
291 getIframeContext().UI.openElements(['customStampModal']);
292 };
293
294 // Register event listener for custom annotation behavior checkbox
295 const onClickCreateTextStamp = document.getElementById('choose-file-button-text');
296 if (onClickCreateTextStamp) {
297 onClickCreateTextStamp.addEventListener('click', setTextStamp);
298 }
299}
300
301function createUIElements() {
302 // Create a container for all controls (label, dropdown, and buttons)
303 // Dynamically load ui-elements.js if not already loaded
304 if (!window.SidePanel) {
305 const script = document.createElement('script');
306 script.src = '/showcase-demos/pdf-stamps/ui-elements.js';
307 script.onload = () => {
308 UIElements.init('viewer');
309
310 };
311 document.head.appendChild(script);
312 }
313}
314
315function loadObservableUtils() {
316 if (!window.registerObservable) {
317 const script = document.createElement('script');
318 script.src = '/showcase-demos/pdf-stamps/observable-utils.js';
319 script.onload = () => {
320 registerObservable();
321 };
322 document.head.appendChild(script);
323 } else {
324 registerObservable();
325 }
326}
327
328initializeWebViewer();
329loadObservableUtils();
330
1/* Main layout - side by side containers within #viewer */
2#viewer {
3 display: flex;
4 height: 100%;
5 width: 100%;
6}
7
8/* Side Panel Styles */
9.viewer-wrapper {
10 display: flex;
11 height: 100vh;
12 width: 100%;
13}
14
15.side-panel {
16 width: 300px;
17 min-width: 250px;
18 max-width: 400px;
19 background-color: #f5f5f5;
20 border-right: 1px solid #ddd;
21 box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
22 transition: transform 0.3s ease;
23 z-index: 1000;
24 display: flex;
25 flex-direction: column;
26}
27
28.stamp-image {
29 margin: 10px auto;
30 max-width: 40%;
31 height: 25px;
32}
33
34.stamp-image:hover {
35 cursor: pointer;
36 transform: scale(1.05);
37 transition: transform 0.2s;
38}
39
40#stamp-images {
41 text-align: center;
42 margin-bottom: 20px;
43}
44
45.button-container {
46 text-align: center;
47 margin-bottom: 20px;
48 font-weight: 200;
49}
50
51.butoon-header {
52 font-size: 16px;
53 margin-bottom: 8px;
54 color: #333;
55 font-weight: 500;
56}
57
58.side-panel-content {
59 flex: 1;
60 padding: 20px;
61 overflow-y: auto;
62}
63
64.panel-section {
65 margin-bottom: 25px;
66}
67
68.panel-section h4 {
69 margin: 0 0 12px 0;
70 font-size: 14px;
71 font-weight: 600;
72 color: #555;
73 text-transform: uppercase;
74 letter-spacing: 0.5px;
75}
76
77
78
79/* Choose File Button Styles */
80#choose-file {
81 display: block;
82 width: 100%;
83 padding: 12px 20px;
84 margin: 10px 0;
85 background-color: #007bff;
86 color: white;
87 border: none;
88 border-radius: 8px;
89 cursor: pointer;
90 font-size: 16px;
91 font-weight: 500;
92 text-align: center;
93 transition: all 0.3s ease;
94 box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2);
95}
96
97#choose-file:hover {
98 background-color: #0056b3;
99 transform: translateY(-1px);
100 box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
101}
102
103#choose-file:active {
104 background-color: #004085;
105 transform: translateY(0);
106 box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2);
107}
108
109#choose-file:focus {
110 outline: none;
111 box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
112}
113
114
115
116
117/* Choose File Button Styles */
118.choose-file-button {
119 display: block;
120 width: 100%;
121 padding: 12px 20px;
122 margin: 10px 0;
123 background-color: #eff2f5;
124 color: #007bff;
125 border: #007bff 2px solid;
126 border-radius: 8px;
127 cursor: pointer;
128 font-size: 16px;
129 font-weight: 500;
130 text-align: center;
131 transition: all 0.3s ease;
132 box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2);
133}
134
135.choose-file-button:hover {
136 background-color: rgb(0,226,234);
137 transform: translateY(-1px);
138 box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
139}
140
141.choose-file-button:active {
142 background-color: #004085;
143 transform: translateY(0);
144 box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2);
145}
146
147.choose-file-button:focus {
148 outline: none;
149 box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
150}
151
152
153
154
155.viewer-with-panel {
156 flex: 1;
157 height: 100vh;
158}
159
160
161
162
163
164/* Text color changes */
165#dark-mode:checked ~ .switch-option.left {
166 color: white;
167}
168
169#light-mode:checked ~ .switch-option.right {
170 color: white;
171}
172
173.switch-option {
174 color: #6c757d;
175}
176
177/* Dark mode theme styles */
178@media (prefers-color-scheme: dark) {
179 .theme-switch {
180 background-color: #4a5568;
181 border-color: #718096;
182 }
183
184 .switch-option {
185 color: #a0aec0;
186 }
187
188 #dark-mode:checked ~ .switch-option.left {
189 color: white;
190 }
191
192 #light-mode:checked ~ .switch-option.right {
193 color: white;
194 }
195}
196
197/* Styles for the stamp slider */
198#stamp-slider {
199 position: absolute;
200 top: 80px;
201 left: 0px;
202 width: 300px;
203 min-height: 550px;
204 max-height: 550px;
205 overflow-y: auto;
206 background-color: #ffffff;
207 border: 1px solid #ddd;
208 box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
209 border-radius: 8px;
210 padding: 16px;
211 z-index: 2000;
212}
213
214#stamp-slider .image-container {
215 text-align: center;
216 margin-bottom: 16px;
217}
218
219#stamp-slider #preview-stamp-image {
220 max-width: 100%;
221 height: auto;
222 border: 1px solid #ccc;
223 border-radius: 4px;
224}
225
226#stamp-slider .slider-container {
227 display: flex;
228 align-items: center;
229 margin-bottom: 16px;
230}
231
232#stamp-slider .slider-container label {
233 margin-right: 8px;
234 font-weight: bold;
235}
236
237#stamp-slider .slider-container input[type="range"] {
238 flex: 1;
239}
240
241#stamp-slider .slider-container span {
242 margin-left: 8px;
243 font-size: 14px;
244}
245
246#stamp-slider .btn-style, #add-more-stamps-button {
247 display: block;
248 width: 100%;
249 padding: 10px;
250 background-color: #007bff;
251 color: #ffffff;
252 border: none;
253 border-radius: 4px;
254 cursor: pointer;
255 text-align: center;
256 font-size: 14px;
257}
258
259#stamp-slider .btn-style:hover {
260 background-color: #0056b3;
261}
262
263#controls-container {
264 bottom: 10px;
265 left: 10px;
266 right: 10px;
267 position: absolute;
268 flex-direction: column;
269 gap: 12px;
270}
271
272.button-header {
273 font-size: 14px;
274 margin-bottom: 8px;
275 color: #333;
276 font-weight: bolder;
277}
278
1// ES6 Compliant Syntax
2// GitHub Copilot, version: 1.0.0, model: GPT-4, version: 2024-06, date: 2025-10-23
3// File: showcase-demos/pdf-stamps/ui-elements.js
4
5
6class UIElements {
7
8 static init(viewerId) {
9 this.createSidePanel(viewerId);
10 }
11
12 // Function to create a side panel that sits on the left side of the viewer
13 static createSidePanel(viewerId) {
14 const viewerElement = document.getElementById(viewerId);
15 if (!viewerElement) {
16 console.error(`Viewer element with id '${viewerId}' not found.`);
17 return;
18 }
19
20 // Create the side panel container
21 const sidePanel = document.createElement('div');
22 sidePanel.id = 'side-panel';
23 sidePanel.className = 'side-panel';
24
25 // Create side panel content
26 const content = document.createElement('div');
27 content.className = 'side-panel-content';
28
29 // Add the text extraction content
30 const sampleContent = document.createElement('div');
31 sampleContent.innerHTML = `
32 <div class="panel-section">
33 <h4>JS PDF Stamps Creation Demo</h4>
34 <p>A demo of the document stamping capability in WebViewer, a JavaScript-based PDF SDK for web apps. By default WebViewer includes several standard stamps that are part of the PDF specification, but it also supports the creation of custom stamps with custom text and colors.</p>
35
36 <div class="button-container">
37 <button id="choose-file" class="btn-style">Choose File</button>
38 </div>
39 <p class="button-header">Select a Sample Image</p>
40 <div id="stamp-images" class="stamp-images">
41 <img class="stamp-image draggable-image" width="100px" src="data:image/png;base64,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" class="chakra-image css-el3l7b">
42 <img class="stamp-image draggable-image" width="100px" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAbEAAABaCAMAAAAB+ph2AAADAFBMVEVHcEydDgSdDwWcDgSdDgScDgScDgScDgScDgScDgScDgSdDgSeDgayISGfDw+cDgScDgShGA+rLSS1Qjq9VU29WFC9V0+9Vk69Uky9UUq+T0m+Tki+TUe4RT2vMyymIhmgDwfFa2Xksaz66OX66ef66eb66Ob66ef76eb65+X76Ob65+P75+X75+P65eL75uL75uP65eH75eH75eL64+H74+H74+H64uD74uD74uD64eD74eD74d/64d/84d/74N/74N784N/739774N384N3739z839373tz73tv83dz73Nv83Nv73Nr729r83Nv829r729n829n72tn829r82tn72tj82tj72dj82tn82dj82df82Nf92dj82db819X82NX92Nb81dT91tX91dT81NP81NT91dT91NP91NP809P809L909P909L909P80tL90tL909L90tH90dH90c/90tD90ND/0M/9z8/9z87/z8/9zs790M/9zs3/zs79zc3/zs3/zcz9zcz/zs39zMz/zMv9zMv/zMz9y8v/y8v/y8v9ysr/ysrztbTQcm2xOzOeDgWcEASiGxLOgHr34+D66Ob75+P75uL83tz82Nb919X80dH/zMv/y8r/ycnmmpawNy/Ag4O3SED129j66uj56ef65+XTeHSeEgj/y8n9ysj/yMj839z9yMjoo6CgFQudEAb66+fahoLBVU7HY13srKqqKiHCXFWtMSn34Nz33drswr7UjYfIb2i/XFW7UUm2RTzBYlzJcmzemZX009H32tj32NX62NXjrKfnq6j509L50dD5z876zczusrDGZ2HCWFK7S0PJa2Xcj4v2xMP5ysn7xsXOe3XZlpHEZl/strPNd3GzPTXNc2yfEwmkHhXkn5zNcGm5TkXzysjpsa7ai4fpqKXyu7nNa2b2v73eop7qu7fjm5fFX1nzzsviqKOoKB/uyMXltbDdnJizPzfxwL6oJR3flJH3vLzRg33Yko3UhoHYgX354d/wuLbKaWPVfXlwOlItAAABAHRSTlMAMoSwzuX+/+/WvZhMBxD3cPz49PDw8PDw8PDw8PP3+h/t4dnZ2dnY2NnY2djY2djZ2djZ2dnY2dnY2dnZ2djZ2djZ2NjZ2NnZ2NnZ2dnY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" alt="Approved Stamp" />
43 </div>
44 <div id="added-stamps-container" class="added-stamps-container">
45 <!-- Added stamps will be displayed here -->
46 </div>
47 <p class="button-header">Or create a custom image</p>
48 <div class="button-container">
49 <button id="choose-file-button-image" class="choose-file-button">Choose Image to stamp</button>
50 </div>
51
52 <p class="button-header">Or create a text-based stamp</p>
53 <div class="button-container">
54 <button id="choose-file-button-text" class="choose-file-button">Create text-based stamp</button>
55 </div>
56
57 <p class="button-header">Set custom annotations behaviour</p>
58 <div class="custom-container">
59 <input type="checkbox" id="custom-annotation-behavior" name="custom-annotation-behavior" checked>
60 <label for="custom-annotation-behavior">Move off page to delete</label><br>
61 </div>
62
63 </div>
64 `;
65
66 content.appendChild(sampleContent);
67 sidePanel.appendChild(content);
68
69 // Create a wrapper to contain both the side panel and viewer
70 const wrapper = document.createElement('div');
71 wrapper.id = 'viewer-wrapper';
72 wrapper.className = 'viewer-wrapper';
73
74 // Insert the wrapper before the viewer element
75 viewerElement.parentNode.insertBefore(wrapper, viewerElement);
76
77 // Move the viewer element into the wrapper and add the side panel
78 wrapper.appendChild(sidePanel);
79 wrapper.appendChild(viewerElement);
80
81 // Add the viewer-with-panel class to the viewer element
82 viewerElement.classList.add('viewer-with-panel');
83
84 // Add event handler for the choose-file button
85 const chooseFileButton = document.getElementById('choose-file');
86 if (chooseFileButton) {
87 chooseFileButton.addEventListener('click', () => {
88 // Get the WebViewer instance and document viewer
89 const instance = window.WebViewer ? window.WebViewer.getInstance() : null;
90 if (instance && instance.Core && instance.Core.documentViewer) {
91 const documentViewer = instance.Core.documentViewer;
92 const loadingMessage = document.createElement('div');
93 loadingMessage.id = 'loading-message';
94 loadingMessage.style.marginTop = '10px';
95
96 // Add loading message to the panel if it doesn't exist
97 if (!document.getElementById('loading-message')) {
98 chooseFileButton.parentNode.appendChild(loadingMessage);
99 } else {
100 loadingMessage = document.getElementById('loading-message');
101 }
102
103 // Create and trigger the file picker
104 const filePickerButton = UIElements.createFilePickerButton(documentViewer, loadingMessage);
105 filePickerButton.click();
106 } else {
107 console.error('WebViewer instance not found');
108 }
109 });
110 }
111
112 // Add event handler for the choose-file-button-image
113 const chooseImageButton = document.getElementById('choose-file-button-image');
114 if (chooseImageButton) {
115 chooseImageButton.addEventListener('click', () => {
116 // Create hidden file input element
117 const fileInput = document.createElement('input');
118 fileInput.type = 'file';
119 fileInput.accept = 'image/*';
120 fileInput.style.display = 'none';
121
122 // Add event handler for file selection
123 fileInput.addEventListener('change', (event) => {
124 const file = event.target.files[0];
125 if (file && file.type.startsWith('image/')) {
126 const reader = new FileReader();
127 reader.onload = function (e) {
128 // Show the stamp slider instead of directly adding the image
129 UIElements.showStampSlider(e.target.result);
130
131 // Clean up
132 document.body.removeChild(fileInput);
133 };
134 reader.readAsDataURL(file);
135 } else {
136 alert('Please select a valid image file.');
137 document.body.removeChild(fileInput);
138 }
139 });
140
141 // Add file input to DOM and trigger click
142 document.body.appendChild(fileInput);
143 fileInput.click();
144 });
145 }
146
147 // Add click event listeners to the two images on lines 41 and 42
148 const stampImages = document.querySelectorAll('#stamp-images .stamp-image');
149 stampImages.forEach((image) => {
150 image.addEventListener('click', () => {
151
152 // Ensure the added stamps container exists
153 let addedStampsContainer = document.getElementById('added-stamps-container');
154 if (!addedStampsContainer) {
155 addedStampsContainer = document.createElement('div');
156 addedStampsContainer.id = 'added-stamps-container';
157 addedStampsContainer.style.display = 'flex';
158 addedStampsContainer.style.flexWrap = 'wrap';
159 addedStampsContainer.style.gap = '10px';
160 addedStampsContainer.style.marginTop = '20px';
161
162 // Append the container to the panel section or body as a fallback
163 const panelSection = document.querySelector('.panel-section');
164 if (panelSection) {
165 panelSection.appendChild(addedStampsContainer);
166 } else {
167 document.body.appendChild(addedStampsContainer);
168 }
169 }
170
171 // Call UIElements.showStampSlider with the image source
172 UIElements.showStampSlider(image.src);
173 });
174 });
175
176 console.log('Side panel created successfully');
177 }
178
179 // Function to add content to the side panel
180 addPanelContent(content) {
181 const panelContent = document.querySelector('.side-panel-content');
182 if (panelContent) {
183 const contentDiv = document.createElement('div');
184 contentDiv.className = 'panel-section';
185 contentDiv.innerHTML = content;
186 panelContent.appendChild(contentDiv);
187 }
188 }
189
190 // Setup event handlers for the UI elements
191 static updateUIElements() {
192 const pageInput = document.getElementById('input-page-number');
193 const pageCountLabel = document.getElementById('page-count-label');
194 const pageTextContent = document.getElementById('page-text-content');
195 const pageAnnotationsContent = document.getElementById('page-annotations-content');
196 const annotationsLabel = document.getElementById('annotations-label');
197
198 // Function to update page count display
199 const updatePageCount = () => {
200 const totalPages = window.pageCount || 0;
201 pageCountLabel.textContent = `of ${totalPages} full page text`;
202 };
203 // Function to update content displays
204 const updateContent = (pageNumber) => {
205 // Update annotations label
206 annotationsLabel.textContent = `Page ${pageNumber} text under annotations:`;
207
208 // Call the global function to get text
209 if (window.getAllTextFromDocument) {
210 window.getAllTextFromDocument(pageNumber);
211 }
212 };
213
214 // Function to immediately update UI content from global variables
215 const updateUIContent = () => {
216 if (window.textContent !== undefined) {
217 pageTextContent.value = window.textContent || 'No text found on this page.';
218 }
219 if (window.annotTextContent !== undefined) {
220 pageAnnotationsContent.value = window.annotTextContent || 'No annotation text found on this page.';
221 }
222 };
223
224 // Expose the UI update function globally
225 window.updateUIContent = updateUIContent;
226
227 // Function to update page input from window.currentPage
228 const updatePageInput = () => {
229 console.log('Updating page input. Current page:', window.currentPage);
230 if (window.currentPage !== undefined && pageInput.value != window.currentPage) {
231 console.log('Page input changed, updating to:', window.currentPage);
232 pageInput.value = window.currentPage;
233 updateContent(window.currentPage);
234 }
235 };
236
237 // Monitor for page count updates and current page changes
238 const checkPageCount = () => {
239 updatePageCount();
240 updatePageInput();
241 if (window.pageCount > 0) {
242 pageInput.max = window.pageCount;
243 }
244 };
245
246 // Update content for the current page
247 updatePageCount();
248 updatePageInput();
249 updateContent(window.currentPage);
250 }
251
252 static createFilePickerButton(documentViewer, loadingMessage) {
253 const filePickerButton = document.createElement('button');
254 filePickerButton.className = 'btn-style';
255 filePickerButton.id = 'file-picker';
256 filePickerButton.innerHTML = 'Pick File';
257 filePickerButton.onclick = () => {
258 // Open file picker dialog
259 const fileInput = document.createElement('input');
260 fileInput.type = 'file';
261 fileInput.accept = '.pdf,.doc,.docx,.ppt,.pptx';// Accept Office file formats
262 fileInput.onchange = async (event) => {
263 const file = event.target.files[0];
264 if (file) {
265 try {
266 const url = URL.createObjectURL(file);
267
268 // Extract file extension from filename
269 const fileName = file.name;
270 const extension = fileName.split('.').pop().toLowerCase();
271
272 console.log(`Loading file: ${file.name} (extension: ${extension}) blob URL: ${url}`);
273
274 // Add loading feedback
275 loadingMessage.innerHTML = `Loading ${fileName}...`;
276 loadingMessage.style.color = '#666';
277
278 // Load document with explicit extension and error handling
279 await documentViewer.loadDocument(url, {
280 extension: extension,
281 // Add additional options for better compatibility
282 withCredentials: false,
283 customHeaders: {}
284 });
285
286 } catch (error) {
287 console.error('Error loading file:', error);
288 loadingMessage.innerHTML = `Error loading file: ${error.message || 'Network failure'}. Please try again.`;
289 loadingMessage.style.color = 'red';
290
291 // Clean up blob URL on error
292 if (url) {
293 URL.revokeObjectURL(url);
294 }
295 }
296 }
297 };
298 fileInput.click();
299 };
300
301 return filePickerButton;
302 }
303
304
305 static imageSizeAdjuster(imageBlob, maxWidth, maxHeight) {
306 return new Promise((resolve) => {
307 const img = new Image();
308 img.src = URL.createObjectURL(imageBlob);
309 img.onload = () => {
310 const aspectRatio = img.naturalWidth / img.naturalHeight;
311 //const maxWidth = maxWidth //300; // Set your desired max width
312 //const maxHeight = // 300; // Set your desired max height
313 let width = maxWidth;
314 let height = maxHeight;
315
316 if (aspectRatio > 1) {
317 // Landscape orientation
318 height = maxHeight / aspectRatio;
319 } else {
320 // Portrait orientation
321 width = maxWidth * aspectRatio;
322 }
323
324 resolve({ width, height });
325 };
326 });
327 }
328
329
330 static sliderValueDisplay() {
331 // Create the stamp slider container
332 const stampSlider = document.createElement('div');
333 stampSlider.id = 'stamp-slider';
334
335 // Create container for the image preview
336 const imageContainer = document.createElement('div');
337 imageContainer.className = 'image-container';
338
339 // Create controls section for the slider
340 const controlsContainer = document.createElement('div');
341 controlsContainer.id = 'controls-container';
342
343
344 // Create the preview image element
345 const previewImage = document.createElement('img');
346 previewImage.id = 'preview-stamp-image';
347
348 // Create slider container
349 const sliderContainer = document.createElement('div');
350 sliderContainer.className = 'slider-container';
351
352
353 // Create slider label
354 const sliderLabel = document.createElement('label');
355 sliderLabel.textContent = 'Width: ';
356
357 // Create the range slider
358 const widthSlider = document.createElement('input');
359 widthSlider.type = 'range';
360 widthSlider.min = '10';
361 widthSlider.max = '300';
362 widthSlider.value = '150';
363 widthSlider.id = 'width-slider';
364
365 // Create value display
366 const valueDisplay = document.createElement('span');
367 valueDisplay.id = 'width-value';
368 valueDisplay.textContent = '150px';
369
370 // Create info paragraph
371 const infoParagraph = document.createElement('p');
372 infoParagraph.textContent = 'Click the stamp above to add it or select the button below to return to adding more stamps. This stamp is also selectable from the toolbar now.';
373
374 // Create Add More Stamps button
375 const addMoreButton = document.createElement('button');
376 addMoreButton.className = 'add-more-stamps-button';
377 addMoreButton.textContent = 'Add More Stamps';
378 addMoreButton.id = 'add-more-stamps-button';
379
380 // Assemble the slider components
381 sliderContainer.appendChild(sliderLabel);
382 sliderContainer.appendChild(widthSlider);
383 sliderContainer.appendChild(valueDisplay);
384
385 // Append to .panel-section
386 const panelSection = document.querySelector('.panel-section');
387 if (panelSection) {
388 panelSection.appendChild(stampSlider);
389 } else {
390 console.error('Panel section not found');
391 }
392
393 // Slider event handler
394 widthSlider.addEventListener('input', function () {
395 const width = this.value;
396 valueDisplay.textContent = width + 'px';
397 previewImage.style.width = width + 'px';
398 });
399
400 // Preview image click handler - adds stamp and closes slider
401 previewImage.addEventListener('click', function () {
402 UIElements.addStampFromSlider(previewImage.src, widthSlider.value);
403 UIElements.closeStampSlider();
404 });
405
406 // Add More Stamps button handler - clones the image with the current width
407 addMoreButton.addEventListener('click', function () {
408 const clonedImage = previewImage.cloneNode(true);
409 clonedImage.style.width = widthSlider.value + 'px';
410 clonedImage.className = 'draggable-image';
411
412 // Append the cloned image to a designated container
413 const addedStampsContainer = document.getElementById('added-stamps-container');
414 if (addedStampsContainer) {
415 addedStampsContainer.appendChild(clonedImage);
416 } else {
417 console.error('Added stamps container not found');
418 }
419
420 // Close the slider
421 UIElements.closeStampSlider();
422 });
423
424 stampSlider.appendChild(imageContainer);
425 imageContainer.appendChild(previewImage);
426 controlsContainer.appendChild(sliderLabel);
427 controlsContainer.appendChild(sliderContainer);
428 controlsContainer.appendChild(infoParagraph);
429 controlsContainer.appendChild(addMoreButton);
430 stampSlider.appendChild(controlsContainer);
431 console.log('Stamp slider created: ', stampSlider);
432
433 return stampSlider;
434 }
435
436 // Helper method to show the stamp slider
437 static showStampSlider(imageSrc) {
438 let stampSlider = document.getElementById('stamp-slider');
439 if (!stampSlider) {
440 stampSlider = UIElements.sliderValueDisplay();
441 }
442
443 const previewImage = stampSlider.querySelector('#preview-stamp-image');
444 if (previewImage) {
445 console.log('Setting preview image source');
446 previewImage.src = imageSrc;
447 previewImage.style.width = '150px'; // Reset to default width
448 }
449
450 const panelSection = document.querySelector('.panel-section');
451 if (panelSection) {
452 console.log('Appending stamp slider to panel section');
453 panelSection.appendChild(stampSlider);
454 }
455 }
456
457 static closeStampSlider() {
458 const stampSlider = document.getElementById('stamp-slider');
459 if (stampSlider) {
460 stampSlider.remove();
461 }
462 }
463
464 static addStampFromSlider(imageSrc, width) {
465 console.log(`Adding stamp with width: ${width}`);
466 // Implementation for adding the stamp
467 }
468}
469
470
471
472
1// ES6 Compliant Syntax
2// GitHub Copilot, version: 1.0.0, model: GPT-4, version: 2024-06, date: 2025-10-23
3// File: showcase-demos/pdf-stamps/observableUtils.js
4
5function registerObservable() {
6 const sidePanel = document.getElementById('side-panel');
7 if (!sidePanel) {
8 console.error('Side panel not found. Observable not registered.');
9 return;
10 }
11
12 // Register existing elements with the 'draggable-image' class
13 sidePanel.querySelectorAll('.draggable-image').forEach((img) => {
14 img.setAttribute('draggable', true);
15 });
16
17 // Set up the MutationObserver for dynamically added elements
18 const observer = new MutationObserver((mutationsList) => {
19 mutationsList.forEach((mutation) => {
20 if (mutation.type === 'childList') {
21 mutation.addedNodes.forEach((node) => {
22 if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('draggable-image')) {
23 node.setAttribute('draggable', true);
24 }
25 });
26 }
27 });
28 });
29
30 observer.observe(sidePanel, { childList: true, subtree: true });
31}
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales