> For the complete documentation index, see [llms.txt](https://docs.apryse.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.apryse.com/web/get-started/samples/showcase-demo-compare-files.md).

# Compare Files Showcase Demo Sample Code

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

{% hint style="info" %}
**Requirements**

*These packages are required to use these features in production. Trial keys have unlimited access to all features*

<a href="/web/get-started/readme.md" class="button primary">Web SDK</a><a href="https://apryse.com/capabilities#Compare" class="button primary">Package: Compare</a><a href="/web/full-api/full-api-overview.md" class="button primary">Full API</a><a href="https://showcase.apryse.com/compare-files" class="button primary">Live demo</a>
{% endhint %}

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](/web/full-api/full-api-overview.md) 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](/web/get-started/readme.md) 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-license-key/apryse-license-key platform="WEB\_VIEWER" variant="compact" %}

<pre class="language-js" data-line-numbers><code class="lang-js">/* ES6 Compliant Syntax */
/* GitHub Copilot Chat v0.22.4, GPT-4o model, July 22, 2025 */
/* File: index.js */

import WebViewer from '@pdftron/webviewer';

// File Compare section
// 
// Code to customize the WebViewer to use PDFNet for file comparison,
// The two files are loaded and compared visually, the differences are 
// highlighted in a new document which is then displayed in the viewer
// 

const defaultDoc1 = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/houseplan-A.pdf';
const defaultDoc2 = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/houseplan-B.pdf';

let error1 = '';
let error2 = '';
let file1 = null;
let file2 = null;
let title1 = '';
let title2 = '';

const customizeUI = async (instance) => {

    // Load the default files for comparison
    // Default file 1
    try {
        file1 = await instance.Core.createDocument(defaultDoc1);
        title1 = file1.getFilename() || 'File 1';
    } catch (err) {
        error1 = 'Error loading File 1: ' + err.message;
        console.error(error1);
        throw err;
    }

    // Default file 2
    try {
        file2 = await instance.Core.createDocument(defaultDoc2);
        title2 = file2.getFilename() || 'File 2';
    } catch (err) {
        error2 = 'Error loading File 2: ' + err.message;
        console.error(error2);
        throw err;
    }

    // Run the visual comparison
    await compare(instance);
}

// Function to compare two PDF documents
// This function uses PDFNet to create a visual difference comparison
// The resulting document highlights the differences and is displayed in the viewer
const compare = async (instance) => {
    if (!file1 || !file2) {
        console.error('Files are not loaded properly.');
        return;
    }

    // Initialize PDFNet
    const { PDFNet } = instance.Core;
    await PDFNet.initialize();

    // Create a new PDFDoc for each loaded Document
    const [PDFDoc1, PDFDoc2] = await Promise.all([
        file1.getPDFDoc(),
        file2.getPDFDoc()
    ]);

    // Helper function to get all pages from a PDFDoc
    const getPageArray = async (PDFDoc) => {
        const pageArray = [];
        const pageIterator = await PDFDoc.getPageIterator();

        for (pageIterator; await pageIterator.hasNext(); pageIterator.next()) {
            const page = await pageIterator.current();
            pageArray.push(page);
        }
        return pageArray;
    }

    // Get all pages from both documents
    const [doc1Pages, doc2Pages] = await Promise.all([
        getPageArray(PDFDoc1),
        getPageArray(PDFDoc2)
    ]);
    console.log(doc1Pages, doc2Pages);

    // Create a new PDFDoc to hold the visual difference comparison
    const comparisonDoc = await PDFNet.PDFDoc.create();
    comparisonDoc.lock();

    // Check for mismatched page counts
    const biggestLength = Math.max(doc1Pages.length, doc2Pages.length);

    // Add a blank page if one document has fewer pages
    for (let i = 0; i &#x3C; biggestLength; i++) {
        let page1 = doc1Pages[i];
        let page2 = doc2Pages[i];

        if (!page1) {
            page1 = await PDFDoc1.pageCreate();
        }
        if (!page2) {
            page2 = await PDFDoc2.pageCreate();
        }

        // Append the page with highlighted differences to the comparison document
        await comparisonDoc.appendVisualDiff(page1, page2);
    }
    comparisonDoc.unlock();

    // Load the comparison document into the viewer
    instance.UI.loadDocument(comparisonDoc);
}

// WebViewer section
// 
// This code initializes the WebViewer with the basic settings
// that are found in the default showcase WebViewer
//

const searchParams = new URLSearchParams(window.location.search);
const history = window.history || window.parent.history || window.top.history;
const licenseKey = '<code class="expression">visitor.claims.wvKey || "YOUR_WEBVIEWER_LICENSE_KEY"</code>'; 
const element = document.getElementById('viewer');

// Initialize WebViewer with the specified settings
WebViewer({
    path: '/lib',
    licenseKey: licenseKey,
    fullAPI: true, // Required for PDFNet features
}, element).then((instance) => {
    // Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool
    const cloudyTools = [
        instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
        instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
        instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
        instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
    ];
    instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
    instance.UI.disableTools(cloudyTools);

    // Set default toolbar group to Annotate
    instance.UI.setToolbarGroup('toolbarGroup-Annotate');

    // Set default tool on mobile devices to Pan.
    // https://apryse.atlassian.net/browse/WVR-3134
    if (isMobileDevice()) {
        instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
    }

    instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
        if (searchParams.has('file')) {
            searchParams.delete('file');
            history.replaceState(null, '', '?' + searchParams.toString());
        }
    });

    instance.Core.annotationManager.enableAnnotationNumbering();

    instance.UI.NotesPanel.enableAttachmentPreview();

    // Add the demo-specific functionality
    customizeUI(instance).then(() => {
        // Create UI controls after demo is initialized
        createUIControls(instance);
    });
});

// Function to check if the user is on a mobile device
const isMobileDevice = () => {
    return (
        /(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(
            window.navigator.userAgent
        ) ||
        /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(
            window.navigator.userAgent.substring(0, 4)
        )
    );
}

// Cleanup function for when the demo is closed or page is unloaded
const cleanup = (instance) => {
    if (typeof instance !== 'undefined' &#x26;&#x26; instance.UI) {
        // Clean up any resources if needed
        console.log('Cleaning up compare-files demo');
    }
};

// Register cleanup for page unload
window.addEventListener('beforeunload', () => cleanup());
window.addEventListener('unload', () => cleanup());

// UI section
// 
// Helper code to add controls to the viewer holding the buttons
// This code creates a container for the buttons, styles them, and adds them to the viewer
//

// File Picker Component - Reusable component for both files
const createFilePicker = (instance, fileConfig) => {
    const {
        fileNumber,
        labelText,
        getColor,
        titleVar,
        errorVar,
        setFile,
        setTitle,
        setError
    } = fileConfig;

    // Create the wrapper and container elements
    const wrapper = document.createElement('div');
    wrapper.className = 'file-wrapper';
    const container = document.createElement('div');
    container.className = 'file-container';

    // Error message
    const error = document.createElement('p');
    error.className = 'error-message';
    error.textContent = errorVar;

    // Selected file text
    const selectedFile = document.createElement('p');
    selectedFile.textContent = titleVar || 'No file selected';

    // Label for the file
    const label = document.createElement('div');
    label.className = 'file-label';

    // Create a block showing the color used to highlight differences
    const diffOptions = new instance.Core.PDFNet.PDFDoc.DiffOptions();
    const color = getColor(diffOptions);
    const colorBlock = document.createElement('span');
    colorBlock.className = 'color-block';
    colorBlock.style.backgroundColor = `rgba(${color.R}, ${color.G}, ${color.B}, ${color.A})`;

    // Text for the label
    const text = document.createElement('span');
    text.className = 'file-label-text';
    text.textContent = labelText;

    label.appendChild(colorBlock);
    label.appendChild(text);

    // Button to select the file
    const button = document.createElement('button');
    button.className = 'btn-filepicker with-margin';
    button.textContent = 'Select File';
    button.onclick = () => {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.pdf';
        input.onchange = async (event) => {
            const file = event.target.files[0];
            if (file) {
                try {
                    const doc = await instance.Core.createDocument(file);
                    setFile(doc);
                    const newTitle = file.name || `File ${fileNumber}`;
                    setTitle(newTitle);
                    selectedFile.textContent = newTitle;
                    error.textContent = ''; // Clear any previous errors
                    console.log(`File ${fileNumber} selected:`, newTitle);
                } catch (err) {
                    const errorMessage = `Error loading File ${fileNumber}: ${err.message}`;
                    setError(errorMessage);
                    error.textContent = errorMessage;
                    console.error(errorMessage);
                }
            }
        };
        input.onerror = (err) => {
            const errorMessage = `Error selecting File ${fileNumber}: ${err.message}`;
            setError(errorMessage);
            error.textContent = errorMessage;
            console.error(errorMessage);
        };
        input.click();
    };

    container.appendChild(label);
    container.appendChild(button);
    container.appendChild(error);
    container.appendChild(selectedFile);

    wrapper.appendChild(container);
    return wrapper;
};

// First File Button
const firstFile = (instance) => {
    return createFilePicker(instance, {
        fileNumber: 1,
        labelText: 'First File',
        getColor: (diffOptions) => diffOptions.getColorA(),
        titleVar: title1,
        errorVar: error1,
        setFile: (doc) => { file1 = doc; },
        setTitle: (title) => { title1 = title; },
        setError: (error) => { error1 = error; }
    });
};

// Second File Button
const secondFile = (instance) => {
    return createFilePicker(instance, {
        fileNumber: 2,
        labelText: 'Second File',
        getColor: (diffOptions) => diffOptions.getColorB(),
        titleVar: title2,
        errorVar: error2,
        setFile: (doc) => { file2 = doc; },
        setTitle: (title) => { title2 = title; },
        setError: (error) => { error2 = error; }
    });
};

// Compare button
const compareButton = (instance) => {
    const button = document.createElement('button');
    button.className = 'btn-compare bottom-margin';
    button.textContent = 'Compare Files';
    button.onclick = async () => {
        if (!file1 || !file2) {
            console.error('Files are not selected.');
            return;
        }
        await compare(instance);
    };
    return button;
};

const createUIControls = (instance) => {
    // Create a container for all controls (label, dropdown, and buttons)
    const controlsContainer = document.createElement('div');
    controlsContainer.className = 'button-container';

    controlsContainer.appendChild(firstFile(instance));
    controlsContainer.appendChild(secondFile(instance));
    controlsContainer.appendChild(compareButton(instance));

    element.insertBefore(controlsContainer, element.firstChild);
};
</code></pre>

[View the full sample on GitHub](https://github.com/ApryseSDK/webviewer-samples/tree/main/showcase-demos-playground)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.apryse.com/web/get-started/samples/showcase-demo-compare-files.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
