> 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-semantic-text-compare.md).

# Semantic Text Compare Showcase Demo Sample Code

Use the code sample to compare changes in semantic categories, such as headers, paragraphs, and numbers. The code sample is related to our showcase semantic compare demo.

{% 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="https://showcase.apryse.com/semantic-text-compare" class="button primary">Live demo</a>
{% endhint %}

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](https://showcase.apryse.com/semantic-text-compare), 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](/web/get-started/readme.md) 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-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 15, 2025
// File: index.js

// Semantic Compare section
// 
// Code to customize the WebViewer to enable semantic compare functionality,
// opens a comparison panel, and adds the option to enable synchronization
// of scroll and zoom  
//

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

let isSyncScrollZoom = false;
let canStartComparing = false;
let file1 = null;
let file2 = null;
let error1 = '';
let error2 = '';
let title1 = '';
let title2 = '';

// Collection of features and elements to disable or enable in the UI
const disabledElements = [
    'toolbarGroup-Shapes',
    'toolbarGroup-Insert',
    'toolbarGroup-Annotate',
    'toolbarGroup-FillAndSign',
    'toolbarGroup-Measure',
    'toolbarGroup-Edit',
    'toolbarGroup-Forms',
    'searchButton',
    'toggleNotesButton',
];
const enabledElements = [
    'contentEditButton',
];

// Customize the WebViewer for the Semantic Compare demo
const customizeUI = (instance) => {
    const { UI, Core } = instance;
    // Disable unnecessary elements and enable the text editing tools
    UI.disableElements(disabledElements);
    UI.enableElements(enabledElements);

    // Enable document comparison features
    UI.enableFeatures([UI.Feature.MultiViewer]);
    UI.enterMultiViewerMode();
    UI.enableFeatures(UI.Feature.ComparePages);
    UI.setToolbarGroup(UI.ToolbarGroup.VIEW);
    instance.Core.documentViewer.setToolMode(instance.Core.documentViewer.getToolModeMap()['Pan']);

    // Load the default documents into the viewers when ready
    Core.getDocumentViewers()[0]?.loadDocument(defaultDoc1);

    UI.addEventListener(UI.Events.MULTI_VIEWER_READY, () => {
        Core.getDocumentViewers()[1]?.loadDocument(defaultDoc2);
    });

    // When the documents are loaded, get references and start the semantic compare
    instance.Core.getDocumentViewers()[0].addEventListener('documentLoaded', () => {
        file1 = instance.Core.getDocumentViewers()[0].getDocument();
        title1 = file1?.getFilename() || 'File Version A';
    }, { once: true });

    instance.Core.getDocumentViewers()[1].addEventListener(
        'documentLoaded',
        () => {
            file2 = instance.Core.getDocumentViewers()[1].getDocument();
            title2 = file2?.getFilename() || 'File Version B';

            setTimeout(() => {
                startSemanticCompare(instance);
            }, 100);
        },
        { once: true }
    );

    // Add event listeners to synchronize scroll and zoom, when enabled
    const addSyncListener = (
        documentViewer,
        UI
    ) => {
        documentViewer
            ?.getViewerElement()
            ?.closest('.CompareContainer')
            ?.querySelector('control-buttons button')
            ?.addEventListener('click', () => {
                setTimeout(() => {
                    isSyncScrollZoom = UI.isMultiViewerSyncing();
                }, 0);
            });
    };

    // Add event listeners to both document viewers
    return new Promise((resolve) => {
        instance.Core.documentViewer.addEventListener('annotationsLoaded', resolve);
        const documentViewer1 = instance.Core.getDocumentViewers()[0];
        const documentViewer2 = instance.Core.getDocumentViewers()[1];

        addSyncListener(documentViewer1, UI);
        addSyncListener(documentViewer2, UI);
    });
};

// Function to start the semantic compare
const startSemanticCompare = (instance) => {
    instance.UI.openElements(['loadingModal']);

    instance.UI.startTextComparison();
    instance.UI.openElements(['comparePanel']);

    canStartComparing = true;
    updateUIControls(instance);
};

// Cleanup function for when the demo is closed or page is unloaded
const cleanup = (instance) => {
    if (typeof instance !== 'undefined' &#x26;&#x26; instance.UI) {
        instance.UI.exitMultiViewerMode();
        instance.UI.disableFeatures([instance.UI.Feature.MultiViewerMode]);
    }
};

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

import WebViewer from '@pdftron/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');

WebViewer({
    path: '/lib',
    fullAPI: true, // Required for PDFNet features
    licenseKey: licenseKey, 
}, 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)
        )
    );
}

// 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 for File Version A and B
const fileVersion = (instance, error, title, versionText, viewerId, documentSide) => {
    const fileVersionContainer = document.createElement('div');
    fileVersionContainer.style.marginBottom = '10px';

    const errorSpan = document.createElement('span');
    errorSpan.style.fontSize = '12px';
    errorSpan.style.color = 'red';
    errorSpan.textContent = error;
    fileVersionContainer.appendChild(errorSpan);

    const titleP = document.createElement('p');
    titleP.style.textOverflow = 'ellipsis';
    titleP.style.overflow = 'hidden';
    titleP.style.whiteSpace = 'nowrap';
    titleP.style.fontSize = '14px';
    titleP.innerHTML = `&#x3C;strong>File Version ${versionText}:&#x3C;/strong> ${documentSide}`;

    const titleInput = document.createElement('input');
    titleInput.placeholder = title;
    titleInput.autofocus = true;
    titleInput.style.width = '100%';
    titleInput.style.paddingLeft = '5px';
    titleInput.style.fontSize = '14px';
    titleInput.disabled = true;

    const filePicker = document.createElement('button');
    filePicker.className = 'btn-filepicker';
    filePicker.textContent = 'Select File';
    filePicker.onclick = () => {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.pdf';
        input.onchange = async (event) => {
            const file = event.target.files[0];
            if (file) {
                const doc = await instance.Core.createDocument(file);

                if (viewerId === 0) {
                    if (file1) {
                        file1.unloadResources();
                    }
                    file1 = doc;
                    instance.Core.getDocumentViewers()[0].loadDocument(doc);
                    error1 = '';
                    title1 = file.name || 'File Version A';
                    titleInput.placeholder = title1;
                } else {
                    if (file2) {
                        file2.unloadResources();
                    }
                    file2 = doc;
                    instance.Core.getDocumentViewers()[1].loadDocument(doc);
                    error2 = '';
                    title2 = file.name || 'File Version B';
                    titleInput.placeholder = title2;
                }
                canStartComparing = true;
                updateUIControls();
            }
        };
        input.onerror = (error) => {
            if (viewerId === 0) {
                error1 = 'Error loading file: ' + error.message;
            } else {
                error2 = 'Error loading file: ' + error.message;
            }
        };
        input.click();
    };

    fileVersionContainer.appendChild(errorSpan);
    fileVersionContainer.appendChild(titleP);
    fileVersionContainer.appendChild(titleInput);
    fileVersionContainer.appendChild(filePicker);

    return fileVersionContainer;
}

// Compare Button
const compare = (instance) => {
    const compareButton = document.createElement('button');
    compareButton.className = 'btn-compare';
    compareButton.disabled = true;
    compareButton.onclick = async () => {
        if (file1 &#x26;&#x26; file2) {
            // Ensure the correct documents are loaded in the viewers
            const currentDoc1 = instance.Core.getDocumentViewers()[0].getDocument();
            const currentDoc2 = instance.Core.getDocumentViewers()[1].getDocument();
            
            if (!currentDoc1 || currentDoc1 !== file1) {
                await instance.Core.getDocumentViewers()[0].loadDocument(file1);
            }

            if (!currentDoc2 || currentDoc2 !== file2) {
                await instance.Core.getDocumentViewers()[1].loadDocument(file2);
            }

            if (isSyncScrollZoom) {
                instance.UI.enableMultiViewerSync();
            }

            startSemanticCompare(instance);
            canStartComparing = false;
            updateUIControls(instance);
        } else {
            alert('Please ensure both documents are loaded before comparing.');
        }
    };
    compareButton.textContent = 'Compare';

    return compareButton;
}

// Sync Scroll and Zoom Checkbox
const syncScrollZoom = (instance) => {
    const syncScrollZoomContainer = document.createElement('div');
    syncScrollZoomContainer.style.display = 'flex';
    syncScrollZoomContainer.style.alignItems = 'center';

    const syncScrollZoomCheckbox = document.createElement('input');
    syncScrollZoomCheckbox.type = 'checkbox';
    syncScrollZoomCheckbox.className = 'checkbox-syncScrollZoom';
    syncScrollZoomCheckbox.checked = isSyncScrollZoom;
    syncScrollZoomCheckbox.onchange = (e) => {
        isSyncScrollZoom = e.target.checked;
        if (isSyncScrollZoom) {
            instance.UI.enableMultiViewerSync();
        } else {
            instance.UI.disableMultiViewerSync();
        }
    };

    const syncScrollZoomLabel = document.createElement('label');
    syncScrollZoomLabel.className = 'label-syncScrollZoom';
    syncScrollZoomLabel.style.marginLeft = '2';
    syncScrollZoomLabel.style.fontSize = 'xsmall';
    syncScrollZoomLabel.textContent = 'Synchronize Scroll and Zoom';

    syncScrollZoomContainer.appendChild(syncScrollZoomCheckbox);
    syncScrollZoomContainer.appendChild(syncScrollZoomLabel);

    return syncScrollZoomContainer;
}

// Reset Default Documents Button
const resetDefaultDocuments = (instance) => {
    const resetButton = document.createElement('button');
    resetButton.className = 'btn-reset';
    resetButton.textContent = 'Reset Default Documents';
    resetButton.onclick = async () => {
        instance.Core.getDocumentViewers()[0].loadDocument(defaultDoc1);
        instance.Core.getDocumentViewers()[1].loadDocument(defaultDoc2);
        
        // Wait for documents to load before getting references
        instance.Core.getDocumentViewers()[0].addEventListener('documentLoaded', () => {
            file1 = instance.Core.getDocumentViewers()[0].getDocument();
            title1 = file1?.getFilename() || 'File Version A';
        }, { once: true });
        
        instance.Core.getDocumentViewers()[1].addEventListener('documentLoaded', () => {
            file2 = instance.Core.getDocumentViewers()[1].getDocument();
            title2 = file2?.getFilename() || 'File Version B';
        }, { once: true });
        
        error1 = '';
        error2 = '';
        canStartComparing = true;
        updateUIControls();
    };

    return resetButton;

}

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

    // File Version $A [left]
    controlsContainer.appendChild(fileVersion(instance, error1, title1, 'A', 0, '{left}'));

    // File Version $B [right]
    controlsContainer.appendChild(fileVersion(instance, error2, title2, 'B', 1, '{right}'));

    // Compare Button
    controlsContainer.appendChild(compare(instance));

    // Sync Scroll and Zoom Checkbox
    controlsContainer.appendChild(syncScrollZoom(instance));

    // Reset Default Documents Button
    controlsContainer.appendChild(resetDefaultDocuments(instance));

    element.insertBefore(controlsContainer, element.firstChild);
};

const updateUIControls = () => {
    const compareButton = document.querySelector('.btn-compare');
    if (compareButton) {
        compareButton.disabled = !canStartComparing;
    }
}
</code></pre>


---

# 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-semantic-text-compare.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.
