RemitExtract 0.1.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package RemitExtract --version 0.1.0
                    
NuGet\Install-Package RemitExtract -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RemitExtract" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RemitExtract" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="RemitExtract" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RemitExtract --version 0.1.0
                    
#r "nuget: RemitExtract, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RemitExtract@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RemitExtract&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=RemitExtract&version=0.1.0
                    
Install as a Cake Tool

RemitExtract

Offline .NET library that extracts payment fields (IBAN, BIC, amount, currency, beneficiary name, payment reference, US ABA routing number) from invoices, so an international bank-transfer form can be prefilled. Everything runs locally: no cloud OCR, no external API calls. Ships as a NuGet package (RemitExtract) plus a CLI sample that demonstrates it end to end.

Install

dotnet add package RemitExtract

Multi-targets net8.0 and net10.0. Packed with SourceLink and a .snupkg symbol package, so you can step into the library's source from a debugger.

Dependencies

Four packages, all MIT or Apache-2.0, all offline: PdfPig 0.1.15 (Apache-2.0, PDF text layer), ZXing.Net 0.16.11 (MIT, barcode decode/encode), ZXing.Net.Bindings.SkiaSharp 0.16.22 (MIT, ZXing's SkiaSharp bitmap binding), SkiaSharp 3.119.4 (MIT, cross-platform image decode). Note the PdfPig nuget id is PdfPig, not UglyToad.PdfPig — the latter id exists but only carries prerelease builds; the namespaces are UglyToad.PdfPig.* either way. Package versions across the solution are centrally managed in Directory.Packages.props (NuGet Central Package Management) — individual .csproj files reference packages without a Version attribute.

Layout

RemitExtract.slnx
├── src/RemitExtract/           the library (NuGet package "RemitExtract")
│   ├── Core/                   model, pipeline, validators, extraction vocabulary
│   ├── Sources/                PdfPig text layer, plain text, barcode decoder, IOcrEngine seam
│   └── Stages/                 the six pipeline stages
├── src/RemitExtract.Sample/    CLI sample that references the library
│   └── samples/                DE / UK / US fixtures, an EPC GiroCode payload, and its QR image
├── src/RemitExtract.Sample.WebApi/  ASP.NET Core sample exposing POST /api/extract
├── tests/RemitExtract.Tests/   xUnit tests against the library directly
└── tests/RemitExtract.Sample.WebApi.Tests/  xUnit tests against the Web API sample

Build, test, pack

dotnet restore
dotnet build
dotnet test

dotnet pack src/RemitExtract/RemitExtract.csproj -c Release   # -> RemitExtract.<version>.nupkg + .snupkg

Run the CLI sample

dotnet run --project src/RemitExtract.Sample -- src/RemitExtract.Sample/samples/invoice-de.txt --trace
dotnet run --project src/RemitExtract.Sample -- src/RemitExtract.Sample/samples/invoice-uk.txt
dotnet run --project src/RemitExtract.Sample -- src/RemitExtract.Sample/samples/invoice-us.txt
dotnet run --project src/RemitExtract.Sample -- src/RemitExtract.Sample/samples/invoice-de.txt --qr src/RemitExtract.Sample/samples/girocode.txt
dotnet run --project src/RemitExtract.Sample -- src/RemitExtract.Sample/samples/invoice-girocode.png --trace

The .txt fixtures stand in for OCR output so you can exercise the whole pipeline without a scanner. Point it at a real digital PDF and the PdfPig path takes over. invoice-girocode.png is the same GiroCode payload as girocode.txt, rendered as a QR image — point the CLI straight at it and BarcodeDecodingStage decodes the code itself, no --qr flag needed.

Run the Web API sample

A minimal ASP.NET Core host wraps the same pipeline behind one HTTP endpoint, listening on http://localhost:5080.

dotnet run --project src/RemitExtract.Sample.WebApi

curl -X POST http://localhost:5080/api/extract \
  -F "file=@src/RemitExtract.Sample/samples/invoice-de.txt"

The JSON response has one entry per field (empty fields are still present, just with a null value), plus elapsedMs and any rejected candidates that failed validation — it's the same six-stage pipeline as the CLI sample, run synchronously. GET / returns a small banner naming the endpoint, and an OpenAPI document is served at /openapi/v1.json in the Development environment (the default via launchSettings.json); the same environment also serves an interactive Swagger UI at http://localhost:5080/swagger, so you can try the endpoint from a browser instead of curl. This is a wiring demonstration for a real backend, not a production API — no auth, no async job queue for slow OCR; see "Wiring into a web API" below for what a production deployment needs on top of it.

What is real vs. what is a seam

Component State
IBAN mod-97 + country length table Fully implemented, verified
US ABA routing checksum Fully implemented, verified
Amount parsing (1.234,56 vs 1,234.56) Fully implemented, verified
EPC069-12 GiroCode parser Fully implemented
Swiss QR-bill parser Fully implemented
Factur-X / ZUGFeRD CII XML Implemented, namespace-agnostic
PDF text layer (PdfPig, words + boxes) Implemented
Factur-X / ZUGFeRD PDF/A-3 attachment Implemented via document.Advanced.TryGetEmbeddedFiles
BIC directory lookup SeamBicValidator.ExistsInDirectory returns false; wire to your SWIFT directory
OCR engine SeamIOcrEngine, currently NullOcrEngine
Barcode decoding (GiroCode / Swiss QR-bill, from a .jpg/.jpeg/.png) Implemented — ZXingBarcodeDecoder (ZXing.Net + SkiaSharp); --qr manual injection still works too
Layout model (LiLT / local LLM) Seam — register a stage at Order = 50

No model of any kind is called at runtime. The pipeline is deterministic C# plus one managed PDF reader — no network egress, no external service dependency. The seams above are all designed for local components too.

Adding a real OCR engine

Implement IOcrEngine and return TextLines with boxes. Two offline options:



<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.19.2" />


<PackageReference Include="TesseractOCR" Version="5.3.5" />

Run it out-of-process (gRPC sidecar) in production. A native crash in ONNX Runtime or leptonica must not take down the transfer API pod, and OCR pods need to scale — and possibly get a GPU — independently of the API.

Adding a local layout model

public sealed class LocalModelStage : IExtractionStage
{
    public int Order => 50;   // after rules, before the validation gate

    public async Task ExecuteAsync(ExtractionContext ctx, CancellationToken ct)
    {
        // Skip entirely if a structured payload already answered everything.
        if (ctx.HasStructuredPayload) return;

        // LiLT (ONNX, int8, ~130MB, ~300ms/page CPU) or a local LLM sidecar
        // with GBNF-constrained JSON output. Emit FieldCandidate with
        // Source = ExtractionSource.LocalModel so it always ranks below
        // deterministic sources — and always passes back through the gate.
    }
}

The contract matters more than the model: anything the model proposes is re-checked by ValidationGateStage, so a hallucinated IBAN dies at mod-97 rather than reaching a customer.

Extending coverage cheaply

Core/ExtractionVocabulary.cs is the highest-leverage file. RuleBasedFieldStage, BeneficiaryNameStage, and ValidationGateStage all take an optional IExtractionVocabulary constructor parameter — pass null (or nothing) for the built-in 8-language default, or build a custom one with ExtractionVocabularyBuilder:

var vocabulary = new ExtractionVocabularyBuilder()
    .AddLabels(TransferField.PaymentReference, "გადახდის დანიშნულება")
    .AddCurrencyCode("XYZ")
    .AddBicStopwords("SUBSCRIPTION")
    .ReplaceLabels(TransferField.Amount, "amount due")   // escape hatch: replaces the built-in set
    .Build();

var pipeline = new ExtractionPipeline(
[
    new EmbeddedPayloadStage(),
    new TextAcquisitionStage(sources: [...], ocr: ...),
    new RuleBasedFieldStage(vocabulary),
    new BeneficiaryNameStage(vocabulary),
    new ValidationGateStage(vocabulary),
]);

Add* methods are additive (they extend the built-in set); Replace*/Clear* methods are the explicit escape hatch for a built-in label or stopword that's actively wrong for your corridor. No deploy needed to add a corridor - build the vocabulary from a DB table or config file at startup.

Notes on behaviour worth keeping

  • A candidate that fails a checksum is dropped, not demoted. A blank field costs the customer seconds; a wrong prefilled IBAN costs a recall procedure.
  • Beneficiary name is never auto-filled even from a QR code — it always requires explicit confirmation, because the invoice's company name frequently differs from the registered account holder and sanctions screening runs on the confirmed value.
  • BIC country is cross-checked against IBAN country; a mismatch demotes the BIC to "empty" rather than prefilling a likely misread.
  • MaxPages = 3 in PdfTextLayerSource — payment details live on page 1 or the last page, and this caps worst-case OCR cost.
  • The context holds no document bytes beyond the stage that reads them. Log extracted fields, never the document.

Wiring into a web API

The pipeline doesn't care that the sample host is a CLI. Swap the array literal in Program.cs for DI registrations and it drops into any ASP.NET Core host — or any other .NET host with a container — the same way:

services.AddSingleton<ITextSource, PdfTextLayerSource>();
services.AddSingleton<ITextSource, PlainTextSource>();
services.AddSingleton<IOcrEngine, GrpcOcrEngine>();

services.AddSingleton<IExtractionStage, EmbeddedPayloadStage>();
services.AddSingleton<IExtractionStage, TextAcquisitionStage>();
services.AddSingleton<IExtractionStage, RuleBasedFieldStage>();
services.AddSingleton<IExtractionStage, BeneficiaryNameStage>();
services.AddSingleton<IExtractionStage, ValidationGateStage>();

services.AddSingleton<ExtractionPipeline>();

ExtractionPipeline is stateless once built, so registering it as a singleton is fine; give individual ITextSource/IOcrEngine implementations a shorter lifetime if they hold per-request or per-connection state.

How you expose the pipeline from there depends on your host and callers. If extraction is fast (structured payload, digital PDF, no OCR), a plain synchronous endpoint that runs the pipeline and returns the result is the simplest option. If OCR is in play, it can take a few seconds, so callers that can't hold a request open that long may be better served by an async pattern — accept the file, return a job identifier, and let the caller poll or subscribe for the result however your stack already does that (webhooks, SignalR, polling, a message queue). Either way, treat local extraction like any other pipeline you're putting behind an API: cap the upload size, put a timeout on each stage so one slow step can't hang the whole request, and bound how many extractions run concurrently so a burst of uploads can't exhaust memory or CPU.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.1 94 8/7/2026
0.1.0 90 8/7/2026