Easily compare changes in semantic categories, such as headers, paragraphs, and numbers, and generate a document with a summary of differences for review.
Semantic comparison is a method of identifying and highlighting differences between two versions of a document by analyzing the meaning of the text rather than just the literal wording.
This demo allows you to:
Implementation steps
To use semantic compare in PDFs with WebViewer:
Step 1: Choose your preferred web stack
Step 2: Download any required modules listed in the Demo Dependencies section below
Step 3: Add the ES6 JavaScript sample code provided through GitHub.
Demo Dependencies
This sample uses the following:
1// ES6 Compliant Syntax
2// GitHub Copilot Chat v0.22.4, GPT-4o model, July 15, 2025
3// File: index.js
4
5// Semantic Compare section
6//
7// Code to customize the WebViewer to enable semantic compare functionality,
8// opens a comparison panel, and adds the option to enable synchronization
9// of scroll and zoom
10//
11
12const defaultDoc1 = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/semantic_1.pdf';
13const defaultDoc2 = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/semantic_2.pdf';
14
15let isSyncScrollZoom = false;
16let canStartComparing = false;
17let file1 = null;
18let file2 = null;
19let error1 = '';
20let error2 = '';
21let title1 = '';
22let title2 = '';
23
24// Collection of features and elements to disable or enable in the UI
25const disabledElements = [
26 'toolbarGroup-Shapes',
27 'toolbarGroup-Insert',
28 'toolbarGroup-Annotate',
29 'toolbarGroup-FillAndSign',
30 'toolbarGroup-Measure',
31 'toolbarGroup-Edit',
32 'toolbarGroup-Forms',
33 'searchButton',
34 'toggleNotesButton',
35];
36const enabledElements = [
37 'contentEditButton',
38];
39
40// Customize the WebViewer for the Semantic Compare demo
41const customizeUI = (instance) => {
42 const { UI, Core } = instance;
43 // Disable unnecessary elements and enable the text editing tools
44 UI.disableElements(disabledElements);
45 UI.enableElements(enabledElements);
46
47 // Enable document comparison features
48 UI.enableFeatures([UI.Feature.MultiViewer]);
49 UI.enterMultiViewerMode();
50 UI.enableFeatures(UI.Feature.ComparePages);
51 UI.setToolbarGroup(UI.ToolbarGroup.VIEW);
52 instance.Core.documentViewer.setToolMode(instance.Core.documentViewer.getToolModeMap()['Pan']);
53
54 // Load the default documents into the viewers when ready
55 Core.getDocumentViewers()[0]?.loadDocument(defaultDoc1);
56
57 UI.addEventListener(UI.Events.MULTI_VIEWER_READY, () => {
58 Core.getDocumentViewers()[1]?.loadDocument(defaultDoc2);
59 });
60
61 // When the documents are loaded, get references and start the semantic compare
62 instance.Core.getDocumentViewers()[0].addEventListener('documentLoaded', () => {
63 file1 = instance.Core.getDocumentViewers()[0].getDocument();
64 title1 = file1?.getFilename() || 'File Version A';
65 }, { once: true });
66
67 instance.Core.getDocumentViewers()[1].addEventListener(
68 'documentLoaded',
69 () => {
70 file2 = instance.Core.getDocumentViewers()[1].getDocument();
71 title2 = file2?.getFilename() || 'File Version B';
72
73 setTimeout(() => {
74 startSemanticCompare(instance);
75 }, 100);
76 },
77 { once: true }
78 );
79
80 // Add event listeners to synchronize scroll and zoom, when enabled
81 const addSyncListener = (
82 documentViewer,
83 UI
84 ) => {
85 documentViewer
86 ?.getViewerElement()
87 ?.closest('.CompareContainer')
88 ?.querySelector('control-buttons button')
89 ?.addEventListener('click', () => {
90 setTimeout(() => {
91 isSyncScrollZoom = UI.isMultiViewerSyncing();
92 }, 0);
93 });
94 };
95
96 // Add event listeners to both document viewers
97 return new Promise((resolve) => {
98 instance.Core.documentViewer.addEventListener('annotationsLoaded', resolve);
99 const documentViewer1 = instance.Core.getDocumentViewers()[0];
100 const documentViewer2 = instance.Core.getDocumentViewers()[1];
101
102 addSyncListener(documentViewer1, UI);
103 addSyncListener(documentViewer2, UI);
104 });
105};
106
107// Function to start the semantic compare
108const startSemanticCompare = (instance) => {
109 instance.UI.openElements(['loadingModal']);
110
111 instance.UI.startTextComparison();
112 instance.UI.openElements(['comparePanel']);
113
114 canStartComparing = true;
115 updateUIControls(instance);
116};
117
118// Cleanup function for when the demo is closed or page is unloaded
119const cleanup = (instance) => {
120 if (typeof instance !== 'undefined' && instance.UI) {
121 instance.UI.exitMultiViewerMode();
122 instance.UI.disableFeatures([instance.UI.Feature.MultiViewerMode]);
123 }
124};
125
126// WebViewer section
127//
128// This code initializes the WebViewer with the basic settings
129// that are found in the default showcase WebViewer
130//
131
132import WebViewer from '@pdftron/webviewer';
133
134const searchParams = new URLSearchParams(window.location.search);
135const history = window.history || window.parent.history || window.top.history;
136const licenseKey = 'YOUR_LICENSE_KEY_HERE';
137const element = document.getElementById('viewer');
138
139WebViewer({
140 path: '/lib',
141 serverUrl: null,
142 forceClientSideInit: true,
143 fullAPI: true,
144 css: '../styles/stylesheet.css',
145 ui: 'beta',
146 licenseKey: licenseKey,
147}, element).then((instance) => {
148 // Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool
149 const cloudyTools = [
150 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
151 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
152 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
153 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
154 ];
155 instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
156 instance.UI.disableTools(cloudyTools);
157
158 // Set default toolbar group to Annotate
159 instance.UI.setToolbarGroup('toolbarGroup-Annotate');
160
161 // Set default tool on mobile devices to Pan.
162 // https://apryse.atlassian.net/browse/WVR-3134
163 if (isMobileDevice()) {
164 instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
165 }
166
167 instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
168 if (searchParams.has('file')) {
169 searchParams.delete('file');
170 history.replaceState(null, '', '?' + searchParams.toString());
171 }
172 });
173
174 instance.Core.annotationManager.enableAnnotationNumbering();
175
176 instance.UI.NotesPanel.enableAttachmentPreview();
177
178 // Add the demo-specific functionality
179 customizeUI(instance).then(() => {
180 // Create UI controls after demo is initialized
181 createUIControls(instance);
182 });
183});
184
185// Function to check if the user is on a mobile device
186const isMobileDevice = () => {
187 return (
188 /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(
189 window.navigator.userAgent
190 ) ||
191 /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(
192 window.navigator.userAgent.substring(0, 4)
193 )
194 );
195}
196
197// Register cleanup for page unload
198window.addEventListener('beforeunload', cleanup);
199window.addEventListener('unload', cleanup);
200
201// UI section
202//
203// Helper code to add controls to the viewer holding the buttons
204// This code creates a container for the buttons, styles them, and adds them to the viewer
205//
206
207// File Picker for File Version A and B
208const fileVersion = (instance, error, title, versionText, viewerId, documentSide) => {
209 const fileVersionContainer = document.createElement('div');
210 fileVersionContainer.style.marginBottom = '10px';
211
212 const errorSpan = document.createElement('span');
213 errorSpan.style.fontSize = '12px';
214 errorSpan.style.color = 'red';
215 errorSpan.textContent = error;
216 fileVersionContainer.appendChild(errorSpan);
217
218 const titleP = document.createElement('p');
219 titleP.style.textOverflow = 'ellipsis';
220 titleP.style.overflow = 'hidden';
221 titleP.style.whiteSpace = 'nowrap';
222 titleP.style.fontSize = '14px';
223 titleP.innerHTML = `<strong>File Version ${versionText}:</strong> ${documentSide}`;
224
225 const titleInput = document.createElement('input');
226 titleInput.placeholder = title;
227 titleInput.autofocus = true;
228 titleInput.style.width = '100%';
229 titleInput.style.paddingLeft = '5px';
230 titleInput.style.fontSize = '14px';
231 titleInput.disabled = true;
232
233 const filePicker = document.createElement('button');
234 filePicker.className = 'btn-filepicker';
235 filePicker.textContent = 'Select File';
236 filePicker.onclick = () => {
237 const input = document.createElement('input');
238 input.type = 'file';
239 input.accept = '.pdf';
240 input.onchange = async (event) => {
241 const file = event.target.files[0];
242 if (file) {
243 const doc = await instance.Core.createDocument(file);
244
245 if (viewerId === 0) {
246 if (file1) {
247 file1.unloadResources();
248 }
249 file1 = doc;
250 instance.Core.getDocumentViewers()[0].loadDocument(doc);
251 error1 = '';
252 title1 = file.name || 'File Version A';
253 titleInput.placeholder = title1;
254 } else {
255 if (file2) {
256 file2.unloadResources();
257 }
258 file2 = doc;
259 instance.Core.getDocumentViewers()[1].loadDocument(doc);
260 error2 = '';
261 title2 = file.name || 'File Version B';
262 titleInput.placeholder = title2;
263 }
264 canStartComparing = true;
265 updateUIControls();
266 }
267 };
268 input.onerror = (error) => {
269 if (viewerId === 0) {
270 error1 = 'Error loading file: ' + error.message;
271 } else {
272 error2 = 'Error loading file: ' + error.message;
273 }
274 };
275 input.click();
276 };
277
278 fileVersionContainer.appendChild(errorSpan);
279 fileVersionContainer.appendChild(titleP);
280 fileVersionContainer.appendChild(titleInput);
281 fileVersionContainer.appendChild(filePicker);
282
283 return fileVersionContainer;
284}
285
286// Compare Button
287const compare = (instance) => {
288 const compareButton = document.createElement('button');
289 compareButton.className = 'btn-compare';
290 compareButton.disabled = true;
291 compareButton.onclick = async () => {
292 if (file1 && file2) {
293 // Ensure the correct documents are loaded in the viewers
294 const currentDoc1 = instance.Core.getDocumentViewers()[0].getDocument();
295 const currentDoc2 = instance.Core.getDocumentViewers()[1].getDocument();
296
297 if (!currentDoc1 || currentDoc1 !== file1) {
298 await instance.Core.getDocumentViewers()[0].loadDocument(file1);
299 }
300
301 if (!currentDoc2 || currentDoc2 !== file2) {
302 await instance.Core.getDocumentViewers()[1].loadDocument(file2);
303 }
304
305 if (isSyncScrollZoom) {
306 instance.UI.enableMultiViewerSync();
307 }
308
309 startSemanticCompare(instance);
310 canStartComparing = false;
311 updateUIControls(instance);
312 } else {
313 alert('Please ensure both documents are loaded before comparing.');
314 }
315 };
316 compareButton.textContent = 'Compare';
317
318 return compareButton;
319}
320
321// Sync Scroll and Zoom Checkbox
322const syncScrollZoom = (instance) => {
323 const syncScrollZoomContainer = document.createElement('div');
324 syncScrollZoomContainer.style.display = 'flex';
325 syncScrollZoomContainer.style.alignItems = 'center';
326
327 const syncScrollZoomCheckbox = document.createElement('input');
328 syncScrollZoomCheckbox.type = 'checkbox';
329 syncScrollZoomCheckbox.className = 'checkbox-syncScrollZoom';
330 syncScrollZoomCheckbox.checked = isSyncScrollZoom;
331 syncScrollZoomCheckbox.onchange = (e) => {
332 isSyncScrollZoom = e.target.checked;
333 if (isSyncScrollZoom) {
334 instance.UI.enableMultiViewerSync();
335 } else {
336 instance.UI.disableMultiViewerSync();
337 }
338 };
339
340 const syncScrollZoomLabel = document.createElement('label');
341 syncScrollZoomLabel.className = 'label-syncScrollZoom';
342 syncScrollZoomLabel.style.marginLeft = '2';
343 syncScrollZoomLabel.style.fontSize = 'xsmall';
344 syncScrollZoomLabel.textContent = 'Synchronize Scroll and Zoom';
345
346 syncScrollZoomContainer.appendChild(syncScrollZoomCheckbox);
347 syncScrollZoomContainer.appendChild(syncScrollZoomLabel);
348
349 return syncScrollZoomContainer;
350}
351
352// Reset Default Documents Button
353const resetDefaultDocuments = (instance) => {
354 const resetButton = document.createElement('button');
355 resetButton.className = 'btn-reset';
356 resetButton.textContent = 'Reset Default Documents';
357 resetButton.onclick = async () => {
358 instance.Core.getDocumentViewers()[0].loadDocument(defaultDoc1);
359 instance.Core.getDocumentViewers()[1].loadDocument(defaultDoc2);
360
361 // Wait for documents to load before getting references
362 instance.Core.getDocumentViewers()[0].addEventListener('documentLoaded', () => {
363 file1 = instance.Core.getDocumentViewers()[0].getDocument();
364 title1 = file1?.getFilename() || 'File Version A';
365 }, { once: true });
366
367 instance.Core.getDocumentViewers()[1].addEventListener('documentLoaded', () => {
368 file2 = instance.Core.getDocumentViewers()[1].getDocument();
369 title2 = file2?.getFilename() || 'File Version B';
370 }, { once: true });
371
372 error1 = '';
373 error2 = '';
374 canStartComparing = true;
375 updateUIControls();
376 };
377
378 return resetButton;
379
380}
381
382// Create UI controls after WebViewer is initialized
383const createUIControls = (instance) => {
384 // Create a container for all controls (label, dropdown, and buttons)
385 const controlsContainer = document.createElement('div');
386 controlsContainer.className = 'button-container';
387
388 // File Version $A [left]
389 controlsContainer.appendChild(fileVersion(instance, error1, title1, 'A', 0, '{left}'));
390
391 // File Version $B [right]
392 controlsContainer.appendChild(fileVersion(instance, error2, title2, 'B', 1, '{right}'));
393
394 // Compare Button
395 controlsContainer.appendChild(compare(instance));
396
397 // Sync Scroll and Zoom Checkbox
398 controlsContainer.appendChild(syncScrollZoom(instance));
399
400 // Reset Default Documents Button
401 controlsContainer.appendChild(resetDefaultDocuments(instance));
402
403 element.insertBefore(controlsContainer, element.firstChild);
404};
405
406const updateUIControls = () => {
407 const compareButton = document.querySelector('.btn-compare');
408 compareButton.disabled = !canStartComparing;
409}
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales