> 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/diff.md).

# Compare documents by image, pixels

Compare documents as images directly in a web browser using JavaScript with Apryse WebViewer.

{% 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>
{% endhint %}

This Javascript sample shows how to render three synchronized panels where the middle panel shows pixel differences between the two documents. [Try out the diff demo now](https://sdk.apryse.com/samples/web/samples/advanced/diff/).

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.

Learn more about our [Web SDK](/web/get-started/guides.md) and [all file compare options](/web/compare-files/compare.md).

### **Implementation steps**

To use compare PDFs side-by-side in JavaScript with WebViewer:

Step 1: Follow [get started in your preferred web stack for WebViewer](/web/get-started/readme.md) Step 2: Add the sample code provided in this guide

This full sample is one of many in the samples folder included with the [manual download of WebViewer.](/web/get-started/manually.md#1-download-webviewer)

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">(async exports => {
  const Core = exports.Core;
  const PDFNet = exports.Core.PDFNet;
  Core.setWorkerPath('../../../lib/core');
  Core.enableFullPDF();
  await PDFNet.initialize();

  const parentDoc = window.parent.window.document;

  // Replace with your license key here as it needs to be passed when instantiating the worker transport promise
  const licenseKey = '<code class="expression">visitor.claims.wvKey || "YOUR_LICENSE_KEY"</code>';

  // Shared worker
  const workerTransportPromise = Core.initPDFWorkerTransports('pdf', {}, licenseKey);

  const containers = ['viewer1', 'viewer2'];
  const instances = [];
  const initialFiles = ['/samples/files/semantic_test_doc_1.pdf', '/samples/files/semantic_test_doc_2.pdf'];

  let scrollTimeout = null;
  let originalScroller = null;

  const initializeViewer = containerName => {
    return new Promise(resolve => {
      WebViewer(
        {
          path: '../../../lib',
          // Use shared worker
          workerTransportPromise,
          fullAPI: true,
        },
        document.getElementById(containerName)
      ).then(instance => {
        instance.Core.syncNamespaces({ PDFNet });

        instance.Core.documentViewer.addEventListener('documentLoaded', () => {
          const scrollView = instance.Core.documentViewer.getScrollViewElement();
          scrollView.onscroll = function() {
            if (!originalScroller || originalScroller === scrollView) {
              originalScroller = scrollView;
              const leftPercentage = scrollView.scrollLeft / scrollView.scrollWidth;
              const topPercentage = scrollView.scrollTop / scrollView.scrollHeight;
              syncDocumentContainerScrolls(leftPercentage, topPercentage);
              clearTimeout(scrollTimeout);
              scrollTimeout = setTimeout(() => {
                originalScroller = null;
              }, 50);
            }
          };

          // Update zoom value of the WebViewer instances
          instance.Core.documentViewer.addEventListener('zoomUpdated', zoom => {
            // zoom events will also trigger a scroll event
            // set the original scroll to be the same panel that first triggers the zoom event
            // so that scroll events are handled properly and in the correct order
            // some browsers such as Chrome do not respect the scroll event ordering correctly
            if (!originalScroller) {
              originalScroller = scrollView;
              clearTimeout(scrollTimeout);
              scrollTimeout = setTimeout(() => {
                originalScroller = null;
              }, 50);
            }
            syncZoom(zoom);
          });

          instance.Core.documentViewer.addEventListener('rotationUpdated', rotation => {
            syncRotation(rotation);
          });
        });

        instances.push(instance);
        resolve(instance);
      });
    });
  };

  const main = async () => {
    const initTasks = containers.map(containerName => initializeViewer(containerName));

    const docTasks = initialFiles.map(initialFile => PDFNet.PDFDoc.createFromURL(initialFile));

    await Promise.all([...initTasks, ...docTasks]);
    const docs = await Promise.all(docTasks);

    await compareDoc(docs[0], docs[1]);

    parentDoc.getElementById('fileUpload1').disabled = false;
    parentDoc.getElementById('fileUpload2').disabled = false;
    const compareButton = parentDoc.getElementById('compareButton');
    compareButton.addEventListener('click', async () => {
      const doc1 = uploadedDoc[0];
      const doc2 = uploadedDoc[1];
      disableCompareButton();
      await compareDoc(doc1, doc2);
    });
  };

  main();

  const uploadedDoc = [null, null];
  const recentDiffs = [];

  const syncDocumentContainerScrolls = (scrollLeftPercentage, scrollTopPercentage) => {
    instances.forEach(instance => {
      const scrollView = instance.Core.documentViewer.getScrollViewElement();
      if (!scrollView) {
        return;
      }
      const currentLeftPosition = scrollView.scrollLeft / scrollView.scrollWidth;
      const currentTopPosition = scrollView.scrollTop / scrollView.scrollHeight;
      if (currentLeftPosition !== scrollLeftPercentage) {
        scrollView.scrollLeft = scrollView.scrollWidth * scrollLeftPercentage;
      }
      if (currentTopPosition !== scrollTopPercentage) {
        scrollView.scrollTop = scrollView.scrollHeight * scrollTopPercentage;
      }
    });
  };

  const syncZoom = zoom => {
    instances.forEach(instance => {
      if (instance.UI.getZoomLevel() !== zoom) {
        instance.UI.setZoomLevel(zoom);
      }
    });
  };

  const syncRotation = rotation => {
    instances.forEach(instance => {
      const documentViewer = instance.Core.documentViewer;
      if (documentViewer.getRotation() !== rotation) {
        documentViewer.setRotation(rotation);
      }
    });
  };

  const compareDoc = async (doc1, doc2) => {
    const leftPageCount = await doc1.getPageCount();
    const rightPageCount = await doc2.getPageCount();

    const leftDoc = await PDFNet.PDFDoc.create();
    leftDoc.lock();

    const rightDoc = await PDFNet.PDFDoc.create();
    rightDoc.lock();

    const newDoc = await PDFNet.PDFDoc.create();
    newDoc.lock();

    await newDoc.appendTextDiffDoc(doc1, doc2);

    const totalPageCount = await newDoc.getPageCount();
    const isLeftDocLarger = leftPageCount > rightPageCount;
    const smallerCount = leftPageCount > rightPageCount ? leftPageCount : rightPageCount;

    for (let i = 0; i &#x3C; totalPageCount; i++) {
      const page = await newDoc.getPage(i + 1);
      if (i === smallerCount) {
        if (isLeftDocLarger) {
          await leftDoc.pagePushBack(page);
        } else {
          await rightDoc.pagePushBack(page);
        }
        continue;
      }
      if (i % 2 === 0) {
        await leftDoc.pagePushBack(page);
      } else {
        await rightDoc.pagePushBack(page);
      }
    }

    await newDoc.unlock();
    await leftDoc.unlock();
    await rightDoc.unlock();

    instances[0].UI.loadDocument(leftDoc);
    instances[1].UI.loadDocument(rightDoc);

    recentDiffs.push([...uploadedDoc]);

    // Skip default comparison
    if (recentDiffs.length === 1) {
      return;
    }

    const recentElement = parentDoc.getElementById('recentFiles');
    const comparisonElement = document.createElement('button');
    comparisonElement.innerText = `Compare ${doc1.fileName} (A) &#x26; ${doc2.fileName} (B)`;
    comparisonElement.classList.add('link');
    comparisonElement.onclick = onClickRecentLink.bind({ idx: recentDiffs.length - 1 });
    recentElement.appendChild(comparisonElement);
  };

  const enableCompareButton = async () => {
    const compareButton = parentDoc.getElementById('compareButton');

    if (!compareButton.classList.contains('disabled')) {
      return;
    }

    compareButton.classList.remove('disabled');
  };

  const disableCompareButton = async () => {
    const compareButton = parentDoc.getElementById('compareButton');

    if (compareButton.classList.contains('disabled')) {
      return;
    }

    compareButton.classList.add('disabled');
  };

  const getPDFDocFromUpload = async (file, fileIndex) => {
    const newDoc = await Core.createDocument(file, {});
    uploadedDoc[fileIndex] = await newDoc.getPDFDoc();
    uploadedDoc[fileIndex].fileName = file.name;
    if (uploadedDoc[1] !== null &#x26;&#x26; uploadedDoc[0] !== null) {
      enableCompareButton();
    }
  };

  parentDoc.getElementById('fileUpload1').addEventListener('change', e => {
    getPDFDocFromUpload(e.target.files[0], 0);
  });

  parentDoc.getElementById('fileUpload2').addEventListener('change', e => {
    getPDFDocFromUpload(e.target.files[0], 1);
  });

  const onClickRecentLink = async function() {
    await compareDoc(recentDiffs[this.idx][0], recentDiffs[this.idx][1]);
  };
})(window);
// eslint-disable-next-line spaced-comment
//# sourceURL=config.js
</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/diff.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.
