Programmatically Get Differences in TypeScript PDF Viewer

1 Oct 202624 minutes to read

The semantic text comparison feature provides programmatic access to all differences found between two PDF documents. Using the semanticTextCompare() method on PdfViewer, you can retrieve structured difference data for custom processing, reporting, or integration with other workflows.

Overview

The comparison provides:

  • Async comparison API - Use semanticTextCompare() to compare documents programmatically
  • Difference array access - Get all detected differences with type and content
  • Structured data - Each difference contains text, type, location, and page information
  • Categorized results - Differences grouped by type (Added, Deleted, Modified)
  • Custom processing - Export, filter, or analyze differences programmatically

Prerequisites

  • Syncfusion TypeScript PDF Viewer installed
  • PdfViewer class available
  • Two PDF documents loaded for comparison
  • Semantic text comparison feature enabled

Steps

Step 1: Import required components

import { PdfViewer, Toolbar, Magnification, Navigation, LinkAnnotation, BookmarkView, ThumbnailView, Print, TextSelection, TextSearch, Annotation, FormDesigner, FormFields, PageOrganizer } from '@syncfusion/ej2-pdfviewer';

Step 2: Create the dual viewer comparison setup

Set up two side-by-side PDF viewers with semantic text comparison:

interface AppState {
    viewer1: PdfViewer | null;
    viewer2: PdfViewer | null;
    loadedCount: number;
    viewersLoaded: boolean;
}

const appState: AppState = {
    viewer1: null,
    viewer2: null,
    loadedCount: 0,
    viewersLoaded: false
};

const RESOURCE_URL = 'https://cdn.syncfusion.com/ej2/31.2.2/dist/ej2-pdfviewer-lib';
const ORIGINAL_PDF = 'https://cdn.syncfusion.com/content/pdf/original-document.pdf';
const MODIFIED_PDF = 'https://cdn.syncfusion.com/content/pdf/modified-document.pdf';

PdfViewer.Inject(Toolbar, Magnification, Navigation, LinkAnnotation, BookmarkView, ThumbnailView, Print, TextSelection, TextSearch, Annotation, FormDesigner, FormFields, PageOrganizer);

function handleDocumentLoad(): void {
    appState.loadedCount++;
    if (appState.loadedCount === 2 && appState.viewer1 && appState.viewer2) {
        appState.viewersLoaded = true;
        appState.viewer1.syncViewers(appState.viewer2, true);
    }
}

document.addEventListener('DOMContentLoaded', () => {
    appState.viewer1 = new PdfViewer({
        documentPath: ORIGINAL_PDF,
        resourceUrl: RESOURCE_URL,
        documentLoad: handleDocumentLoad
    });
    appState.viewer1.appendTo('#pdfViewer1');

    appState.viewer2 = new PdfViewer({
        documentPath: MODIFIED_PDF,
        resourceUrl: RESOURCE_URL,
        documentLoad: handleDocumentLoad
    });
    appState.viewer2.appendTo('#pdfViewer2');
});

Step 3: Perform semantic text comparison

Compare the documents programmatically and access differences:

async function handleCompare(): Promise<void> {
  if (!appState.viewersLoaded || !appState.viewer1 || !appState.viewer2) {
    console.warn('Viewers not ready');
    return;
  }

  const options: any = {
    beforeColor: '#FF0000',      // Red for original
    afterColor: '#00FF00',       // Green for modified
    beforeColorOpacity: 0.4,
    afterColorOpacity: 0.4,
    enableHighlights: true
  };

  try {
    const result = await appState.viewer1.semanticTextCompare(appState.viewer2, options);
    console.log('Full Comparison Result:', result);

    // Access annotations from both documents
    const originalAnnotations = result?.originalDocumentAnnotations || [];
    const modifiedAnnotations = result?.modifiedDocumentAnnotations || [];
    const totalTextDiffCount = result?.totalTextDiffCount || 0;

    console.log('Total Text Differences:', totalTextDiffCount);

    // Extract and categorize all differences
    let addedCount = 0;
    let deletedCount = 0;
    let modifiedCount = 0;

    // Process original document annotations (deletions and modifications)
    originalAnnotations.forEach((pageAnnotations: any) => {
      pageAnnotations.differenceAnnotations?.forEach((diff: any) => {
        const type = diff.textDiffType;
        if (type === 'deleted') deletedCount++;
        if (type === 'modified') modifiedCount++;
        if (type === 'added') addedCount++;
      });
    });

    console.log('Added:', addedCount);
    console.log('Deleted:', deletedCount);
    console.log('Modified:', modifiedCount);
  } catch (error) {
    console.error('Error during comparison:', error);
  }
}

Step 4: Generate comparison report

Generate a detailed report with differences grouped by page:

