Recursive AI

Invoices as fields

All oneocr recipes

Read an invoice image into typed fields with their EN 16931 business-term ids, line items included, and see which words and box each value came from.

Extraction needs the extraction model, which ships in its own package:

Terminal
dotnet add package RecursiveAI.OneOcr --version 1.0.0
dotnet add package RecursiveAI.OneOcr.Models.Documents --version 1.0.0

A small layout-aware model tags the words of the page, and deterministic code assembles the fields from the tagged words, so a value is always text that is on the page. The image is invoice.png from the sample images:

C#
using OneImage;
using OneOcr;
using OneOcr.Documents;
using OneOcr.Models.Documents;
 
OcrEngine engine = OcrEngine.LoadBundled();
DocumentModels.Configure();                      // the extraction model from RecursiveAI.OneOcr.Models.Documents, once
 
Invoice invoice = Image.Load("invoice.png").ExtractInvoice(engine);
 
Console.WriteLine($"{invoice.Number?.Value}, issued {invoice.IssueDate?.Value}, total {invoice.Total?.Value}");
Console.WriteLine($"{invoice.SellerName?.Value} -> {invoice.BuyerName?.Value}, IBAN {invoice.Iban?.Value}");
foreach (InvoiceLine line in invoice.Lines)
    Console.WriteLine($"  {line.Description?.Text}: {line.Quantity?.Value} {line.Unit?.Text} x {line.UnitPrice?.Value} = {line.Amount?.Value}");
 
ExtractedField total = invoice.Extraction.Get("BT-112")!;
Console.WriteLine($"BT-112 \"{total.Text}\" -> {total.Number}, words [{string.Join(", ", total.WordIndices)}], " +
                  $"box ({total.Box.Left:F0}, {total.Box.Top:F0}), confidence {total.Confidence:F2}");
Console.WriteLine($"Due date: {invoice.DueDate?.Value ?? "null"}, delivery date: {invoice.DeliveryDate?.Value ?? "null"}");
Text
INV-2041, issued 2026-10-14, total 215.78
Northwind Supply Ltd -> Blue Heron Bakery, IBAN GB33 BUKB 2020 1555 5555 55
  Flour, 25 kg bag: 4 pcs x 31.50 = 126
  Cane sugar, 10 kg: 2 pcs x 18.25 = 36.50
  Baking parchment roll: 6 pcs x 7.40 = 44.40
BT-112 "£215.78" -> 215.78, words [83], box (2072, 1474), confidence 1.00
Due date: null, delivery date: 2026-11-13

What came back

  • Each ExtractedField has the exact Text, a normalized Value (an ISO date, a plain decimal amount), typed Number or Date where it applies, the indices of its words in the page and their box.
  • invoice.Extraction.Get("BT-112") looks any field up by its business term: BT-1 number, BT-2 issue date, BT-27 seller, BT-44 buyer, BT-84 IBAN, BT-106 subtotal, BT-110 tax, BT-112 total, and line items from BT-126 to BT-153.
  • A field the model didn’t find is null, never a guess. Here the model tagged the second date as the delivery date (BT-72) rather than the due date (BT-9), so DueDate is null: read the fields you depend on, and look at Confidence, which is the mean probability of the field’s word tags (not a calibrated probability like a word’s).
  • Amounts are normalized to plain decimals: 126.00 came back as 126.

Good to know

  • ExtractInvoice and ExtractReceipt take a DocumentExtractionOptions (MaxDegreeOfParallelism, Strict) and have Async twins.
  • For your own document type, Extract<T> takes a source-generated JsonTypeInfo<T> and a model you fine-tuned; the extraction model spec describes the model and the overview the API.
  • Receipts work the same way: see Receipts.