Henara.Scan.Client 0.7.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 every field of both sheets as typed objects.
- Extracts the front page of either form with Mistral OCR or the Henara (Azure) OCR, both in one shape, so a page read by either compares field by field.
- Cuts the form's stamp and signature boxes out as images, under the Vordruck's own field ids.
- 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
builder.Services.AddScanClient(builder.Configuration);
Binds from the configuration root, so environment variables alone are enough. Returns the
IHttpClientBuilder:
builder.Services.AddScanClient(builder.Configuration)
.AddHttpMessageHandler<MyTracingHandler>();
Configuration
| Variable | Required | Default | Meaning |
|---|---|---|---|
SCAN_SERVICE_URL |
yes | — | Base address of the scan service. https:// or http://. |
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. |
Validated at host start, so a missing address or token fails there rather than on the first call.
Calling
One method per form. Every method returns Result<TValue, ScanError> and none throws: a rejected
request, a failed call, a cancellation and an unexpected error all come back as the error side.
var message = (await scan.Crop(document, cancellationToken)).Match(
page => Save(page.Content, page.ContentType),
error => $"{error.Code}: {error.Message}");
if ((await scan.Crop(document, cancellationToken)).TryGetValue(out var page, out var error) == false)
return error;
IsAutocropEnabled is off by default and the service never overrides it. Leave it off for images
that were cropped already — warping twice degrades them.
Crop one page
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 a Muster 13
One image reads that side alone against its own schema; two are read as one document spanning both sides.
var reading = await scan.ReadMuster13Images(
new Muster13ImageReadRequest(
[new SheetImage(SheetSide.Front, front), new SheetImage(SheetSide.Back, back)],
IsRegionCropEnabled: true,
new ReadOutputs(RenderTargets.Pdf | RenderTargets.Html, false, false),
IsAutocropEnabled: true),
cancellationToken);
Muster13Reading.Front and .Back carry every field of the sheet they name, and are Option.None
for a sheet the read produced nothing for.
ReadMuster13Pdf takes a PDF of one page or two, and carries neither crop option. A PDF read
returns no regions.
var reading = await scan.ReadMuster13Pdf(
new PdfReadRequest(pdf, new ReadOutputs(RenderTargets.None, false, false)),
cancellationToken);
Read a Muster 56
ReadMuster56Images and ReadMuster56Pdf, returning Muster56Reading. All three of this form's
boxes sit on the back.
var reading = await scan.ReadMuster56Images(
new Muster56ImageReadRequest(
[new SheetImage(SheetSide.Front, front), new SheetImage(SheetSide.Back, back)],
new ReadOutputs(RenderTargets.None, false, false),
IsAutocropEnabled: true,
IsRegionCropEnabled: true),
cancellationToken);
Read a front page with either engine
The only two calls that take an engine, and the only ones that read with OcrEngine.Azure. Exactly
one image.
var frontPage = await scan.ReadMuster13FrontPage(
OcrEngine.Azure,
new FrontPageReadRequest(
front,
new ReadOutputs(RenderTargets.None, false, false),
IsRegionCropEnabled: true),
cancellationToken);
ActScanFrontPage.Fields is the act-scan shape, so a Muster 56 read this way answers only with the
fields the two forms share — not sport type, the units-and-months scope or the illness list. Use
ReadMuster56Images for those.
ActScanFrontPage.MistralOnly is filled only after an OcrEngine.Mistral read: 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
The one call that answers with the annotation rather than typed fields; nothing is cropped or rendered. Several images are composed into one PDF and read as a single document.
var reading = await scan.ReadImagesWithSchemaWithMistral(
new SchemaReadRequest(schemaJson, [page1, page2], IsAutocropEnabled: true),
cancellationToken);
What comes back
The typed fields are always there. IsDocumentAnnotationRequested and IsRawJsonRequested add the
engine's own annotation and response body; they are diagnostics. Render decides whether the filled
form comes back as PDF, HTML or not at all.
A closed set on the paper is an enum: Copayment, RemedyField, TherapyChange,
ApplicationType, SportType, RehabScope, FunctionalTrainingScope, TrainingForm, Illness,
HeartGroup, PrescriptionKind, FollowUpReason, WeeklyFrequency, PayerRehabScope.
A field the read did not fill is an Option<T> carrying nothing, never null; a list it did not
fill is empty.
if (front.PatientName.TryGetValue(out var name))
Console.WriteLine(name);
var city = front.PatientCity.Or("unknown");
var age = front.PatientBirthDate.Match(born => Years(born), () => -1);
Or(fallback), Map, Then, HasValue, and ToString() for the value or an empty string.
Dates are DateOnly, whether the Vordruck prints TT.MM.JJJJ or the row of single boxes captioned
T T M M J J; a two-digit year is read as this century. Muster56Back.ParticipatingSince is a
YearMonth — its boxes are captioned M M J J and carry no day.
The three printed remedy rows of a Muster 13 are IReadOnlyList<PrescribedRemedy>. The
supplementary remedy is not one of them: it has AdditionalRemedy and AdditionalRemedyUnits.
The receipt table on the back is IReadOnlyList<ReceiptRow>, its three columns already zipped by
row, so a row left blank in one column still lines up with the others.
Regions
A stamp or signature box comes back as an image and is never transcribed: it may hold nothing, a
stamp, a signature, a barcode-like graphic or any combination, and the signature usually sits on top
of the stamp. IsRegionCropEnabled asks for them; every box the read looked at then comes back,
whatever the box held.
Id |
Form | Sheet | Box |
|---|---|---|---|
stempel-arzt |
Muster 13 | front | Vertragsarztstempel / Unterschrift des Arztes |
stempel-leistungserbringer |
Muster 13 | back | Stempel/Unterschrift des Leistungserbringers |
leistung-unterschrift-1 … -20 |
Muster 13 | back | Unterschrift des Versicherten, one per receipt row |
stempel-arzt |
Muster 56 | back | Vertragsarztstempel / Unterschrift des Arztes |
unterschrift-versicherter |
Muster 56 | back | Unterschrift des Versicherten |
stempel-kk |
Muster 56 | back | Stempel der Krankenkasse / Unterschrift |
The Id is the Vordruck's own field id, so a region binds straight to the field it belongs to; the
receipt table appends the printed row number, 1-based.
HasInk says whether anything was written in the box. An unsigned prescription comes back with the
region present and HasInk false — so a region that is missing entirely means only one thing, that
the read was not asked to cut any.
var stamp = reading.Regions.FirstOrDefault(region => region.Id == "stempel-arzt");
if (stamp is { HasInk: true })
Save(stamp.Content, stamp.ContentType);
Each crop reaches a little past the printed box, because a signature routinely overruns it and
because autocrop and skew shift the rectangle across the paper. On the receipt table that means a
row's crop can catch a stroke from the row above or below. Rect is where the crop was taken from,
in 0 to 1 against the sheet.
Only image reads crop. ReadMuster13Pdf, ReadMuster56Pdf and
ReadImagesWithSchemaWithMistral return no regions.
Errors
ScanError carries a ScanErrorCode, a message, and a ScanEngine — None when no upstream is
named. Requests the client refuses on its own, such as an empty image or two images naming the same
side, never reach the service.
ScanErrorCode |
What happened | What to do |
|---|---|---|
InvalidRequest |
The request was malformed or unusable. | Fix the request; retrying will not help. |
Unreadable |
The bytes were a kind the service accepts, but nothing usable came out. | Rescan the page. |
Unauthenticated |
The token was missing, wrong or not permitted. | Fix SCAN_API_TOKEN. |
Unavailable |
The service, or something it needs to answer, could not be reached. | Retry. |
Timeout |
The call ran past its deadline. | Retry, or raise SCAN_DEADLINE_SECONDS. |
Upstream |
The service reached an OCR engine and that engine failed. | Retry; check Engine. |
Internal |
The service itself failed. | Report it; retrying will not help. |
Cancelled |
The call was cancelled. | — |
Unknown |
Anything else. | — |
Other languages
protos/scan.proto ships inside the package. The gRPC status carries the class of failure —
FailedPrecondition is Unreadable — and the scan-error-code and scan-upstream trailers carry
the exact code and engine.
No packages depend on Henara.Scan.Client.
.NET 10.0
- Google.Protobuf (>= 3.35.1)
- Grpc.Net.ClientFactory (>= 2.83.0)
- Henara.Scan.Contracts (>= 0.3.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.11)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.11)