async function generateReport(): Promise<void> {
  if (!appState.viewer1 || !appState.viewer2) {
    console.warn('Viewers not ready');
    return;
  }

  const options: any = {
    beforeColor: '#FF0000',
    afterColor: '#00FF00',
    beforeColorOpacity: 0.4,
    afterColorOpacity: 0.4,
    enableHighlights: true
  };

  try {
    const result = await appState.viewer1.semanticTextCompare(appState.viewer2, options);

    const originalAnnotations = result?.originalDocumentAnnotations || [];
    const modifiedAnnotations = result?.modifiedDocumentAnnotations || [];
    const totalTextDiffCount = result?.totalTextDiffCount || 0;

    let addedCount = 0;
    let deletedCount = 0;
    let modifiedCount = 0;
    const byPage: any = {};

    // Process all annotations
    originalAnnotations.forEach((pageAnnotations: any) => {
      const pageNum = pageAnnotations.pageNumber;
      if (!byPage[pageNum]) {
        byPage[pageNum] = { deleted: 0, added: 0, modified: 0 };
      }

      pageAnnotations.differenceAnnotations?.forEach((diff: any) => {
        const type = diff.textDiffType;
        if (type === 'deleted') {
          deletedCount++;
          byPage[pageNum].deleted++;
        } else if (type === 'added') {
          addedCount++;
          byPage[pageNum].added++;
        } else if (type === 'modified') {
          modifiedCount++;
          byPage[pageNum].modified++;
        }
      });
    });

    modifiedAnnotations.forEach((pageAnnotations: any) => {
      const pageNum = pageAnnotations.pageNumber;
      if (!byPage[pageNum]) {
        byPage[pageNum] = { deleted: 0, added: 0, modified: 0 };
      }

      pageAnnotations.differenceAnnotations?.forEach((diff: any) => {
        const type = diff.textDiffType;
        if (type === 'deleted') {
          deletedCount++;
          byPage[pageNum].deleted++;
        } else if (type === 'added') {
          addedCount++;
          byPage[pageNum].added++;
        } else if (type === 'modified') {
          modifiedCount++;
          byPage[pageNum].modified++;
        }
      });
    });

    const report: any = {
      totalDifferences: totalTextDiffCount,
      summary: {
        added: addedCount,
        deleted: deletedCount,
        modified: modifiedCount
      },
      byPage: byPage
    };

    console.log('Comparison Report:', report);
    return report;
  } catch (error) {
    console.error('Error generating report:', error);
  }
}

// Export functions to global scope
declare global {
    interface Window {
        handleCompare: typeof handleCompare;
        generateReport: typeof generateReport;
    }
}

window.handleCompare = handleCompare;
window.generateReport = generateReport;

Step 5: HTML Structure

Create the HTML structure with two viewers:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Semantic Text Comparison</title>
    <link href="https://cdn.syncfusion.com/ej2/31.2.2/material.css" rel="stylesheet" />
</head>
<body>
    <div style="display: flex; height: 100vh; gap: 0;">
        <div id="pdfViewer1" style="width: 50%; height: 100%; border-right: 1px solid #ccc;"></div>
        <div id="pdfViewer2" style="width: 50%; height: 100%;"></div>
    </div>
    <button onclick="handleCompare()" style="position: fixed; bottom: 20px; left: 20px; padding: 10px 20px;">Compare</button>
    <button onclick="generateReport()" style="position: fixed; bottom: 20px; left: 150px; padding: 10px 20px;">Generate Report</button>
    <script src="https://cdn.syncfusion.com/ej2/31.2.2/dist/ej2.min.js"></script>
    <script src="app.js"></script>
</body>
</html>

Step 6: Add control buttons and synchronization

Manage synchronization, highlights, and comparison controls:

function handleToggleSync(): void {
  const newSyncState = !appState.viewersLoaded;
  if (appState.viewer1 && appState.viewer2) {
    appState.viewer1.syncViewers(appState.viewer2, newSyncState);
  }
}

function handleToggleHighlights(): void {
  if (appState.viewersLoaded && appState.viewer1 && appState.viewer2) {
    const options: any = {
      beforeColor: '#FF0000',
      afterColor: '#00FF00',
      beforeColorOpacity: 0.4,
      afterColorOpacity: 0.4,
      enableHighlights: true
    };

    try {
      appState.viewer1.removeSemanticTextCompare?.(appState.viewer2);
      appState.viewer1.semanticTextCompare(appState.viewer2, options);
      console.log('Highlights updated');
    } catch (error) {
      console.error('Error updating highlights:', error);
    }
  }
}

function handleClearAnnotations(): void {
  if (appState.viewer1 && appState.viewer2) {
    (appState.viewer1 as any).removeSemanticTextCompare?.(appState.viewer2);
  }
}

Viewing differences

Differences panel shows all detected changes

The differences panel on the right displays all detected differences categorized by type:

  • Deleted - Text removed from the original document (shown in red in the document)
  • Replaced - Text that was modified or changed
  • Each item shows the page number and change details

Difference object structure

Each difference object contains:

Property Type Description
type string Type of difference: ‘Added’, ‘Deleted’, or ‘Modified’
text string The actual text content of the difference
pageNumber number Page number where difference is located (1-based)
bounds object Position and size of the difference
bounds.x number X coordinate of the difference
bounds.y number Y coordinate of the difference
bounds.width number Width of the text bounding box
bounds.height number Height of the text bounding box

Common use cases

Filter differences by type

const getDifferencesByType = async (type) => {
    if (!viewer1Ref.current || !viewer2Ref.current) return [];

    const options = {
        beforeColor: '#FF0000',
        afterColor: '#00FF00',
        beforeColorOpacity: 0.4,
        afterColorOpacity: 0.4,
        enableHighlights: true,
    };

    try {
        const result = await viewer1Ref.current.semanticTextCompare(viewer2Ref.current, options);

        const originalAnnotations = result?.originalDocumentAnnotations || [];
        const modifiedAnnotations = result?.modifiedDocumentAnnotations || [];
        const differences = [];

        // Extract differences by type from original annotations
        originalAnnotations.forEach((pageAnnotations) => {
          pageAnnotations.differenceAnnotations?.forEach((diff) => {
            if (diff.textDiffType === type.toLowerCase()) {
              differences.push({
                pageNumber: pageAnnotations.pageNumber,
                type: diff.textDiffType,
                text: diff.textDiffData,
                bounds: diff.annotation?.bounds,
                color: diff.annotation?.color
              });
            }
          });
        });

        // Extract differences by type from modified annotations
        modifiedAnnotations.forEach((pageAnnotations) => {
          pageAnnotations.differenceAnnotations?.forEach((diff) => {
            if (diff.textDiffType === type.toLowerCase()) {
              const exists = differences.find(d =>
                d.pageNumber === pageAnnotations.pageNumber &&
                d.text === diff.textDiffData
              );
              if (!exists) {
                differences.push({
                  pageNumber: pageAnnotations.pageNumber,
                  type: diff.textDiffType,
                  text: diff.textDiffData,
                  bounds: diff.annotation?.bounds,
                  color: diff.annotation?.color
                });
              }
            }
          });
        });

        console.log(`${type} differences (${differences.length}):`, differences);
        return differences;
    } catch (error) {
        console.error('Error filtering differences:', error);
        return [];
    }
};

// Usage
const addedDifferences = await getDifferencesByType('added');
const deletedDifferences = await getDifferencesByType('deleted');
const modifiedDifferences = await getDifferencesByType('modified');

Filter differences by type

async function getDifferencesByType(type: string): Promise<any[]> {
    if (!appState.viewer1 || !appState.viewer2) return [];

    const options: any = {
        beforeColor: '#FF0000',
        afterColor: '#00FF00',
        beforeColorOpacity: 0.4,
        afterColorOpacity: 0.4,
        enableHighlights: true
    };

    try {
        const result = await appState.viewer1.semanticTextCompare(appState.viewer2, options);
        const originalAnnotations = result?.originalDocumentAnnotations || [];
        const modifiedAnnotations = result?.modifiedDocumentAnnotations || [];
        const differences: any[] = [];

        originalAnnotations.forEach((pageAnnotations: any) => {
          pageAnnotations.differenceAnnotations?.forEach((diff: any) => {
            if (diff.textDiffType === type.toLowerCase()) {
              differences.push({
                pageNumber: pageAnnotations.pageNumber,
                type: diff.textDiffType,
                text: diff.textDiffData,
                bounds: diff.annotation?.bounds,
                color: diff.annotation?.color
              });
            }
          });
        });

        modifiedAnnotations.forEach((pageAnnotations: any) => {
          pageAnnotations.differenceAnnotations?.forEach((diff: any) => {
            if (diff.textDiffType === type.toLowerCase()) {
              const exists = differences.find(d =>
                d.pageNumber === pageAnnotations.pageNumber &&
                d.text === diff.textDiffData
              );
              if (!exists) {
                differences.push({
                  pageNumber: pageAnnotations.pageNumber,
                  type: diff.textDiffType,
                  text: diff.textDiffData,
                  bounds: diff.annotation?.bounds,
                  color: diff.annotation?.color
                });
              }
            }
          });
        });

        console.log(`${type} differences (${differences.length}):`, differences);
        return differences;
    } catch (error) {
        console.error('Error filtering differences:', error);
        return [];
    }
}

// Usage
const addedDifferences = await getDifferencesByType('added');
const deletedDifferences = await getDifferencesByType('deleted');
const modifiedDifferences = await getDifferencesByType('modified');

View Sample in GitHub.