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 ScanErrorCode — InvalidRequest, 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.
.NET 10.0
- Google.Protobuf (>= 3.35.1)
- Grpc.Net.ClientFactory (>= 2.83.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)