The problem with comparing two scans
Print a page, scan it, edit one word, print and scan it again. Now compare the two images.
Nothing lines up. The second scan sits two millimetres lower, the margins are off by a few pixels, the line spacing drifted. A pixel comparison dutifully reports what it sees: almost every line differs. Somewhere in that output is the one word that changed, and no reviewer is going to find it.
This is not a flaw in pixel comparison - it is answering the question it was asked. The trouble is that the question was wrong. Nobody wants to know which pixels differ between two scans. They want to know which words differ.
GroupDocs.Comparison for .NET 26.10 adds OCR-based image comparison, which asks the right question. It reads the text out of both images and compares that instead.
What is OCR-based image comparison?
OCR-based image comparison compares two images by the text recognized in them instead of by their pixels. Each image is read by a recognition engine, and the resulting word sequences are compared. Because the unit of comparison is a word rather than a pixel, text that merely shifted position between two scans counts as unchanged, and only genuinely different wording is reported as a change.
Enabling it
One property:
using var comparer = new Comparer("documents/source.png");
comparer.Add("documents/target.png");
comparer.Compare("Result/compared.png", new CompareOptions
{
Ocr = new OcrOptions { Enabled = true },
GenerateSummaryPage = false,
});
Console.WriteLine($"Changes: {comparer.GetChanges().Length}");
That is the entire integration. Enabled defaults to false, so every existing
comparison in your codebase behaves exactly as it did - even if it happens to
pass an OcrOptions instance without turning it on.
Two things are worth knowing before you run it. The package requirement is
26.10.0 or later, because the feature does not exist before that. And it needs a
.NET Core-era target: the recognition engine supplies its native runtime for
.NET Core and later only, so asking for OCR on .NET Framework throws
NotSupportedException rather than failing obscurely inside the engine.
What it does not need is a second package. The OCR engine arrives as a
transitive dependency of GroupDocs.Comparison, so dotnet add package and
nothing else.
What comes back
Running the snippet above against two deliberately misaligned renderings of the same page - one with a word swapped, a line rewritten, a line deleted, a line inserted and a heading changed in the table of contents - returns seven changes. Not seven hundred. Seven, at recognition confidence between 0.97 and 1.00:
Modified: 'alpha' -> 'gamma' (confidence 0.98)
Modified: 'is short.' -> 'has been rewritten ...' (confidence 0.99)
Modified: 'five' -> 'six' (confidence 0.99)
Modified: 'heading' -> 'rewritten' (confidence 1.00)
The whole text block sits at a different offset on each page, and none of that offset appears in the output. A word that only moved is still the same word.
Choosing how the result is drawn
OcrDisplayMode has two values, and the choice is about the audience rather
than the comparison.
SideBySide is the default. It reproduces both pages in their original layout
and marks each where it differs from the other - fonts, tables, columns and
images intact. This is what a human approver wants: the changed clause sitting
in the contract it belongs to.
Ocr = new OcrOptions
{
Enabled = true,
DisplayMode = OcrDisplayMode.SideBySide,
},
Text renders the recognized words of both documents as a single stream with
deletions and insertions highlighted, reproducing none of the original layout.
The result file came out at roughly a quarter the size of the side-by-side one
in the sample run. It suits an audit log, a digest email, or anywhere the
wording matters and the page design does not.
Ocr = new OcrOptions
{
Enabled = true,
DisplayMode = OcrDisplayMode.Text,
},
Both modes find the same edits. The mode decides how the result is drawn, not what the comparison finds - a distinction worth holding on to, because it means you can switch output format without re-validating your change detection.
Languages
OcrLanguage carries 154 values. Single alphabets (Deu, Rus, Jpn,
Spa), script-wide models (Cyrillic, Chinese, Indic, PersoArabic), and
Auto for detection when the documents are mixed or unknown.
Ocr = new OcrOptions
{
Enabled = true,
Language = OcrLanguage.Cyrillic,
},
It defaults to Latin, and that default matters operationally: Latin is the
only model inside the package. Every other value needs a model fetched from the
Aspose.OCR repository on first use and cached locally. Naming the script your
documents actually use reads their characters more accurately than a
general-purpose model does, so it is worth setting when you know it.
Reading where the changes are
A side-by-side image is enough when a person is looking at it. When the application draws its own overlay - a review UI, a redlining tool, a diff viewer
- it needs coordinates.
foreach (ChangeInfo change in comparer.GetChanges())
{
if (change is not OcrChangeInfo ocrChange)
{
continue;
}
Console.WriteLine($"{ocrChange.Type}: '{ocrChange.SourceText}'"
+ $" -> '{ocrChange.TargetText}'"
+ $" (confidence {ocrChange.Confidence:F2})");
foreach (Rectangle box in ocrChange.TargetBoxes)
{
Highlight(box.X, box.Y, box.Width, box.Height);
}
}
GetChanges() is typed as ChangeInfo[], so the OCR detail needs a cast to
OcrChangeInfo. That subtype adds SourceBoxes and TargetBoxes - one
rectangle per line of text, in pixels of that side’s own image, with float
coordinates - and Confidence from 0 to 1.
Two details save debugging time. Rectangle here is
GroupDocs.Comparison.Result.Rectangle; adding using System.Drawing; to the
file makes the name ambiguous and the build fails with CS0104. And both box
arrays are empty under DisplayMode.Text, which is consistent rather than
surprising: that mode does not reproduce the pages, so there is nothing to
report positions against.
The confidence value is more useful than it first appears. Recording it beside each change lets a pipeline route anything below a threshold to manual review instead of treating all recognized text as equally certain.
Running where there is no network
A build agent or an air-gapped host should not attempt an unannounced download of a recognition model. Two properties prevent it:
Ocr = new OcrOptions
{
Enabled = true,
Language = OcrLanguage.Latin,
DisableResourceDownloads = true,
ResourcesPath = "/opt/ocr-models",
},
DisableResourceDownloads turns a missing model into an error rather than a
fetch that hangs or is refused outright. ResourcesPath names the folder models
are read from and cached in.
Both apply to the entire process rather than to a single call, so set them once during startup rather than on each comparison - a detail easy to miss when the options object is constructed per request.
Latin text works offline with no preparation at all, since that model ships
inside the package. For any other script, copy the cached model folder from a
machine that has fetched it once and point ResourcesPath at it.
Handling the failures worth handling
Two failure modes deserve explicit treatment rather than a general catch.
The first is the platform one. If any code path can reach OCR comparison from a
.NET Framework target, it will throw NotSupportedException, and catching it
specifically lets you log something a maintainer can act on instead of a stack
trace from inside the engine.
The second is a model that is not there. With DisableResourceDownloads set, a
language whose model has not been cached fails rather than fetching - which is
the point of the setting, but only helps if the error is recognisable. The
engine reports it through the exception chain, so inspect InnerException as
well as the outer message when deciding whether a failure is about the model or
about the comparison itself.
try
{
comparer.Compare(outputPath, options);
}
catch (NotSupportedException)
{
logger.LogError("OCR comparison needs a .NET Core-era runtime.");
throw;
}
Neither case is exotic. The first shows up when a library is shared between a modern service and a legacy host; the second, the first time a deployment runs somewhere the cache was not copied.
A note on image quality
Recognition accuracy sets the ceiling on comparison accuracy, and nothing in the comparison can recover text the engine misread. The sample pair used here is clean - rendered text, high contrast, no skew - and comes back at 0.97 to 1.00. Real scans of older paper will not always do that.
This is the practical argument for recording Confidence rather than discarding
it. A change reported at 0.6 is not necessarily wrong, but it is a different
kind of claim than one at 0.99, and a pipeline that treats them identically will
eventually surface a recognition artefact as a document edit. Thresholding on
confidence, and routing what falls below it to a person, costs little and keeps
that out of the output.
When to reach for this
OCR comparison is the right tool when the documents arrived as images and the question is about their wording: scanned contracts, archived invoices, faxed amendments, screenshots of a report from two different builds. Anywhere two renderings of the same content will not align to the pixel.
It is not a replacement for comparing documents you already have as text. If both sides are DOCX or PDF with a real text layer, compare them directly - the text is already there, and recognition would only add a step and a source of error.
Start with Enabled = true and the defaults, run it against a pair of pages
where you know what changed, and check the count matches. Then add language
selection and coordinate handling as the application needs them.