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.
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 andRecursiveAI.OneFont.RecursiveAI.OneDiff.Pdf: digital PDFs in; changes marked on the original pages, or a rendered redline PDF. It brings in.Documents,RecursiveAI.OnePdfandRecursiveAI.OneRender.Pdf.RecursiveAI.OneDiff.Ocr: scanned PDFs and images, read by a local OCR engine (RecursiveAI.OneOcr).RecursiveAI.OneDiff.Tool: theonediffcommand-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:
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}");
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:
dotnet add package RecursiveAI.OneDiff.Documents --version 1.0.0Save two versions of a short contract as v1.txt and v2.txt:
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:
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}");
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:
dotnet add package RecursiveAI.OneDiff.Pdf --version 1.0.0With two versions of a digital PDF, v1.pdf and v2.pdf (here, the two text files above saved as PDF):
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}");
Replace: 30 days -> 21 daysmarkup.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:
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:
dotnet add package RecursiveAI.OneDiff.Ocr --version 1.0.0using 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}");
Replace: 30 days -> 21 days, confidence 0.98, possible OCR error: FalseA 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
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
--- 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
- Overview: what OneDiff is, the packages and the happy path.
- Migrating from DiffPlex and from WmlComparer.
- Results: every measured result, including the rows that lose.