Semantic Text Compare Showcase Demo Sample Code

Requirements
View Demo

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 code sample follows the Apryse Showcase: Semantic Text Compare Demo,
and allows you to:

  • Upload your own PDF files to compare
  • Set comparison file scroll and zoom synchronization
  • View changes side-by-side with semantic differences highlighted

Implementation steps
To use semantic compare in PDFs with WebViewer:

Step 1: Follow get started in your preferred web stack for WebViewer
Step 2: Add the ES6 JavaScript sample code provided here or through GitHub.

1// ES6 Compliant Syntax
2// GitHub Copilot Chat v0.22.4, GPT-4o model, July 15, 2025
3// File: index.js
4
5// Symantic 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 fullAPI: true, // Required for PDFNet features
142 licenseKey: licenseKey,
143}, element).then((instance) => {
144 // Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool
145 const cloudyTools = [
146 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
147 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
148 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
149 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
150 ];
151 instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
152 instance.UI.disableTools(cloudyTools);
153
154 // Set default toolbar group to Annotate
155 instance.UI.setToolbarGroup('toolbarGroup-Annotate');
156
157 // Set default tool on mobile devices to Pan.
158 // https://apryse.atlassian.net/browse/WVR-3134
159 if (isMobileDevice()) {
160 instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
161 }
162
163 instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
164 if (searchParams.has('file')) {
165 searchParams.delete('file');
166 history.replaceState(null, '', '?' + searchParams.toString());
167 }
168 });
169
170 instance.Core.annotationManager.enableAnnotationNumbering();
171
172 instance.UI.NotesPanel.enableAttachmentPreview();
173
174 // Add the demo-specific functionality
175 customizeUI(instance).then(() => {
176 // Create UI controls after demo is initialized
177 createUIControls(instance);
178 });
179});
180
181// Function to check if the user is on a mobile device
182const isMobileDevice = () => {
183 return (
184 /(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(
185 window.navigator.userAgent
186 ) ||
187 /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(
188 window.navigator.userAgent.substring(0, 4)
189 )
190 );
191}
192
193// Register cleanup for page unload
194window.addEventListener('beforeunload', cleanup);
195window.addEventListener('unload', cleanup);
196
197// UI section
198//
199// Helper code to add controls to the viewer holding the buttons
200// This code creates a container for the buttons, styles them, and adds them to the viewer
201//
202
203// File Picker for File Version A and B
204const fileVersion = (instance, error, title, versionText, viewerId, documentSide) => {
205 const fileVersionContainer = document.createElement('div');
206 fileVersionContainer.style.marginBottom = '10px';
207
208 const errorSpan = document.createElement('span');
209 errorSpan.style.fontSize = '12px';
210 errorSpan.style.color = 'red';
211 errorSpan.textContent = error;
212 fileVersionContainer.appendChild(errorSpan);
213
214 const titleP = document.createElement('p');
215 titleP.style.textOverflow = 'ellipsis';
216 titleP.style.overflow = 'hidden';
217 titleP.style.whiteSpace = 'nowrap';
218 titleP.style.fontSize = '14px';
219 titleP.innerHTML = `<strong>File Version ${versionText}:</strong> ${documentSide}`;
220
221 const titleInput = document.createElement('input');
222 titleInput.placeholder = title;
223 titleInput.autofocus = true;
224 titleInput.style.width = '100%';
225 titleInput.style.paddingLeft = '5px';
226 titleInput.style.fontSize = '14px';
227 titleInput.disabled = true;
228
229 const filePicker = document.createElement('button');
230 filePicker.className = 'btn-filepicker';
231 filePicker.textContent = 'Select File';
232 filePicker.onclick = () => {
233 const input = document.createElement('input');
234 input.type = 'file';
235 input.accept = '.pdf';
236 input.onchange = async (event) => {
237 const file = event.target.files[0];
238 if (file) {
239 const doc = await instance.Core.createDocument(file);
240
241 if (viewerId === 0) {
242 if (file1) {
243 file1.unloadResources();
244 }
245 file1 = doc;
246 instance.Core.getDocumentViewers()[0].loadDocument(doc);
247 error1 = '';
248 title1 = file.name || 'File Version A';
249 titleInput.placeholder = title1;
250 } else {
251 if (file2) {
252 file2.unloadResources();
253 }
254 file2 = doc;
255 instance.Core.getDocumentViewers()[1].loadDocument(doc);
256 error2 = '';
257 title2 = file.name || 'File Version B';
258 titleInput.placeholder = title2;
259 }
260 canStartComparing = true;
261 updateUIControls();
262 }
263 };
264 input.onerror = (error) => {
265 if (viewerId === 0) {
266 error1 = 'Error loading file: ' + error.message;
267 } else {
268 error2 = 'Error loading file: ' + error.message;
269 }
270 };
271 input.click();
272 };
273
274 fileVersionContainer.appendChild(errorSpan);
275 fileVersionContainer.appendChild(titleP);
276 fileVersionContainer.appendChild(titleInput);
277 fileVersionContainer.appendChild(filePicker);
278
279 return fileVersionContainer;
280}
281
282// Compare Button
283const compare = (instance) => {
284 const compareButton = document.createElement('button');
285 compareButton.className = 'btn-compare';
286 compareButton.disabled = true;
287 compareButton.onclick = async () => {
288 if (file1 && file2) {
289 // Ensure the correct documents are loaded in the viewers
290 const currentDoc1 = instance.Core.getDocumentViewers()[0].getDocument();
291 const currentDoc2 = instance.Core.getDocumentViewers()[1].getDocument();
292
293 if (!currentDoc1 || currentDoc1 !== file1) {
294 await instance.Core.getDocumentViewers()[0].loadDocument(file1);
295 }
296
297 if (!currentDoc2 || currentDoc2 !== file2) {
298 await instance.Core.getDocumentViewers()[1].loadDocument(file2);
299 }
300
301 if (isSyncScrollZoom) {
302 instance.UI.enableMultiViewerSync();
303 }
304
305 startSemanticCompare(instance);
306 canStartComparing = false;
307 updateUIControls(instance);
308 } else {
309 alert('Please ensure both documents are loaded before comparing.');
310 }
311 };
312 compareButton.textContent = 'Compare';
313
314 return compareButton;
315}
316
317// Sync Scroll and Zoom Checkbox
318const syncScrollZoom = (instance) => {
319 const syncScrollZoomContainer = document.createElement('div');
320 syncScrollZoomContainer.style.display = 'flex';
321 syncScrollZoomContainer.style.alignItems = 'center';
322
323 const syncScrollZoomCheckbox = document.createElement('input');
324 syncScrollZoomCheckbox.type = 'checkbox';
325 syncScrollZoomCheckbox.className = 'checkbox-syncScrollZoom';
326 syncScrollZoomCheckbox.checked = isSyncScrollZoom;
327 syncScrollZoomCheckbox.onchange = (e) => {
328 isSyncScrollZoom = e.target.checked;
329 if (isSyncScrollZoom) {
330 instance.UI.enableMultiViewerSync();
331 } else {
332 instance.UI.disableMultiViewerSync();
333 }
334 };
335
336 const syncScrollZoomLabel = document.createElement('label');
337 syncScrollZoomLabel.className = 'label-syncScrollZoom';
338 syncScrollZoomLabel.style.marginLeft = '2';
339 syncScrollZoomLabel.style.fontSize = 'xsmall';
340 syncScrollZoomLabel.textContent = 'Synchronize Scroll and Zoom';
341
342 syncScrollZoomContainer.appendChild(syncScrollZoomCheckbox);
343 syncScrollZoomContainer.appendChild(syncScrollZoomLabel);
344
345 return syncScrollZoomContainer;
346}
347
348// Reset Default Documents Button
349const resetDefaultDocuments = (instance) => {
350 const resetButton = document.createElement('button');
351 resetButton.className = 'btn-reset';
352 resetButton.textContent = 'Reset Default Documents';
353 resetButton.onclick = async () => {
354 instance.Core.getDocumentViewers()[0].loadDocument(defaultDoc1);
355 instance.Core.getDocumentViewers()[1].loadDocument(defaultDoc2);
356
357 // Wait for documents to load before getting references
358 instance.Core.getDocumentViewers()[0].addEventListener('documentLoaded', () => {
359 file1 = instance.Core.getDocumentViewers()[0].getDocument();
360 title1 = file1?.getFilename() || 'File Version A';
361 }, { once: true });
362
363 instance.Core.getDocumentViewers()[1].addEventListener('documentLoaded', () => {
364 file2 = instance.Core.getDocumentViewers()[1].getDocument();
365 title2 = file2?.getFilename() || 'File Version B';
366 }, { once: true });
367
368 error1 = '';
369 error2 = '';
370 canStartComparing = true;
371 updateUIControls();
372 };
373
374 return resetButton;
375
376}
377
378// Create UI controls after WebViewer is initialized
379const createUIControls = (instance) => {
380 // Create a container for all controls (label, dropdown, and buttons)
381 const controlsContainer = document.createElement('div');
382 controlsContainer.className = 'button-container';
383
384 // File Version $A [left]
385 controlsContainer.appendChild(fileVersion(instance, error1, title1, 'A', 0, '{left}'));
386
387 // File Version $B [right]
388 controlsContainer.appendChild(fileVersion(instance, error2, title2, 'B', 1, '{right}'));
389
390 // Compare Button
391 controlsContainer.appendChild(compare(instance));
392
393 // Sync Scroll and Zoom Checkbox
394 controlsContainer.appendChild(syncScrollZoom(instance));
395
396 // Reset Default Documents Button
397 controlsContainer.appendChild(resetDefaultDocuments(instance));
398
399 element.insertBefore(controlsContainer, element.firstChild);
400};
401
402const updateUIControls = () => {
403 const compareButton = document.querySelector('.btn-compare');
404 if (compareButton) {
405 compareButton.disabled = !canStartComparing;
406 }
407}

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales