Compare Files Showcase Demo Sample Code

Requirements
View Demo

Easily compare two files by converting them to images and laying them on top of each other for a pixel-by-pixel comparison.

This demo allows you to:

  • Choose your own PDF files to compare
  • Display the differences
  • Customize compliance checks
  • Download a PDF with differences highlighted

The full API is enabled by passing the fullAPI option into the WebViewer constructor. This is already included in your sample code below.

Implementation steps

To add PDF comparison capability with WebViewer:
Step 1: Get started with your preferred web stack for WebViewer
Step 2: Add the ES6 JavaScript sample code provided in this guide

1/* ES6 Compliant Syntax */
2/* GitHub Copilot Chat v0.22.4, GPT-4o model, July 22, 2025 */
3/* File: index.js */
4
5import WebViewer from '@pdftron/webviewer';
6
7// File Compare section
8//
9// Code to customize the WebViewer to use PDFNet for file comparison,
10// The two files are loaded and compared visually, the differences are
11// highlighted in a new document which is then displayed in the viewer
12//
13
14const defaultDoc1 = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/houseplan-A.pdf';
15const defaultDoc2 = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/houseplan-B.pdf';
16
17let error1 = '';
18let error2 = '';
19let file1 = null;
20let file2 = null;
21let title1 = '';
22let title2 = '';
23
24const customizeUI = async (instance) => {
25
26 // Load the default files for comparison
27 // Default file 1
28 try {
29 file1 = await instance.Core.createDocument(defaultDoc1);
30 title1 = file1.getFilename() || 'File 1';
31 } catch (err) {
32 error1 = 'Error loading File 1: ' + err.message;
33 console.error(error1);
34 throw err;
35 }
36
37 // Default file 2
38 try {
39 file2 = await instance.Core.createDocument(defaultDoc2);
40 title2 = file2.getFilename() || 'File 2';
41 } catch (err) {
42 error2 = 'Error loading File 2: ' + err.message;
43 console.error(error2);
44 throw err;
45 }
46
47 // Run the visual comparison
48 await compare(instance);
49}
50
51// Function to compare two PDF documents
52// This function uses PDFNet to create a visual difference comparison
53// The resulting document highlights the differences and is displayed in the viewer
54const compare = async (instance) => {
55 if (!file1 || !file2) {
56 console.error('Files are not loaded properly.');
57 return;
58 }
59
60 // Initialize PDFNet
61 const { PDFNet } = instance.Core;
62 await PDFNet.initialize();
63
64 // Create a new PDFDoc for each loaded Document
65 const [PDFDoc1, PDFDoc2] = await Promise.all([
66 file1.getPDFDoc(),
67 file2.getPDFDoc()
68 ]);
69
70 // Helper function to get all pages from a PDFDoc
71 const getPageArray = async (PDFDoc) => {
72 const pageArray = [];
73 const pageIterator = await PDFDoc.getPageIterator();
74
75 for (pageIterator; await pageIterator.hasNext(); pageIterator.next()) {
76 const page = await pageIterator.current();
77 pageArray.push(page);
78 }
79 return pageArray;
80 }
81
82 // Get all pages from both documents
83 const [doc1Pages, doc2Pages] = await Promise.all([
84 getPageArray(PDFDoc1),
85 getPageArray(PDFDoc2)
86 ]);
87 console.log(doc1Pages, doc2Pages);
88
89 // Create a new PDFDoc to hold the visual difference comparison
90 const comparisonDoc = await PDFNet.PDFDoc.create();
91 comparisonDoc.lock();
92
93 // Check for mismatched page counts
94 const biggestLength = Math.max(doc1Pages.length, doc2Pages.length);
95
96 // Add a blank page if one document has fewer pages
97 for (let i = 0; i < biggestLength; i++) {
98 let page1 = doc1Pages[i];
99 let page2 = doc2Pages[i];
100
101 if (!page1) {
102 page1 = await PDFDoc1.pageCreate();
103 }
104 if (!page2) {
105 page2 = await PDFDoc2.pageCreate();
106 }
107
108 // Append the page with highlighted differences to the comparison document
109 await comparisonDoc.appendVisualDiff(page1, page2);
110 }
111 comparisonDoc.unlock();
112
113 // Load the comparison document into the viewer
114 instance.UI.loadDocument(comparisonDoc);
115}
116
117// WebViewer section
118//
119// This code initializes the WebViewer with the basic settings
120// that are found in the default showcase WebViewer
121//
122
123const searchParams = new URLSearchParams(window.location.search);
124const history = window.history || window.parent.history || window.top.history;
125const licenseKey = 'YOUR_LICENSE_KEY_HERE';
126const element = document.getElementById('viewer');
127
128// Initialize WebViewer with the specified settings
129WebViewer({
130 path: '/lib',
131 licenseKey: licenseKey,
132 fullAPI: true, // Required for PDFNet features
133}, element).then((instance) => {
134 // Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool
135 const cloudyTools = [
136 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
137 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
138 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
139 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
140 ];
141 instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
142 instance.UI.disableTools(cloudyTools);
143
144 // Set default toolbar group to Annotate
145 instance.UI.setToolbarGroup('toolbarGroup-Annotate');
146
147 // Set default tool on mobile devices to Pan.
148 // https://apryse.atlassian.net/browse/WVR-3134
149 if (isMobileDevice()) {
150 instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
151 }
152
153 instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
154 if (searchParams.has('file')) {
155 searchParams.delete('file');
156 history.replaceState(null, '', '?' + searchParams.toString());
157 }
158 });
159
160 instance.Core.annotationManager.enableAnnotationNumbering();
161
162 instance.UI.NotesPanel.enableAttachmentPreview();
163
164 // Add the demo-specific functionality
165 customizeUI(instance).then(() => {
166 // Create UI controls after demo is initialized
167 createUIControls(instance);
168 });
169});
170
171// Function to check if the user is on a mobile device
172const isMobileDevice = () => {
173 return (
174 /(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(
175 window.navigator.userAgent
176 ) ||
177 /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(
178 window.navigator.userAgent.substring(0, 4)
179 )
180 );
181}
182
183// Cleanup function for when the demo is closed or page is unloaded
184const cleanup = (instance) => {
185 if (typeof instance !== 'undefined' && instance.UI) {
186 // Clean up any resources if needed
187 console.log('Cleaning up compare-files demo');
188 }
189};
190
191// Register cleanup for page unload
192window.addEventListener('beforeunload', () => cleanup());
193window.addEventListener('unload', () => cleanup());
194
195// UI section
196//
197// Helper code to add controls to the viewer holding the buttons
198// This code creates a container for the buttons, styles them, and adds them to the viewer
199//
200
201// File Picker Component - Reusable component for both files
202const createFilePicker = (instance, fileConfig) => {
203 const {
204 fileNumber,
205 labelText,
206 getColor,
207 titleVar,
208 errorVar,
209 setFile,
210 setTitle,
211 setError
212 } = fileConfig;
213
214 // Create the wrapper and container elements
215 const wrapper = document.createElement('div');
216 wrapper.className = 'file-wrapper';
217 const container = document.createElement('div');
218 container.className = 'file-container';
219
220 // Error message
221 const error = document.createElement('p');
222 error.className = 'error-message';
223 error.textContent = errorVar;
224
225 // Selected file text
226 const selectedFile = document.createElement('p');
227 selectedFile.textContent = titleVar || 'No file selected';
228
229 // Label for the file
230 const label = document.createElement('div');
231 label.className = 'file-label';
232
233 // Create a block showing the color used to highlight differences
234 const diffOptions = new instance.Core.PDFNet.PDFDoc.DiffOptions();
235 const color = getColor(diffOptions);
236 const colorBlock = document.createElement('span');
237 colorBlock.className = 'color-block';
238 colorBlock.style.backgroundColor = `rgba(${color.R}, ${color.G}, ${color.B}, ${color.A})`;
239
240 // Text for the label
241 const text = document.createElement('span');
242 text.className = 'file-label-text';
243 text.textContent = labelText;
244
245 label.appendChild(colorBlock);
246 label.appendChild(text);
247
248 // Button to select the file
249 const button = document.createElement('button');
250 button.className = 'btn-filepicker with-margin';
251 button.textContent = 'Select File';
252 button.onclick = () => {
253 const input = document.createElement('input');
254 input.type = 'file';
255 input.accept = '.pdf';
256 input.onchange = async (event) => {
257 const file = event.target.files[0];
258 if (file) {
259 try {
260 const doc = await instance.Core.createDocument(file);
261 setFile(doc);
262 const newTitle = file.name || `File ${fileNumber}`;
263 setTitle(newTitle);
264 selectedFile.textContent = newTitle;
265 error.textContent = ''; // Clear any previous errors
266 console.log(`File ${fileNumber} selected:`, newTitle);
267 } catch (err) {
268 const errorMessage = `Error loading File ${fileNumber}: ${err.message}`;
269 setError(errorMessage);
270 error.textContent = errorMessage;
271 console.error(errorMessage);
272 }
273 }
274 };
275 input.onerror = (err) => {
276 const errorMessage = `Error selecting File ${fileNumber}: ${err.message}`;
277 setError(errorMessage);
278 error.textContent = errorMessage;
279 console.error(errorMessage);
280 };
281 input.click();
282 };
283
284 container.appendChild(label);
285 container.appendChild(button);
286 container.appendChild(error);
287 container.appendChild(selectedFile);
288
289 wrapper.appendChild(container);
290 return wrapper;
291};
292
293// First File Button
294const firstFile = (instance) => {
295 return createFilePicker(instance, {
296 fileNumber: 1,
297 labelText: 'First File',
298 getColor: (diffOptions) => diffOptions.getColorA(),
299 titleVar: title1,
300 errorVar: error1,
301 setFile: (doc) => { file1 = doc; },
302 setTitle: (title) => { title1 = title; },
303 setError: (error) => { error1 = error; }
304 });
305};
306
307// Second File Button
308const secondFile = (instance) => {
309 return createFilePicker(instance, {
310 fileNumber: 2,
311 labelText: 'Second File',
312 getColor: (diffOptions) => diffOptions.getColorB(),
313 titleVar: title2,
314 errorVar: error2,
315 setFile: (doc) => { file2 = doc; },
316 setTitle: (title) => { title2 = title; },
317 setError: (error) => { error2 = error; }
318 });
319};
320
321// Compare button
322const compareButton = (instance) => {
323 const button = document.createElement('button');
324 button.className = 'btn-compare bottom-margin';
325 button.textContent = 'Compare Files';
326 button.onclick = async () => {
327 if (!file1 || !file2) {
328 console.error('Files are not selected.');
329 return;
330 }
331 await compare(instance);
332 };
333 return button;
334};
335
336const createUIControls = (instance) => {
337 // Create a container for all controls (label, dropdown, and buttons)
338 const controlsContainer = document.createElement('div');
339 controlsContainer.className = 'button-container';
340
341 controlsContainer.appendChild(firstFile(instance));
342 controlsContainer.appendChild(secondFile(instance));
343 controlsContainer.appendChild(compareButton(instance));
344
345 element.insertBefore(controlsContainer, element.firstChild);
346};

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales