Henara.Scan.Client 0.2.0

Henara.Scan.Client

Reads German medical act scans (Muster 13, Muster 56):

  • Detects the page in a photo or scan, then crops and deskews it.
  • Combines several pages into one PDF.
  • Extracts the prescription fields with Mistral OCR or the Henara (Azure) OCR. Both engines answer in one shape, so a front page read by either can be compared field by field.
  • Cuts the form's declared regions — the doctor's stamp, the signature — out as images.
  • Renders the filled form back as PDF or HTML.
  • Reads arbitrary images against an annotation schema of your own.

Setup

dotnet add package Henara.Scan.Client
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScanClient(builder.Configuration);

AddScanClient binds from the configuration root, so plain environment variables are enough. It returns the underlying IHttpClientBuilder, so a caller can add a message handler of its own:

builder.Services.AddScanClient(builder.Configuration)
    .AddHttpMessageHandler<MyTracingHandler>();

Configuration

Variable Required Default Meaning
SCAN_SERVICE_URL yes Base address of the scan service.
SCAN_API_TOKEN yes Sent as a bearer token on every call.
SCAN_DEADLINE_SECONDS no 300 Deadline applied to every call. 1 to 3600.
SCAN_MAX_MESSAGE_BYTES no 67108864 Largest request or response message.

Settings are validated when the host starts, so a missing address or token fails at startup rather than on the first call. SCAN_SERVICE_URL takes either an https:// or an http:// address; the token is sent on both.

Calling

Every method returns a Result<TValue, ScanError>. No method throws: a rejected request, a failed call, a cancellation and an unexpected error all come back as the error side.

Resolve it with Match:

var message = (await scan.Crop(document, cancellationToken)).Match(
    page => Save(page.Content, page.ContentType),
    error => $"{error.Code}: {error.Message}");

Or take it apart with HasValue to return early:

if (!(await scan.Crop(document, cancellationToken)).HasValue(out var page, out var error))
    return error;

Crop one page

Detects the page in an image, then crops and deskews it. An image no page could be detected in comes back as it was given.

var cropped = await scan.Crop(scannedFront, cancellationToken);

Combine pages into a PDF

One image per page, in page order, at least two.

var pdf = await scan.Combine([front, back], isAutocropEnabled: true, cancellationToken);

Read both sides with Mistral

Two images are read as one document, so the read spans both sides. Leave IsAutocropEnabled off for images that were cropped already — warping twice degrades them.

var reading = await scan.ReadImagesWithMistral(
    new ImageReadRequest(
        ScanForm.Muster13,
        [new SheetImage(SheetSide.Front, front), new SheetImage(SheetSide.Back, back)],
        IsAutocropEnabled: true,
        IsRegionCropEnabled: true,
        new ReadOutputs(
            RenderTargets.Pdf | RenderTargets.Html,
            IsDocumentAnnotationRequested: true,
            IsRawJsonRequested: false)),
    cancellationToken);

Read a PDF with Mistral

A PDF has no frame to detect a page in and no sheet to cut a rectangle out of, so the request carries neither crop option. One page or exactly two.

var reading = await scan.ReadPdfWithMistral(
    new PdfReadRequest(
        pdf,
        ScanForm.Muster13,
        new ReadOutputs(
            RenderTargets.None,
            IsDocumentAnnotationRequested: true,
            IsRawJsonRequested: false)),
    cancellationToken);

Read the front page with either engine

Both engines answer in one shape, so the same call compares them. Exactly one image; any other count is refused. OcrEngine.Mistral always crops and overrides an autocrop left off.

var frontPage = await scan.ReadFrontPage(
    OcrEngine.Azure,
    new ImageReadRequest(
        ScanForm.Muster13,
        [new SheetImage(SheetSide.Front, front)],
        IsAutocropEnabled: false,
        IsRegionCropEnabled: false,
        new ReadOutputs(
            RenderTargets.None,
            IsDocumentAnnotationRequested: false,
            IsRawJsonRequested: false)),
    cancellationToken);

ActScanFrontPage.MistralOnly is filled only after an OcrEngine.Mistral read. It carries what Azure does not report at all, and the full values behind the single-valued fields — every ICD-10 code, every remedy, every treatment count.

Read against a schema of your own

No known form stands behind such a read, so nothing is cropped out and nothing is rendered. Several images are composed into one PDF by the service and read as a single document.

var reading = await scan.ReadImagesWithSchemaWithMistral(
    new SchemaReadRequest(schemaJson, [page1, page2], IsAutocropEnabled: true),
    cancellationToken);

What comes back

A read returns only what the request asked for. IsDocumentAnnotationRequested and IsRawJsonRequested decide whether the engine's extraction and its own response body are sent at all; Render decides whether the filled form comes back as a PDF, as HTML, or not at all. Asking for less is a smaller message.

Errors

ScanError carries a ScanErrorCodeInvalidRequest, Unauthenticated, Unavailable, Timeout, Upstream, Cancelled, Unknown — a message, and the upstream engine's name when the failure came from one. Requests the client can refuse on its own, such as an empty image or a form it does not know, never reach the service.

Other languages

protos/scan.proto ships inside the package.

No packages depend on Henara.Scan.Client.

Version Downloads Last updated
0.10.0 5 09/06/2026
0.9.0 3 09/06/2026
0.8.0 13 09/05/2026
0.7.0 10 09/05/2026
0.6.0 33 08/29/2026
0.5.0 7 08/29/2026
0.4.0 1 08/28/2026
0.3.0 6 08/26/2026
0.2.0 1 08/26/2026