Programmatically Get Differences
23 Sep 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 PdfViewerComponent, 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 React PDF Viewer installed
-
PdfViewerComponentavailable - Two PDF documents loaded for comparison
- Semantic text comparison feature enabled
Steps
Step 1: Import required components
import { PdfViewerComponent, Inject, Toolbar, Magnification, Navigation, Annotation, LinkAnnotation, BookmarkView, ThumbnailView, Print, TextSelection, TextSearch, FormFields, FormDesigner, PageOrganizer } from '@syncfusion/ej2-react-pdfviewer';
import React, { useRef, useState } from 'react';Step 2: Create the dual viewer comparison setup
Set up two side-by-side PDF viewers with semantic text comparison:
export default function App() {
const viewer1Ref = useRef(null);
const viewer2Ref = useRef(null);
const [loadedCount, setLoadedCount] = useState(0);
const [viewersLoaded, setViewersLoaded] = useState(false);
const [synchronizationEnabled, setSynchronizationEnabled] = useState(true);
const [highlightsEnabled, setHighlightsEnabled] = useState(true);
const handleDocumentLoad = () => {
setLoadedCount((prev) => {
const newCount = prev + 1;
if (newCount === 2 && viewer1Ref.current && viewer2Ref.current) {
setViewersLoaded(true);
viewer1Ref.current.syncViewers(viewer2Ref.current, synchronizationEnabled);
}
return newCount;
});
};
return (
<div style={{ height: '100%', width: '100%' }}>
{/* PDF Viewers Container */}
<div style={{
display: 'flex',
height: '600px',
gap: 0
}}>
{/* Viewer 1 - Original Document */}
<div style={{ width: '50%', height: '100%', borderRight: '1px solid #ccc' }}>
<PdfViewerComponent
ref={viewer1Ref}
id="pdfViewer1"
documentPath="https://cdn.syncfusion.com/content/pdf/original-document.pdf"
resourceUrl="https://cdn.syncfusion.com/ej2/34.2.4/dist/ej2-pdfviewer-lib"
documentLoad={handleDocumentLoad}
style={{ height: '100%', width: '100%' }}
>
<Inject services={[
Toolbar,
Magnification,
Navigation,
Annotation,
LinkAnnotation,
BookmarkView,
ThumbnailView,
Print,
TextSelection,
TextSearch,
FormFields,
FormDesigner,
PageOrganizer
]} />
</PdfViewerComponent>
</div>
{/* Viewer 2 - Modified Document */}
<div style={{ width: '50%', height: '100%' }}>
<PdfViewerComponent
ref={viewer2Ref}
id="pdfViewer2"
documentPath="https://cdn.syncfusion.com/content/pdf/modified-document.pdf"
resourceUrl="https://cdn.syncfusion.com/ej2/34.2.4/dist/ej2-pdfviewer-lib"
documentLoad={handleDocumentLoad}
style={{ height: '100%', width: '100%' }}
>
<Inject services={[
Toolbar,
Magnification,
Navigation,
Annotation,
LinkAnnotation,
BookmarkView,
ThumbnailView,
Print,
TextSelection,
TextSearch,
FormFields,
FormDesigner,
PageOrganizer
]} />
</PdfViewerComponent>
</div>
</div>
</div>
);
}Step 3: Perform semantic text comparison
Compare the documents programmatically and access differences:
const handleCompare = async () => {
if (!viewersLoaded || !viewer1Ref.current || !viewer2Ref.current) {
return;
}
const options = {
beforeColor: '#FF0000', // Red for original
afterColor: '#00FF00', // Green for modified
beforeColorOpacity: 0.4,
afterColorOpacity: 0.4,
enableHighlights: highlightsEnabled,
};
try {
const result = await viewer1Ref.current.semanticTextCompare(viewer2Ref.current, 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) => {
pageAnnotations.differenceAnnotations?.forEach((diff) => {
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:
const generateReport = async () => {
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 totalTextDiffCount = result?.totalTextDiffCount || 0;
let addedCount = 0;
let deletedCount = 0;
let modifiedCount = 0;
const byPage = {};
// Process all annotations
originalAnnotations.forEach((pageAnnotations) => {
const pageNum = pageAnnotations.pageNumber;
if (!byPage[pageNum]) {
byPage[pageNum] = { deleted: 0, added: 0, modified: 0 };
}
pageAnnotations.differenceAnnotations?.forEach((diff) => {
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) => {
const pageNum = pageAnnotations.pageNumber;
if (!byPage[pageNum]) {
byPage[pageNum] = { deleted: 0, added: 0, modified: 0 };
}
pageAnnotations.differenceAnnotations?.forEach((diff) => {
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 = {
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);
}
};Step 5: Add control buttons and synchronization
Manage synchronization, highlights, and comparison controls:
const handleToggleSync = () => {
const newSyncState = !synchronizationEnabled;
setSynchronizationEnabled(newSyncState);
if (viewer1Ref.current && viewer2Ref.current) {
viewer1Ref.current.syncViewers(viewer2Ref.current, newSyncState);
}
};
const handleToggleHighlights = async () => {
const newHighlightsState = !highlightsEnabled;
setHighlightsEnabled(newHighlightsState);
// Re-apply comparison with updated highlight state
if (viewersLoaded && viewer1Ref.current && viewer2Ref.current) {
const options = {
beforeColor: '#FF0000',
afterColor: '#00FF00',
beforeColorOpacity: 0.4,
afterColorOpacity: 0.4,
enableHighlights: newHighlightsState,
};
try {
// Clear previous comparison
viewer1Ref.current.removeSemanticTextCompare?.(viewer2Ref.current);
// Apply new comparison with updated highlights state
const result = await viewer1Ref.current.semanticTextCompare(viewer2Ref.current, options);
console.log('Highlights updated:', result);
} catch (error) {
console.error('Error updating highlights:', error);
}
}
};
const handleClearAnnotations = () => {
if (viewer1Ref.current && viewer2Ref.current) {
viewer1Ref.current.removeSemanticTextCompare?.(viewer2Ref.current);
}
};
// Control Buttons
const ControlPanel = () => (
<div style={{
display: 'flex',
gap: '8px',
padding: '12px',
backgroundColor: '#f5f5f5',
borderBottom: '1px solid #ddd'
}}>
<button onClick={handleCompare} style={{
padding: '8px 16px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}>
Compare Documents
</button>
<button onClick={handleToggleHighlights} style={{
padding: '8px 16px',
backgroundColor: '#28a745',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}>
{highlightsEnabled ? 'Disable Highlights' : 'Enable Highlights'}
</button>
<button onClick={handleToggleSync} style={{
padding: '8px 16px',
backgroundColor: '#ffc107',
color: 'black',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}>
{synchronizationEnabled ? 'Disable Sync' : 'Enable Sync'}
</button>
<button onClick={handleClearAnnotations} style={{
padding: '8px 16px',
backgroundColor: '#dc3545',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}>
Clear Annotations
</button>
</div>
);Viewing differences

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');Group differences by page
const groupDifferencesByPage = async () => {
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 grouped = {};
// Group original document differences by page
originalAnnotations.forEach((pageAnnotations) => {
const pageNum = pageAnnotations.pageNumber;
if (!grouped[pageNum]) {
grouped[pageNum] = { original: [], modified: [] };
}
pageAnnotations.differenceAnnotations?.forEach((diff) => {
grouped[pageNum].original.push({
type: diff.textDiffType,
text: diff.textDiffData,
bounds: diff.annotation?.bounds,
color: diff.annotation?.color
});
});
});
// Group modified document differences by page
modifiedAnnotations.forEach((pageAnnotations) => {
const pageNum = pageAnnotations.pageNumber;
if (!grouped[pageNum]) {
grouped[pageNum] = { original: [], modified: [] };
}
pageAnnotations.differenceAnnotations?.forEach((diff) => {
grouped[pageNum].modified.push({
type: diff.textDiffType,
text: diff.textDiffData,
bounds: diff.annotation?.bounds,
color: diff.annotation?.color
});
});
});
console.log('Differences grouped by page:', grouped);
return grouped;
} catch (error) {
console.error('Error grouping differences:', error);
return {};
}
};