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:
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.
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// 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';
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 DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales