Henara.Scan.Client 0.9.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.
Which edition of Muster 13 arrived
Muster 13 is issued in two editions. The pre-printed Vordruck is red print on a blue flood fill, its entry fields boxes cut out of that fill; the Blankoformularbedruckung (Muster 13.1/E and 13.2/E) is printed by the practice software in black on white security paper under a red raster, and its entry fields are printed lines. Same fields, same rules, different paper.
The service tells them apart by the colour of the paper and reads each against its own schema — you
do not choose, and nothing in the request selects it. Edition on Muster13Reading and on
ActScanFrontPage says which one it decided on.
if (reading.Edition.TryGetValue(out var edition) && edition == FormEdition.PracticePrinted)
ShowBlankoformular();
It carries nothing when there was no paper to judge — a PDF read never has any — and when the colour was too washed out to name one, in which case the sheet was read as the pre-printed Vordruck. A Muster 56 read never carries one; that form has a single edition.
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.4.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)