Recursive AI

Getting started with OneDiff

All onediff docs

Install OneDiff 1.0.0 for .NET 10, diff and merge text, compare two documents into a verified change list, and write a redline as HTML, JSON or PDF. Pure C#, with no native code and no network access.

Install

The packages target net10.0, so you need the .NET 10 SDK.

Terminal
dotnet new console -o OneDiffStart
cd OneDiffStart
dotnet add package RecursiveAI.OneDiff --version 1.0.0

Five packages are on NuGet at 1.0.0, all on one version:

  • RecursiveAI.OneDiff: the text core. Diff, patches and three-way merge, with no dependency beyond the .NET base class library.
  • RecursiveAI.OneDiff.Documents: the comparison engine, HTML redlines and the JSON change list. It brings in the core and RecursiveAI.OneFont.
  • RecursiveAI.OneDiff.Pdf: digital PDFs in; changes marked on the original pages, or a rendered redline PDF. It brings in .Documents, RecursiveAI.OnePdf and RecursiveAI.OneRender.Pdf.
  • RecursiveAI.OneDiff.Ocr: scanned PDFs and images, read by a local OCR engine (RecursiveAI.OneOcr).
  • RecursiveAI.OneDiff.Tool: the onediff command-line tool.

Word documents are not part of 1.0. In the README’s words: “Coming in later releases: Word documents in and tracked-changes DOCX out (RecursiveAI.OneDiff.Word, in this repository and not in the first release), spreadsheets, slides, and the OneNLP value recognizer (RecursiveAI.OneDiff.Nlp). Until then a Word file is refused with a message that names the package.”

Diff, patch and merge text

Put this in Program.cs and run dotnet run:

C#
using OneDiff;
using OneDiff.Merge;
 
string oldText = "Deliver within 30 days.\nPay within 45 days.\n";
string newText = "Deliver within 21 days.\nPay within 45 days.\n";
 
// The words that changed, as Markdown
Console.WriteLine(Diff.ToMarkdownInline(Diff.Words(oldText, newText)));
 
// A unified patch, and applying it
string patch = Diff.Unified(oldText, newText, "a/terms.txt", "b/terms.txt");
Console.Write(patch);
Console.WriteLine($"applied: {Diff.ApplyUnified(oldText, patch).Text == newText}");
 
// A three-way merge: edits to different lines merge
string baseText = "one\ntwo\nthree\nfour\n";
string ours     = "ONE\ntwo\nthree\nfour\n";
string theirs   = "one\ntwo\nthree\nFOUR\n";
TextMerge merged = Diff.Merge(baseText, ours, theirs);
Console.Write(merged.Text);
Console.WriteLine($"conflicts: {merged.HasConflicts}");
Text
Deliver within <del>30</del><ins>21</ins> days.
Pay within 45 days.
 
--- a/terms.txt
+++ b/terms.txt
@@ -1,2 +1,2 @@
-Deliver within 30 days.
+Deliver within 21 days.
 Pay within 45 days.
applied: True
ONE
two
three
FOUR
conflicts: False

Edits to the same or neighboring lines are a conflict, written with conflict markers in git’s merge, diff3 or zdiff3 style. See Three-way merge and Patches.

Compare two documents

Document comparison is in .Documents:

Terminal
dotnet add package RecursiveAI.OneDiff.Documents --version 1.0.0

Save two versions of a short contract as v1.txt and v2.txt:

Text
1. Supply. The Supplier shall deliver the goods within 30 days of each order.
 
2. Payment. The Customer shall pay each invoice within 45 days of its date.

In v2.txt, change 30 days to 21 days. Then:

C#
using OneDiff.Documents;
using OneDiff.Documents.Serialization;
 
Comparison cmp = Comparison.Of("v1.txt", "v2.txt");
 
foreach (Change c in cmp.Changes.Where(c => c.IsSubstantive))
{
    Console.WriteLine($"{c.Kind}: {c.Old.Text} -> {c.New.Text}");
    if (c.OldValue is { } was && c.NewValue is { } now)
        Console.WriteLine($"  {c.Type}: {was.Canonical} -> {now.Canonical}");
}
Console.WriteLine($"complete: {cmp.Verification.Passed}");
 
// The output form follows the extension
cmp.SaveAs("redline.html");
cmp.SaveAs("changes.json");
 
Comparison again = ComparisonJson.Read(File.ReadAllBytes("changes.json"));
Console.WriteLine($"read back: {again.Changes.Length} changes, complete: {again.Verification.Passed}");
Text
Replace: 30 days -> 21 days
  Duration: 30 d -> 21 d
complete: True
read back: 1 changes, complete: True

The change is typed: a duration, with both values resolved. complete is the completeness check every comparison runs: the old document plus the changes gives the new one, and the new one minus the changes gives the old. Reading the JSON back runs the check again, so an edited file fails it. See The compare model and Change semantics.

Redline PDFs

PDF input and output are in .Pdf:

Terminal
dotnet add package RecursiveAI.OneDiff.Pdf --version 1.0.0

With two versions of a digital PDF, v1.pdf and v2.pdf (here, the two text files above saved as PDF):

C#
using OneDiff.Documents;
using OneDiff.Pdf;
 
PdfRedline.Register();   // once at startup
 
// Highlights on v2.pdf's own pages, with the old text in the pop-ups
Redline.Create("v1.pdf", "v2.pdf", "markup.pdf");
 
// A rendered redline: a new, tagged PDF laid out from the comparison
Comparison cmp = Comparison.Of("v1.pdf", "v2.pdf");
cmp.SaveAs("redline.pdf", PdfRedline.Rendered);
cmp.SaveAs("side-by-side.html", RedlineForm.HtmlSideBySide);
 
foreach (Change c in cmp.Changes.Where(c => c.IsSubstantive))
    Console.WriteLine($"{c.Kind}: {c.Old.Text} -> {c.New.Text}");
Text
Replace: 30 days -> 21 days

markup.pdf is v2.pdf with a highlight and its pop-up note; the page content is untouched. redline.pdf is a two-page tagged PDF: a summary of the changes by type, then the text with 30 struck through and 21 inserted. Every look the writers have is in Samples and redline looks.

A PDF with no text layer is refused rather than compared as empty:

Text
InvalidDataException: This PDF has no text layer, so it is probably a scan. Reference RecursiveAI.OneDiff.Ocr and call OcrRedline.Register() to read it by OCR.

Scans

Scanned PDFs and images (PNG, JPEG, TIFF) are in .Ocr, which reads them with a local OCR engine:

Terminal
dotnet add package RecursiveAI.OneDiff.Ocr --version 1.0.0
C#
using OneDiff.Documents;
using OneDiff.Ocr;
 
OcrRedline.Register();   // once at startup; reads scans and digital PDFs
 
Redline.Create("scan-v1.png", "scan-v2.png", "scan-markup.pdf");
 
foreach (Change c in Comparison.Of("scan-v1.png", "scan-v2.png").Changes)
    Console.WriteLine($"{c.Kind}: {c.Old.Text} -> {c.New.Text}, " +
                      $"confidence {c.OcrConfidence:0.00}, possible OCR error: {c.PossibleOcrError}");
Text
Replace: 30 days -> 21 days, confidence 0.98, possible OCR error: False

A change whose words were read with a confidence below 0.95 (the default threshold) is flagged as a possible recognition error; the flag never hides a change. Scans are Latin-script only. See Scans and images.

The onediff tool

Terminal
dotnet tool install --global RecursiveAI.OneDiff.Tool --version 1.0.0
onediff diff v1.txt v2.txt
onediff compare v1.pdf v2.pdf -o markup.pdf
Text
--- v1.txt
+++ v2.txt
@@ -1,3 +1,3 @@
-1. Supply. The Supplier shall deliver the goods within 30 days of each order.
+1. Supply. The Supplier shall deliver the goods within 21 days of each order.
 
 2. Payment. The Customer shall pay each invoice within 45 days of its date.

onediff diff exits 1 when the texts differ, as diff does. The other commands are patch, merge, combine (reviewers’ copies of one original) and chain (a series of versions); onediff <command> --help lists each one’s options.

Next