Easily add stamps to your documents. The viewer includes several sample stamps, and you can also create custom stamps 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.
Once you generate your license key, it will automatically be included in your sample code below.
Apryse collects some data regarding your usage of the SDK for product improvement.
The data that Apryse collects include:
For clarity, no other data is collected by the SDK and Apryse has no access to the contents of your documents.
If you wish to continue without data collection, contact us and we will email you a no-tracking trial key for you to get started.
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
7const licenseKey = 'YOUR_WEBVIEWER_LICENSE_KEY';
8
9// State variables for demo
10const dropPoint = {};
11const customStampDemoFile = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/sales-invoice.pdf';
12
13function initializeWebViewer() {
14 WebViewer(
15 {
16 path: '/lib',
17 initialDoc: customStampDemoFile,
18 enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File
19 licenseKey: licenseKey, // Replace with your license key
20 },
21 document.getElementById('viewer')
22 ).then((instance) => {
23
24 createUIElements();
25
26 // Set toolbar group immediately after WebViewer loads
27 instance.UI.setToolbarGroup(['toolbarGroup-Insert']);
28
29 // Wait for document to load before setting tool mode (more reliable)
30 instance.Core.documentViewer.addEventListener('documentLoaded', () => {
31
32 // Set the rubber stamp tool mode
33 instance.UI.setToolMode('AnnotationCreateRubberStamp');
34
35 // Add a small delay before opening the panel
36 setTimeout(() => {
37 instance.UI.openElements(['rubberStampPanel']);
38 }, 150);
39
40 // Register events after the page is loaded
41 demoComplete();
42 registerDragEvents();
43 });
44 });
45};
46
47const getIframeWindow = () => {
48 return (
49 document.getElementById('viewer')?.getElementsByTagName('iframe')?.[0]?.contentWindow || window
50 );
51};
52
53const getIframeContext = () => {
54 return (
55 document.getElementById('viewer')?.getElementsByTagName('iframe')?.[0]?.contentWindow
56 .instance || window.WebViewer.getInstance()
57 );
58};
59
60// Drag and drop handlers
61const dragOver = (e) => {
62 e.preventDefault();
63 //const imgData = e.dataTransfer.getData('text/plain');
64 e.target.style.opacity = 1;
65 e.preventDefault();
66};
67
68// Update drop event to adjust dropPoint to fit document coordinates
69const drop = (e) => {
70 console.log('File dropped:', e);
71 e.preventDefault();
72 e.stopPropagation();
73 const documentViewer = getIframeContext().Core.documentViewer;
74 const scrollElement = documentViewer.getScrollViewElement();
75 const scrollLeft = scrollElement.scrollLeft || 0;
76 const scrollTop = scrollElement.scrollTop || 0;
77 const zoomLevel = documentViewer.getZoomLevel();
78 dropPoint.current = { x: (e.pageX + scrollLeft), y: (e.pageY + scrollTop) };
79 const displayMode = documentViewer.getDisplayModeManager().getDisplayMode();
80 const doc = documentViewer.getDocument();
81
82 const documentWidth = doc.getPageInfo(1).width;
83 const documentHeight = doc.getPageInfo(1).height;
84 dropPoint.current.x = Math.min(Math.max(dropPoint.current.x, 0), documentWidth);
85 dropPoint.current.y = Math.min(Math.max(dropPoint.current.y, 0), documentHeight);
86 const page = displayMode.getSelectedPages(dropPoint.current, dropPoint.current);
87
88 if (!page.first) {
89 console.error('128 Invalid page location for point:', dropPoint.current);
90
91 // Log dimensions of all pages for debugging
92 const pageCount = doc.getPageCount();
93 for (let i = 1; i <= pageCount; i++) {
94 const pageInfo = doc.getPageInfo(i);
95 console.log(`Page ${i} bounds:`, pageInfo);
96 }
97
98 return;
99 }
100
101 const pageInfo = doc.getPageInfo(page.first);
102 console.log('Selected page bounds:', pageInfo);
103
104 let pagePoint = displayMode.windowToPage(dropPoint.current, page.first);
105 console.log('Mapped pagePoint before clamping:', pagePoint);
106
107 if (!pagePoint || pagePoint.x < 0 || pagePoint.y < 0 || pagePoint.x > pageInfo.width || pagePoint.y > pageInfo.height) {
108 console.error('Invalid page coordinates after mapping:', pagePoint);
109 pagePoint = {
110 x: Math.min(Math.max(pagePoint.x || 0, 0), pageInfo.width),
111 y: Math.min(Math.max(pagePoint.y || 0, 0), pageInfo.height),
112 };
113 }
114
115 const imgData = e.dataTransfer.getData('text/plain');
116 if (!imgData) {
117 console.error('No imgData found in dataTransfer');
118 return;
119 }
120
121 console.log('Dimensions of image dataTransfer:', e.dataTransfer);
122 let stampSize = { width: 100, height: 25 }; //Default size
123 let img = new Image();
124 img.src = imgData; // Set the base64 string as the source
125
126 img.onload = () => {
127 document.body.appendChild(img); // Append the image to the DOM temporarily
128 const renderedWidth = img.offsetWidth; // Rendered width of the image
129 const renderedHeight = img.offsetHeight; // Rendered height of the image
130 document.body.removeChild(img); // Remove the image from the DOM
131
132 // You can now use these dimensions to set the stamp size
133 stampSize.width = renderedWidth;
134 stampSize.height = renderedHeight;
135
136 //find slider and get the value for width of image
137 const widthSlider = document.getElementById('width-slider');
138 if (widthSlider) {
139 const sliderValue = parseFloat(widthSlider.value);
140 if (!isNaN(sliderValue) && sliderValue > 0) {
141 stampSize.width = sliderValue;
142 const aspectRatio = renderedWidth / renderedHeight;
143 stampSize.height = sliderValue / aspectRatio;
144 }
145 }
146
147 const viewFactor = 0.66; // Adjust this factor as needed, used to scale the stamp appropriately
148 let stampRect = { width: stampSize.width * zoomLevel * viewFactor, height: stampSize.height * zoomLevel * viewFactor };
149
150 try {
151 addStamp(imgData, pagePoint, stampRect, page.first);
152 console.log('Stamp added at point:', pagePoint, 'with rect:', stampRect);
153 } catch (error) {
154 console.error('Error while adding stamp:', error);
155 console.error('Parameters passed to addStamp:', {
156 imgData,
157 point: pagePoint,
158 rect: stampRect,
159 pageNumber: page.first,
160 });
161 }
162 };
163
164 img.onerror = () => {
165 console.error('Failed to load image from data URL');
166 };
167};
168
169function demoComplete() {
170 //let defaultDocument = customStampDemoFile;
171 //console.log('Demo initialization complete');
172 const documentViewer = getIframeContext().Core.documentViewer;
173 documentViewer.addEventListener('toolModeUpdated', toolModeUpdated);
174 const iframeDoc = getIframeWindow().document.body;
175 iframeDoc.addEventListener('dragover', dragOver);
176 iframeDoc.addEventListener('drop', drop);
177
178 // Allow annotations outside page bounds
179 getIframeContext().Core.Tools.Tool.ALLOW_ANNOTS_OUTSIDE_PAGE = true;
180};
181
182const toolModeUpdated = (type) => {
183 const { id } = type;
184 if (id) {
185 // Check if stamps object exists and has the id
186 if (window.stamps && window.stamps[id]) {
187 data = window.stamps[id];
188 name = id;
189 } else {
190 data = null;
191 }
192 }
193};
194
195const stampPanelWidthOffset = 146;
196
197const addStamp = (
198 imgData,
199 point = {},
200 rect = { height: undefined, width: undefined }
201) => {
202 const { Annotations } = getIframeContext().Core;
203 const { documentViewer, annotationManager } = getIframeContext().Core;
204 const doc = documentViewer.getDocument();
205 const displayMode = documentViewer.getDisplayModeManager().getDisplayMode();
206
207 console.log('Adding stamp at point:', point, 'with rect:', rect);
208
209 //if stamp panel is open add points to x coordinate
210 if (getIframeContext().UI.isElementOpen('rubberStampPanel')) {
211 point.x += stampPanelWidthOffset + (rect.width < 50 ? rect.width + 50 : rect.width); // add width of stamp panel + width
212 console.log('326 Adjusted point for open stamp panel:', point);
213 }
214
215 const page = displayMode.getSelectedPages(point, point);
216 const page_num = page.first || documentViewer.getCurrentPage();
217 if (!page_num || typeof page_num !== 'number') {
218 console.error('Invalid page number:', page_num);
219 return;
220 }
221
222 const page_info = doc.getPageInfo(page_num);
223 const page_point = displayMode.windowToPage(point, page_num);
224 const zoom = documentViewer.getZoomLevel();
225 const stampAnnot = new Annotations.StampAnnotation();
226 stampAnnot.PageNumber = page_num;
227 const rotation = documentViewer.getCompleteRotation(page_num) * 90;
228 stampAnnot.Rotation = rotation;
229 if (rotation === 270 || rotation === 90) {
230 stampAnnot.Width = rect.height / zoom;
231 stampAnnot.Height = rect.width / zoom;
232 } else {
233 stampAnnot.Width = rect.width / zoom;
234 stampAnnot.Height = rect.height / zoom;
235 }
236
237 stampAnnot.X = (page_point.x || page_info.width / 2) - stampAnnot.Width / 2;
238 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
239 stampAnnot.Y = (page_point.y || page_info.height / 2) - stampAnnot.Height / 2;
240 stampAnnot.setImageData(imgData);
241 stampAnnot.Author = annotationManager.getCurrentUser();
242 annotationManager.deselectAllAnnotations();
243 annotationManager.addAnnotation(stampAnnot, true);
244 annotationManager.redrawAnnotation(stampAnnot);
245 annotationManager.selectAnnotation(stampAnnot);
246};
247
248// Register drag and drop events
249function registerDragEvents() {
250 // Required:
251 // target is 'viewer'
252 const dropTarget = document.getElementById('viewer');
253
254 // Just adds the '+' to show while dragging to the drop target
255 dropTarget.addEventListener('dragover', (e) => {
256 e.preventDefault();
257 e.dataTransfer.dropEffect = 'copy'; // with copy, shows '+' symbol during drag
258 });
259
260 dropTarget.addEventListener('drop', (e) => {
261 const imgData = e.dataTransfer.getData('text/plain'); // Retrieve using custom data type
262 if (!imgData) {
263 console.error('No imgData found in dataTransfer');
264 return;
265 }
266
267 const documentViewer = getIframeContext().Core.documentViewer;
268 const scrollElement = documentViewer.getScrollViewElement();
269 const scrollLeft = scrollElement.scrollLeft || 0;
270 const scrollTop = scrollElement.scrollTop || 0;
271 dropPoint.current = { x: e.pageX + scrollLeft, y: e.pageY + scrollTop };
272 const displayMode = documentViewer.getDisplayModeManager().getDisplayMode();
273 const page = displayMode.getSelectedPages(dropPoint.current, dropPoint.current);
274 if (!page.first) {
275 return;
276 }
277 });
278
279 // Custom annotation behavior checkbox handler
280 const setCustomAnnotationBehavior = () => {
281 const isChecked = document.getElementById('custom-annotation-behavior').checked;
282 getIframeContext().Core.Tools.Tool.ALLOW_ANNOTS_OUTSIDE_PAGE = isChecked;
283 };
284
285 // Register event listener for custom annotation behavior checkbox
286 const customAnnotationCheckbox = document.getElementById('custom-annotation-behavior');
287 if (customAnnotationCheckbox) {
288 customAnnotationCheckbox.addEventListener('click', setCustomAnnotationBehavior);
289 }
290
291 // Open custom stamp modal on button click
292 const setTextStamp = () => {
293 getIframeContext().UI.openElements(['customStampModal']);
294 };
295
296 // Register event listener for custom annotation behavior checkbox
297 const onClickCreateTextStamp = document.getElementById('choose-file-button-text');
298 if (onClickCreateTextStamp) {
299 onClickCreateTextStamp.addEventListener('click', setTextStamp);
300 }
301}
302
303function createUIElements() {
304 // Create a container for all controls (label, dropdown, and buttons)
305 // Dynamically load ui-elements.js if not already loaded
306 if (!window.SidePanel) {
307 const script = document.createElement('script');
308 script.src = '/showcase-demos/pdf-stamps/ui-elements.js';
309 script.onload = () => {
310 UIElements.init('viewer');
311
312 };
313 document.head.appendChild(script);
314 }
315}
316
317function loadObservableUtils() {
318 if (!window.registerObservable) {
319 const script = document.createElement('script');
320 script.src = '/showcase-demos/pdf-stamps/observable-utils.js';
321 script.onload = () => {
322 registerObservable();
323 };
324 document.head.appendChild(script);
325 } else {
326 registerObservable();
327 }
328}
329
330initializeWebViewer();
331loadObservableUtils();
332
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/t4dnZ2dnY2NnY2djY2djZ2djZ2dnY2dnY2dnZ2djZ2djZ2NjZ2NnZ2NnZ2dnY2dnZ2djZ2dnZ2NnZ2djY2dnY2djZ2dnY2djZ2dnZ2NnZ2NnZ2djY2NnZ2NnY2djZ2dnZ2NnZ2djZ2djZ2d3p9V47/OnZ2NnZ2djZ2djZ2OH2AvPb2NnY6P7Z2dnZ2eH9/tnm7+zf+e732tre5+zv8fPu6+Tb2tra4eDa2tra3u3v8uvl29ra6eXt3+r06v374ury3ODl4N3q2+Pf4u3c4vnd4OT13fnj2+jm5+bZ3evoKbW/AAAAJApJREFUeAHs1IMVBDEUBdCxbdv9F7jB2vb9eicFhFhHUjTDcvwb+RNESVZUYh+NYvm/96TTJLFNNQQeMC3bcY/xPNDn8kH7oPdycJ8jQCsAN7hVGIIGA/f5ojCKVhuneeM5Q4z7HAlaCbhJkmY5/PU4uiA2FAx4zcuqbtq27dquB1X33VB3dV2P4zjNKAgLGoaBKIDqWdpjZgb/ivabB8Dz/t7n93w+74MeTB6CKf7EBGAccyykBCVXWhkNU8aCRltnvbMu2BhDjMnFHFOOucRaSq0111b7qLPX1ftaawNnA+uec+/9kxAOVhAEARRrb23bttH8/ZtN8kJDhoYshzhe4ASWF0SBFyVRlGRFlVVMUzVN1zTDMC3DwmzLth3kuB50fexPEAYR/AbiIE4gWRrHaZZnRVaUWQXLuiybtqm6qmv6ru+6AY3DNE8z2bws87qs27qv6FjX80TXcd3X817386MILggABIIAgMXGz7008Nt+LvdxPjc89wMIQAAsIKDCoqRqqh4WllGRllVZ2dU1/TFRDupxxFEcrfu57xDMxlqHEydrO1zbtlUrrNv37O9O+b/n3nmA850Z9of0nj57Tp29+F/YS/wQX71+MzbOGx/nMQTDvdG3k7+Z5B5M/eIdrC1MQ9rcL97DGn+Ov7gEWQIhEAiwkCYGEpFEIhFjJSvStVXpKvj9NtZJFmxtsFss2Nre3d3e3drb3drd3wX7ewfE4cHFgezgQnYhO1TIVAo5UCqxkKZR6jVqoNMZtDqDTm80WIDJYDVbTVar1WH7DUTZsJfgmICoE+w5OANXZzCF9bjPPC7gdLlcXozP7/IFvCAQCAUDoUAsEo1FQrF4KB6JR+PxeOo6kUoSySRttpjlyGVzOSykFQp5oloqlbDw9ItavVmvQVenCVqg3W23upDW7d50YavXHdz2hrfD/mD4AQw/foKze5//CXvy+PGXr9++j/HGxpkxsoZheKPMW2Z0koGvKUib/DE9OQNpCI1LDIFNz0PaAnzN8+f5c0uCuSU+X8BfAqhLIBJzjQnhixITS9dwJTirK5TY6jqLu4HDQtw22WJ32C0cUsYlRvfgYu/gAonBlkx2KJfBF8AoEZhKrYYvLaDEdNQY54sSs1rM1JiZU4bDVYbA7Kcn9lOoo8AgDL7wxVBlHueZy+lxOz1OLjGXz+uFL7/XF/QFA+FAKEaNxaIghs6uqbFkPJUA6UQynaXQskkoy8BbrlhAaDkcUlYuILASQqtU4QuJ1ZqNZh23WfvJhFlwx5GcUXS5w5wcDk7kBcOxxWgGWY6YY5Y8kpmZFcFIE47ZXpTZzmZ5/17u/aoNVV9X/4B37nuvSsnYqMUJYLdLpYTYnIypF/P+/QdZ9v3nlghhv1tQKNwtqBWfiC2AMCzxCt+74vWuYqGVH4ApGX6IMwZlHBBWuRTNlAy84lCzumooq1Wshrrmxvq6JkELzrTEluXqxVoZjK1as2rtqjVr9cVgDNn4NoQvboAxNWtrxRbb2iEM0tCso6e7q7urp5cjNOsdHOgf6kcsl7/N4DWMXq7tfuKVDFG5pExLDMqKnEVsEV+EMTTjHMcWccV9MKZm+wDtAGKBGVrxgRicIZZy8Z0Ar5MhFlr5nTtzDlc8pyWevwBily9c5risJU5MotfU5JSHljhdKpWgDM7ulOZKoVn54aPy4/L7H/wry36Sl443EAy55gsL0AylQjF/V9AMuIAsNJMy5WKSZsbYC1Mkxsiw5IuaYg1ShS8CWdIMypqSLzazW6QsmSJrFYBBGaboIJZZtuEJciHW+o3I1Yon6oubNMXQDNUwxc4wRTWDMkwRvnoHhgbQDMqAazg3xc2ApSluC1NUs5BLsVDMXxHNiooVplg0xjDF3WGKaIZU+iL5hWJohicefskUj0PXUR0RyDTF01KmXFgjpngmxVgyReVKMZZ8cWZKwKAMU5yZnSnN3ZkrEWNzd8oPA7KnD5+W//tp9r9/hmI/wRIrPpsvQBi2yPFHNQvGKt6teAfV1OyKjL0jYlKmXi4RkzT1qlQ1m0c1xaO6RtlQjeZh8ZCxWs/mproArAXGPG0earYa0VQtmsc6RsI4N6y/rmboBWEgRvPYqGaqpS12AlmnxihhINYHZAMaozkGYsOcf9YYc8S2cSIWyiXEXIh109KhLe7ir2YyBl42jzGKx95xCaN60DwOqhmMsQ7ZPCgeR9BL2Y5H8zj1PMtE7JTFA73OXEi2eMnigV4QFrY4OTmFZqqFatBVonhgjBIGYhy4IvO4/Plb2XeNsh99J/vXJ/MF9nweYZ4VbCDDFv/w7hWNkW1XZBZSPAyyRXZFZolBZltcVrlsKZwtW1aNMUIYY4rV5EFWzzQx9XmQtdgVscQWgmzlCrZdEUtcE0HGZrTEdeq1gd2KbDDW2t5GlhFlDJp1tvd0EGRsIIOx3sFBziG2hxG2WWNkbyHH4Gz7do2RvT2PsJ1b0YstZEyxOIJe7PExIKMwkmbjtkW64h4jzCA7zPYgwo4ZZGh1nDHFThhkaHXarsicpnicPgdhjIBNhF7siUsTAjY9PYknQhhjit1WL/fsnVkssTyrXu73v8gyC+NrWfbvwmd3iTFnAYjhiQyIMVfegbJ3GBBz7PexjDEm+j1wpSNqR1XqiRzkGJrpiE6DtUNjxBFdILYijDEt2+JajBHE0gKyDRgjiLGwRSDbdL11U9smVmRYW0cnglnumS58sdemaLl3hhxrh57I5GVxG5RtDVt0mWFsxhhjdMQdoxwSNrrb2rF79x6DDF8EMIzRFGMOM7fCGIGLcSkYlGGJ7DNM6vfnDDJijJkwzC4bZPgiMx3lnq5IW6R2TJJhbGyxVHbuPJSxR2WC7PHj8l8/zd7k+ePN7DeLqYnYYtQODjaacSEDrj9U0BOxRKriO+/dQyc0c2uIZhiQodqSiDGuY1VLKyGtmjCLtlhVS+8AM8LMC5m1gztZvWEGZFE7OFoiyWyL1A66x2ot0Z6oJ3KA10Yxsy3SOwQMufgYIBOwiLFuBrxsigNRPRjwciMXfDHgJWZbtUUR43Dv0BZFTGMEM+SiKjL8bYpotp8M23OA3gFmhBm2eIDaAWRiZozlnngW1c6i2dno9mrGjhizdgRk1g4w+wuIRU8EMauH3Z7eIV5zc1SPufDEh+L19DG2+Pj9L7PsB6+8Su34rPCZnihiBemKOzSqpXoPYex3LYuRY3nvIMiUC9HArBJvjGZfuRTCKsELb0xVsRa1WPWp2dc1QlijXZEsi3qPWH4rUc16L2J8euJaLmNPvD9fZ7fijXginEEYG09sS/W+s7OjqwNP7OpK9X6gv5c9GM2eg54oZps351VxG2qx8USW9R61vEOjWKr3EMYuFsfGlGsUwrhD7xu3LEa9P3BgH3cxPBHEOMAr7tCWRTUDL4/TqdlL2BkXnniOHJOwC5fYEwaZOSZhZBmeaFWEM2Ur4Yklq+Kd8hyqGWX2j69s+Jji7+fn784rGoLdjXpPiCkagr1zBbnevfKOlJFjibOFhBgX6CU5Z6hl9wAxigeDWuAFYunhgxSrr61XNItHfWOohV7IRfGIer8yUWbxkLM1ULY23jzWbpAzkky9KIso5Y0s1UROSoc3sh7wSm8efYPINTg4YPEYtHhsRi7UsuGbY9tu4Irbb2y1L+Y5xkeIIZs5tosZKRpjXsgoHszYODGGMe5HsAOH9pFih+geiMa+BWckGVId4UMw6j1JdsricZodnJ0mwbhAe9oVVQu9+BBs2npPjE3Fmwdd0WY/O0O5p3YA2fuh1uP3sUXOr7M3X3k9+2kFjH1WuMtWrYJiMX9I+12iLNZ7aVs/8gVbKIYp5kHmfWzpMqLMCDPJgKwO2SLI+BrqeKRCtjzG3JgiUZZizK0prsljzI0p2u9TkuVtsV3K2qj37A5E6+ns6uwSsNQWvY6xACy9eqTrmBnm0hS5jpllaWmJL4JMyryO7YoY40A0xdq7F8DciMZ17PDBg4cPsFmYImKRZSLGPnFUsYBMxNjIZooRY97HuJBdjOsYGeZ9jAsZdog3GmPsErJR7qXMgl+2Lb4vZBZ8RPsmy155I/t2/jMYu2tbFLEKc8wHD44cMaviewSZb4silt48XiAWVbEyIYZaiMaffg9iJpg5Vl+bI4YppiRLiClXJNmqFatXiRharVllS2RADLnW0xFNMhC7vonr2EY6ooxxtHW2d4IY1Z5ynxCzfSAasonYQCBmVSTIOBJiURUZDt46+J49K45A2aiiyVhCbHSPSQZjFEW6PQmmaPxzxI6AGKIhWyBGT+SjKhJkHL5RmWO0xDMvEIurGEVRxBDNHPPBYyYePnykgjHvYynKyDHoeurx8G8o9mb288/uGmToZVO0f1S40csrNP0jBRl6eYVOornSQ5V3aBmzd9DuKyuRS7ysHjZFKAvEVMyX4DrkSquFoS8iVywsEdFgLWcsibb+CcXDKEMxp62Nh0WDTMU6GJq9iHWYYQ7VgxcPHhZTtWfxQBV9cbPVPupiBFnSi6VoPixG9RhBtF20xZHRJNrYGHVRyLxBG2McB4XscFT7oAzE2OhlU1S0U2708gqtaCbZOfTyoYo7NIjBmMaIbpcmEW3SshiilSYRTMRUzHZvs3el6vE5ir2V/eqzz+7CF55IXbyLWF6h2T7eh1ScEWQ+Ubk4DbKoi1E8rqEaetkVXV6h8yCDsNrauEIDWS2M+dhRF2/3atVk63BWKhaM2Tq8QwuZQQZhqLV+o5ARZNQOhsphXeTs+BPlnjt0d0BmV+zr7ev3Cj1kkEX5oHjQO/IggzAc0QijKnLGy/3N9HY/omaItWuE3hFBZu8Y3ePr/bivijJmuad9wBeycX++ReugfESQyVgqHqeeBZlSYY2o5Q0axqJ4RIThidRFlpYoZABG7WCQi6vYHKe1Y05HfP9hCrIPUCzLfjk/L2N2RSiDMD9OJIOyK9RE1OJMjx4LEe75wyLtQ2e03Ffii1UAhjNSEy2LxBiU6YxBmV3RytjYTPFg2RWhjOZB8WDZFQGNh0WKR/6wyCMVqq1rRTUlU7TWqImopmRtHe2Wjx6uY+lhsbdHZwzKQi5KB+UjKAMxAduOXFKmXAIGYX6cEWRFbLEIZSNjSgZg48g1vteymGLM8hGU2RUB7NhhaqJB5iswlJ09euqElKWHRSojrihl6WHx3ISvVFBGWfTx3orIFRrC0sNiaRrVbksZiHkpm7V8vE/FBzE4yxUrBGP/Z88OMACGgSCKXqY5dkCCWsUesX+r3V5hMEouEGOmeUcdQ8/H7GNkjCrjiPneWNa05x96qPqYfYwXDyJ2xndjufi2fUzZx2ixqzO2dta2V/Yx+9hTY708KDJ5H7OPzX8r1vDQ9zH7WERnLPOmzC6iHLm1MADv2tvG4Rmf6dV7oU2YmXbZhJmZ2SecVZgZh9HtZmZmRjM08zA2uGTde6WyVDlWMqTfrSpZFrg+nY9dUMxLSYnZxy4o4SXOx0rLyisqq9ZV19TWST52Y31DQ/R/c2mM/bpN7WONTViaTT52T0tLa2tLq/Gbysd+bYPSqvSxp9qxPPefPtaR09nVXbWxsKe3T/Kx9/p5ifOxjw8ODBwcMMq7nw5oCvOxfWKNpY/RWXGn1scGbViGTD5Wxuu3yT7WlPGdDUtS0WA9+diwzaqMqH3MTa9wmXxMjL6ze4Z72yQf85rjdtnHhNzm8xd0e506Hwusk/r0VT/5WB5Ud5h9rBASjyOo6fJGduyQ0ix76uavnAfVPrb/IM4xrY+FqK1Kk49V8Hq36GNDBea78g9eDT72j9WAZd2u9LE6v/CasOxj2+LaWBsRfEwViz6GOZYC90sKHxuN75P3HfCxr6BuzORj4z5IAu9u0/R5N/OxzfGBf2JS5WO4j+3U+5idWkn5QfaxIl4/RT42/bXqvtLqYj52Q5bViH2t9rFy8TUR2cfs8a24eh5CH1PGT5CPUU4ltdnsY/nKPnn6uI/lQM2MycdmsfvvOeyaPjvZPqZMU4IqH8OTh9bH5sRGpiQfK3HxRaIBfWw+RX1jC4vMxw7ZrMqw0sfmpCYPSz7Wp2znyEPcx46q42fAx9S5LyD72IymT8fGYz52HCp6ZR+bzIJF1OkY13TZt894eP+lJt18QuFjOMd0PnZYbKJC8rFMGA70scM+3Wj8c7Px0OOk5YidUvqYvPWdlnxsSN1QJfcxTRwCHzuuzlM6RB87ru3TlnbmY2Pw7zOyj+H+tvmdd3t1TTAfG9VdIXUy3sf4iOl9TFqT/OeKPvY9r00HHzvl0g9HteFjayxHrFTlY2F5Ia2RfGyrbuxjPtapiXu5j+nyNMHHciz6VMh8DOePW/IxmmJjDkeergXmY4oUBjvex/iI6X0sTWohU/SxSl75Pfexk0nyR1X6Z1Jp9LCYajVgfqWPwXnLzme55GNHNE0VPMx8TBs/FfMxj+5WnOhjuy371GH42HtQ1yP5GH4ctr3jeFd3pTPMxxQplIk4H4MR0/pYirzViD62AMMY87EpsWdbDy9eVJ950k9VGVEfG66GYsclCqtOqnysdA9fVat49yUfg5tLcg/WZHwtvJsB5mMUu2typTjmY5h73cGuNB/lheBjtWKfOnvHn+t37hb6tI75GJz7Z0Uf64PGXfmO9xx4peBXUhlnPiamExu2pIgXPZGoj5WaThCCj9XB7nmW+dhZ4QS8Lcx9bI4+5nvmRB/DEZuz9rF0WMhCfOEXfWwJb4v5WIS+O3QaPjaCMfOxZiFmPkY587Gj66ib3MdeX6A+VY1zH5v0UJ8mDR+DjueKPrYMr+mO2lg+nlZUPkYp97EVYcyWE/WxUza5TJOPwZz6J+ZjbjoAnSIfu4xOxqcEHwtDpd3ax6Zd/GV3rON/EX0Md+zKmI+F/XjQMXzsNMYxHxNi5mM5mHMfo3tti/lYkPqUQz72Ow19jvE4GP7ZjT4mTrGOdxyOMzh8Kh+jFHzsS7qCP1Efq+Y3DB+2GvKxGjhTsEfBi3RCKBN9bJ4OmoKPDeGiaO1jIbjs7RX8DRB9DJ8pDHIfw7Oo3fAxirmPUcx8DPPd3McCeK/j7FHwOPXptOhjM1jdZfjYNvjaIPjYBLxig+FjeCWvyse6IP0KfewEzbKxBH2MD3YBDE8R+VgVDBDzsRAtObKP4dElVfAxPOS5LX0sk+89vvAv1fDQQ/CxLdBKM/exZny2YTz1wHiM+xjFzMcwd3Ifo693+czHuvHfs7KPebBPxhyDd3wL+dgA7G17OgxywSsFVD5GKfkYjrjtq8R87Acf35imoYFF9DFYY0oNH6OpVHWJ7GPf49sk+Bhub/OWPgYfivRbboMxjpCP/eqDt6WF+1gb3sVDd937KMat3MfaMX4quiy2Us597DkX5JOGj9FUSntF9jE39smwsWUYPvIxfGCZa/jY53Al14DCxw7ifQyQjw34calMzMfgIcXW/8Oy+D342CKcZtiiiFPMVWryMfoKfjP5GK7yDVY+hh+DzNtvg/fgMPkYTpkt4GN4lkiJ+hjF4GMUG4viGObgYxEcCLYobsM+dZh8jL4Qvxf1sd2w1qKP9fth+1syfMyJU1LlY5gWiT6WhntuYj4GK3/Z/+Ez/jX4GGxFRwwfK8nCXdzsY9P0FBd9DOtSLX3sa/iIR31sEDYU8jGcvYXgY6N42IsePDCuAB+j2DjcD+KPg49hnmr4WDv2qdvsY/k2nIzRZRF+LAV9DOdgIfOx3TjjVD6GaZfoY114L6vsnFVUY0v2xt/IM+S6kEX/X/7j7vM07u7u7u7ukrXgdSQrPT4DnRbo9OqGG3TacL/NkNCGQ4dAsCBz6pza365KVWWtM27nDfZJKpWcXbWrvvp94fQx2qRY+z/KtqouqY9NUvoJfgyJVLVYzo918Z4G+LFzWKRV4sdyGDq9w2/0kg7Wx1CN7xI/1o5NZW8D/26EiR9bRljoY4ifIH4MSZUU+tgw+tRczo/xhNfnzWM5uu9zUh/jFJv1+bF76e5DGz92H0VLKj8GRSAbTh+jTYqjD3lIjNJN6mOUAAVxcOA29NXgx3gruQf8WBv9a7sSP0YzclzwY52UMKyPtUBXk/zYdWRFv7exyGHJjylhoY9xXPJjrAQeCn3sHszMBj/GW8gjHj/Whz+kPoasWQ74MbQ0/CP1+m7Aj2HN16zyY8j3+lD62FFZq8U8foye30ygjz2Wur/inad6Uj1ET4Mfwwg4zfwYqqO1CvzYBBQDwY+dp0cC+thKFY1Gkh+7xGv4C6953TWEJT92+RbCNz19bBTxgB+7PP4bFP8f8fSxj6DAvmHwY83okziWg4zbD8bEr8doOpz3uZYRu2tUasznx+bxOTR+DIvFllD6GE39Ge9gznn6kEcfJq5F6p3gxxYiGDMNfgzlQxH82It+Q+/1XDc/9jzK74TPj1F7tdDH0OhUwI+t4jmIxDx1LIdwwI81KWGhjy2xOPP2D+zn/qRsPh0KfawTfRox+DG8NuYff5um5yTQx5BipwJ+zCERpAN+DNEpjR/DqBgPpY/RDFrn8WOo5id8fYzmlavijCnmj7TJjyFVEuDHjuBfFfgxzHUFnx+jwTUGfQwVdP9LX/aqjaVMhK9tTx/rQPhNr3/rrB4W+hhEid9UV+tJUPyI0MdOY+o3+TFMcbU+3EI5lfP1sa/Qn9UjAT82F7FeAwE/hugpjR87hbInlD6WpB/pId7VRtWgr4/hxIDgx6LQVEx+bBdTHPixBtzu5sdeVqQvRfJj8ltNQR9L4huuLf5GV52EPoZwzAj7+pih1OP+ff+44hTqPZMfO8QU55MtpMVv+usxBA8kP+ZoqRTwYxlMcho/dhLNh9LHsEgW/NgCzg6I9Rh9ylWxHKtBPpj8WB2KbPBjKPJ23PwYdpQ6JT9GU+U10seyEcdVfV2c9KgQ9vkxV7w4G/Bj6NOmyY8hAQZ8fmydik4/xYpIMcmPOVpqDvgxRGc1fgw/81wYfWyFZtBfCbLlMTQVL3g/2J58bKu6BT9WjwLe5McSFGsFP3YLtzv5sR4aWtLEj9GLrkt9bMN5wGdV8GMVwj4/5oqnZyU/hj71mfxYLdLI58eoqvy10MdKFBuS/NiPHA9OwI8hGtP5sRhSMYw+VqDJL+DHbsPZgYc9ZI0GecGP7WEe6DL4se4UEor4sS66vd7Nj43jVcSPJSiNpT424RJGV31+bMcVbgr4MbuOXzNM/Nhl9GnE4MdG0acTPj9G67wZTx/7XAt1bkzyY44zAVMBP4ZoUuPHbuK+zTD6WB3pmAE/VqBOe6PiLi2BBT+WR8Fq8mMLiHWRPoZ/ZZz82MUsfYXgx2i835X62KTjjMe1gB9zhUckP2aL199gfmwWn9vkxzoRG/H5MaqBTnm1IqryGeLHMITeV1KvpkAfW8b9Gj82hya2wuhj6/ScB/zYmWM4O4DCY1usn1H7ZU1+DNPYOvixDoyTTn6sFWNYlq5pepHUx2xSe9VUJ/FjcXsY/JgtPr3B/FgT+mTyY/gN4gE/VqJZzZvGauhljcSPoaUlGz+G6M81fiyBVAyjjz2EJq685MeuUs497GH0jmui8ChgUDH4MZ6zGsCPoYoruPgxnHizXG2BPvYy45ZUun8R/NibbOF95sfeAaV+aGgIH6iV+TGMmi0GP/YJ9OnXAT+2SSudT3wSL/sx8WNfqsaK2cKPfYujKj82m0ITYfixszRYED9Gc8Othz9SPvHTTxL8GOaMGoMf49kmD34M1dGKix/rj7ivZKCPPYgcyGSSdy23Npy/qPJjZeGhw9VLGj/Wh4/s7QJvpGj2/wD4sRO4weDHeGLaD/gxmnQyn/5snN7pu8SPNeONbPwYRzV+7CQPimH4MVrDRokfO0oj0yL9mAmfH8PEVG3wY2kKpcGPYdYruvgxnHizXbWBPraL9YqVH+OwnR9D/C4Bt0SxJgI/tok+GfwY90nyYzTnRT/Zh9QAP1ZCSzZ+7JCid6v82E2kWDwUP0Y7GR3gxzL0H0qddp8fwzwW6SrjxxYQ6QA/xicGXPzY6UiFKxboY7hn18qPtSFs58cgZhwKfgxZEwc/1sSbvWX82CYiB5IfI6V0/dPL9CG/CX6MW7LxY0imX6v8GM/Rvw7Fj9XS+gv8GGVdupVqEp8f4wNXazo/1p1moRP8GIa8cQc/Rife7Fcq0MfS2Hm38mMctvNjiF8Q/NhlzHrXiR97kLeNdX5slPvUR/yYfHn0C1n6dZgfS+N9bPwYok0KP1biWuhHYfixMylKHPgr9tC/qKGzPgr9eHR4UvdX5MMfd7K/Ih6gVYe/4p2Ritc1oY/9kT5JtdVf8SLCdn/FSxQ/9j7/iAce9QHix96LPi3r/opMqdxN/opUiiQp/Ypfgr9iI1r6tsVfcQzRH7C/YimFJmZC8WNr1Lzir7hetoCR/ooZPBN5VR+ri3CKwV/x6cdwYsDur7iIXsTVK400EPrYeYxjVn/FVYTt/oqIJwIGOofVxCjxY1H0aVbVx/q5Tz+Cv2JCDvP0hJbYXzGHlmz+ihxlf0XlGHLtD0PxYw003Sj+imWoQ1T6K17hU7Ksj+11KKfu2V8Relna4a+IxGzV/BUxTBWEPjaOrLb6K3LY7q+I+LK0EitiqCZ/xRkmK1gf+9yBcnCY/RWTEt+TD2PL59hf8cdoyeavyFHyV2y+R1kfNofjx2i/tkHxVyQVkb5T6a+Y5zyO5qU+dr5GOejezf6KUFHa7P6KR6DwXdb8Fa+hlhD62APYxbL6KyI8bPdXRPyG9Fccwkclf8V97tPUrNTHci3K8ehR9lcckAlKe1eKv+IgNrRs/ooc9f0Vf9o0qIIZcyH5MVonnlf9FXXIskDmbw8oI+XkubV8oSOj/LapI4q/Ildxdn/FDB4H3V/xtfi/0MewiN2w+isWEbb7KyK+L/0V9/Fxl8hfUYFTq5dLN/c3h6Jqn5oUf8UDfXNS9VdES/csa9e876+oRgfWizr4lG78YSh+rIcEqTOqv+K49p4r5K+4iL7Yrl3VX5GrOKu/Iiao6pUyf8VjlJveoIgjAVmrvyKH7f6KiNfDXxGPYpL8FZsr9ulQ9Vf8tRa6ofgrzjveZPq7YtNjq1IT8S2rv6JbH6OlVFrzV8xrSyP2V2yt0PQV1V/xZagrnmn1V8Q+W125v2KMvlJPHytgM9nqr8hhu79iJ8Zw+Ctihk7Nkr9ipZ2XGc1f8YaWGqq/4qZrH8A/qoio5RpotPsruvWxDnqkdX/FtAo7sr/i46NGk0CBNH9FLvKs/ooLKNpWyv0V09RZTx/DA9Jq9VfksN1fEfEh+CuOYkTqJ3/FD0w5+zSj+ysuaTyh6q944EL9fH9FRM0cPJT+iiH4sSQOl2r+ih0RvsYVf8WuuL3p+gXdX7EBVZzVX7EW35vhr4gtO6/ywGRXsPorctjur4h4J/srogjIXiZ/xZGEo0+bZf6KfWr2aP6Krge55PsrmlHqY+4v8FfM0h6i7q+oAmXnVX/FvTYrc5wv81fkIs/mrzjBKWb4K9IjlPL0MfpwVdes/ooctvsrUjwywv6Km4wYwV/xkydtfRrcL/dXVAn6Tc1f0XU2oc/nx+zRqvUbjX+Bv2KevrpHlvkrcoGbOqr7KxbSRtNHDH9FPjFg8Vd8bg0y0PRXvIM9PTZQYlv9FREu2v0VZxFX/BXfl0WWKP6Kw0af4k0Wf8XfIJ7Q/BVdLgL1vj5mjWZP9f1l/or0tMfL/RXruIo1/BULV7OKHtVx1vRX7EIVZ/NXPIcUu2b6K0I1vv6SHUjKVn/FYWS43V+R46q/4jLvJKr+ipuDSp9qDvat/op8y5Lmr+hyEZjy9bET+sGPmujAXGmp8R/tr7i409DaXndl93z3f46/YvOJK/2nT80c5kb/56/45/buKrt1GIrCsBLDDjMzZ7D3lp/LzB1ivcNMT9Lq+QXLnkLgfH9ivqLMV5T5ii4yMl/RqPmKDtL8fswAf0z8saFQYCP182CEPyb+2NvHEUBpp/9jhj8m/hilnYFmZYg/Jv7YGzUrinHijxnij1GMG6qM4o+Z4Y9RZRzKp+KPGeGPDeXToS4s/pgJ/thQFx4K3n3xx3T3x6aC91DJF39Me39squSrhgM0+3r7Y+KPXZ0BCTXqNA7c3unsj4k/9vQMhNWkUwc4O7/Q1R8Tf+zqGoD/Rk1r2ADqzVZbvh/T7/ux/0fHAOInaq5oIjD68jqfXVuOh/HeWj6X515bJp/h3l6B21tevA6vyFMc7r0reYubj3wevvMM3rdXLpW5t1fh9pZXuVKt1V0ArtVQi8X8DiQ9i1s+tTKf37IdF5JGBYKhcCSqpv0Cvw0Q2iqmcrsAAAAASUVORK5CYII=" 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