Bonoscan.Components 0.33.0
Bonoscan.Components
A reusable C# Blazor document-scanner component. It opens the camera, finds the document in real time (a DocAligner ONNX corner-detection model — no OCR), auto-captures when you hold it steady inside the frame, deskews it with a pure-C# perspective warp, and hands you the result. Also does single-image upload and torch/flashlight where supported.
The library is split into three packages that all sit behind a DI-resolved IScannerEngine:
Bonoscan.Components— the shared, host-agnostic UI and engine pieces: the components (DocumentScanner,ScannerView,ResultView,DocumentUpload),ScannerOptions/ScannerState/ScannerResult/ScannerLabels, the one sharedscanner.jsviewfinder (camera loop + framing + hold-to-capture state machine + overlay) and itsScannerEngineBase, and the pure-C#Warper/OutputDims(homography + bilinear perspective warp overRgbImage, encoded via StbImageWriteSharp). It carries the DocAligner detection contract and image decode/encode in-repo (StbImageSharp / StbImageWriteSharp) — no external detector dependency — but carries no detector implementation itself.Bonoscan.Components.Client— the in-browser engineBrowserScannerEnginefor Blazor WebAssembly / MAUI Blazor Hybrid ("wasm"mode). Live detection runs entirely in JS: the sharedscanner.jsimports the DocAligner Web Worker module (onnxruntime-web, off the main thread) — no .NET on the live path; the engine supplies the C# capture warp + upload. The module, its worker, onnxruntime-web and the model are static web assets of that package.Bonoscan.Components.Server— the Blazor Server engineServerScannerEngine: a thinScannerEngineBasesubclass ("http"mode) where the sharedscanner.jsPOSTs each frame to an off-circuit/_bonoscan/detectendpoint (native ONNX, with the DocAligner model embedded in the package) and a/_bonoscan/captureendpoint (server-side warp), so per-frame work never touches the SignalR circuit.
The Server engine's DocAligner model + ONNX Runtime are bundled inside Bonoscan.Components.Server;
the in-browser model + onnxruntime-web ship inside Bonoscan.Components.Client. Either way the
component is self-contained — nothing is fetched from a CDN at runtime.
Install
Reference the package for your host model — Bonoscan.Components.Client for Blazor
WebAssembly / MAUI, Bonoscan.Components.Server for Blazor Server (each pulls in the shared
Bonoscan.Components):
dotnet add package Bonoscan.Components.Client # or Bonoscan.Components.Server
The components, scoped CSS, the thin JS interop, and the detection model ship inside the
packages as static web assets and are served automatically from _content/… — nothing to
copy, no build step.
Two stylesheets are required, and both must be linked by your host (step 3 below):
{AppName}.styles.css, the Blazor bundle that carries the components' scoped CSS, and
_content/Bonoscan.Components/bonoscan.css, which carries the --scanner-* design tokens and
the shared layout/button/dropzone rules the components are built from. bonoscan.css is not
optional theming — without it the chrome has no colours, radii or blur, the buttons are unstyled,
and the upload dropzone's file input renders as a raw browser control.
Use it
1. Register the service (in Program.cs, or MauiProgram.cs for Hybrid):
// Blazor WebAssembly / MAUI Hybrid
using Microsoft.Extensions.DependencyInjection;
builder.Services.AddBonoscanClient(o =>
{
o.GuideAspect = 0.707; // A4 portrait guide; auto-rotates; null = no guide
o.FillThreshold = 0.60; // document must fill 60% of the guide before capture
o.CaptureWidth = 1240;
o.CaptureHeight = 1754; // PNG by default; set o.Mime = "image/jpeg" for JPEG
});
// Blazor Server
builder.Services.AddBonoscanServer(o =>
{
o.Scanner = s => { s.GuideAspect = 0.707; s.CaptureWidth = 1240; s.CaptureHeight = 1754; };
});
// after the app is built, map the off-circuit endpoints:
app.MapBonoscanDetection();
On Blazor Server the captured image is returned to .NET over the SignalR circuit, so raise the hub's receive limit above its 32 KB default (a scan is larger) or the circuit drops mid-capture:
builder.Services.AddRazorComponents().AddInteractiveServerComponents()
.AddHubOptions(o => o.MaximumReceiveMessageSize = 32 * 1024 * 1024);
2. Drop in the component:
@using Bonoscan.Components
<DocumentScanner OnCaptured="OnCaptured" />
@code {
private void OnCaptured(ScannerResult r)
{
// r.Bytes is the image data you own — store / upload / OCR it.
// r.Mime is the encoded content type (e.g. "image/png"); r.FileName a suggested name.
// r.FromUpload is true if it came from the file picker rather than the camera.
// To display it, render <ScannerImage Result="r" /> — see below.
}
}
The full-bleed camera, auto-capture, framing guide, torch, upload, and a result view with Scan again / Download are all built in.
Upload only (no camera)
For a flow where the user just uploads an image and the library crops/deskews it — no
camera, no <video>/<canvas> — use DocumentUpload (same registration, same
ScannerResult):
@using Bonoscan.Components
<DocumentUpload OnCaptured="OnCaptured" />
It accepts click-to-choose and drag-and-drop. Same Options, Labels,
OnCaptured/OnError/OnConfirm, Layout, and result-view slots as DocumentScanner.
Each mounted scanner owns its own camera, loop and result, so DocumentUpload and
DocumentScanner can coexist on one page. Only the in-browser detection worker is shared, and it
is released when the last scanner on the page goes away.
Getting the image data (server / native, not just display)
The ScannerResult from OnCaptured carries the image bytes in r.Bytes. The bytes stay
valid after the scanner stops, the component unmounts, or another scan runs.
<DocumentScanner OnCaptured="OnCaptured" />
@code {
private void OnCaptured(ScannerResult r)
{
byte[] bytes = r.Bytes.ToArray(); // the image data: store / upload / OCR
// r.Mime is the content type; r.FileName a suggested name.
}
}
r.Bytes is materialized at capture and is identical across WebAssembly, Server, and MAUI.
Displaying a result yourself
ScannerResult is a pure value — it hands out no URL, because a blob: URL is a browser resource
whose correct lifetime is "until the <img> has decoded it", not "as long as someone holds the
object". Render it with ScannerImage, which does that for you and revokes the URL once the paint
completes:
<ScannerImage Result="_result" Presenter="context" Class="preview" />
Presenter is optional and is the scanner that produced the result (the context inside a
ScannerView/DocumentScanner slot, or the Presenter on a ScannerResultContext). Pass it and
the image is painted from the copy the browser already holds — on Blazor Server that means the
image is never sent back across the circuit. Omit it and the bytes are streamed instead.
You never have to reason about whether the presenter is still usable. A presenter serves exactly one
result — the last one its scanner produced — and reports false rather than silently painting
nothing if you hand it anything else: a result kept after the scanner was disposed, or an older
result from a scanner that has since captured again. ScannerImage then falls back to the bytes,
which always work. Size it through --scanner-result-max-width / --scanner-result-max-height
(both 100% by default).
3. Link both stylesheets in your host (index.html for WebAssembly, App.razor or _Host
for Server). The first is the Blazor scoped-CSS bundle the templates already include; the second
is required, not optional:
<link href="YourApp.styles.css" rel="stylesheet" />
<link href="_content/Bonoscan.Components/bonoscan.css" rel="stylesheet" />
Useful parameters
Options (per-instance override of the DI defaults), Labels (a ScannerLabels with all the
static UI text), OnCaptured, OnError, OnConfirm, AutoStart, EnableCamera,
EnableUpload, ShowTorch, ShowResult (set false to render your own result UI from
OnCaptured), ShowDownload, ShowBrand, IdleContent (a custom idle overlay), and Class.
The component is driven entirely by these parameters and its callbacks — there is no @ref
API; to tear it down, stop rendering it (it releases the camera on dispose). For a fully custom
flow, use the headless <ScannerView> below.
Detection is an ONNX model — there are no detection threshold knobs to tune; accuracy is the
model's. The auto-capture feel (hold time, steadiness, framing guide) is fully tunable via
ScannerOptions (see below).
Customize the built-in UI
Keep the ready-made scanner but reshape it — without forking it:
- Embed it inline. By default the scanner is a full-viewport overlay; set
Layout="ScannerLayout.Inline"to make it an in-flow box you size yourself (via the--scanner-width/--scanner-heightvariables or your own wrapper), e.g. inside a card or a column. - Replace individual pieces. Each chrome element is a replaceable slot, so you can swap
just the part you want and keep the rest. The live-scan slots —
BrandContent,StatusContent,GuidanceContent(the live hint pill) — receive the scan handle (context.State+ its commands). The result slots —ResultContent(the whole result view) andResultActionsContent(just its buttons) — receive aScannerResultContext:context.Resultfor the captured image (Result.Bytes) andcontext.Retake/context.Confirmfor the actions. Everything is driven from inside the slot — no@ref.context.Presenteron the result slots renders the image with<ScannerImage Result="context.Result" Presenter="context.Presenter" />.
<DocumentScanner Layout="ScannerLayout.Inline" OnConfirm="Use">
<ResultActionsContent>
<button @onclick="context.Retake">Retake</button>
<button @onclick="context.Confirm">Use this scan</button>
</ResultActionsContent>
</DocumentScanner>
Build your own UI (the building blocks)
There are two ways to use the library — pick per your needs:
- Premade —
<DocumentScanner>, the full styled flow. Override only the colours/sizing via the--scanner-*CSS variables (below). - Building blocks — compose the two single-responsibility components into your own
layout, the way
DocumentScannerdoes internally:<ScannerView>— the scan engine only (camera, detection, auto-capture, upload, torch). It renders just the camera stage and raisesOnCaptured(ScannerResult); it never renders a result. Its slotcontextis anIScannerHandle: the liveState(Phase,Reason,Progress,Fill,IsReady, …) plusStartAsync,StopAsync,RescanAsync,ResetAsync,UploadAsync,ToggleTorchAsync,GetCornersAsync. Attaching to the DOM costs a JS module load, so gate your own start/upload controls onState.IsReady— the commands report an error until it is true.<ResultView>— the result display only. Takes aResult(and optionally thePresenterthat produced it) and raisesOnRetake/OnConfirm.<ScannerImage>— just the<img>, for a fully custom result UI. Needs an interactive render mode: it paints fromOnAfterRenderAsync, which never runs under static SSR.
@if (_result is null)
{
<ScannerView AutoStart="true" OnCaptured="r => _result = r" OnError="e => _err = e" />
}
else
{
<ResultView Result="_result" ShowConfirm="true"
OnRetake="() => _result = null" OnConfirm="Use" />
}
@code {
private ScannerResult? _result;
private string? _err;
private void Use() { var bytes = _result!.Bytes; /* store / upload / OCR */ }
}
Errors from the camera/detector reach OnError and the host's ILogger, so they're
visible in a MAUI WebView and on a Blazor Server circuit, not only the browser console.
Theme it (CSS variables)
bonoscan.css (linked in step 3 above) defines every --scanner-* default at zero specificity —
for both the DOM chrome and the <canvas> viewfinder — alongside the shared layout rules. Every
class it defines outside the token block is prefixed scn- (.scn-btn, .scn-brand,
.scn-status, .scn-dropzone, …) plus the component roots .scanner-app / .bs-stage, so it can
sit in a host's global stylesheet without capturing the host's own elements.
Override any token from your own :root (it always wins, regardless of load order):
:root {
--scanner-accent: #3b82f6; /* primary / lock-on colour */
--scanner-accent-ink: #ffffff; /* text on the accent button */
--scanner-amber: #f5b454; /* "align the frame" colour */
--scanner-text: #eef2f4;
--scanner-danger: #ff6b6b;
--scanner-bg: #000; /* letterbox backdrop */
--scanner-font: "Inter", system-ui, sans-serif;
--scanner-mono: "JetBrains Mono", ui-monospace, monospace;
}
Every other visual value is a token too — set only the ones you want; each falls back to the built-in default, so nothing in the components is a hardcoded colour/size you can't reach:
- Surfaces / borders —
--scanner-chrome-bg,--scanner-chrome-border,--scanner-veil-bg,--scanner-ghost-bg,--scanner-ghost-bg-hover,--scanner-ghost-border,--scanner-ghost-border-hover. - Type sizes —
--scanner-brand-font-size,--scanner-status-font-size,--scanner-hint-font-size,--scanner-pill-font-size,--scanner-btn-font-size. - Radii / spacing —
--scanner-radius-pill,--scanner-radius-control,--scanner-pad-top,--scanner-pad-bottom,--scanner-pad-edge,--scanner-gap,--scanner-veil-gap,--scanner-btn-pad,--scanner-pill-pad,--scanner-status-pad, and (inline layout)--scanner-width/--scanner-height. - Sizing —
--scanner-control-size(round controls like the torch),--scanner-dot-size, and--scanner-result-max-width/--scanner-result-max-heightfor the rendered result image. - Effects —
--scanner-blur,--scanner-primary-shadow,--scanner-brand-shadow,--scanner-torch--scanner-torch-glow, and the upload dropzone's--scanner-dropzone-border.
The library uses no ::deep, so for the scoped component chrome these tokens are the only
way to retheme it. The live viewfinder drawn on the <canvas> (detected-document quad,
framing guide, guidance banner, progress ring) themes from the same variables — plus two
canvas-only ones: --scanner-guide (neutral framing guide + ring track, default white) and
--scanner-overlay-scrim (guidance-banner bg + dim mask, default black). The camera frame's fit
inside the stage is --scanner-video-fit (contain inline, cover fullscreen). The canvas reads
these once when a scan starts.
The Class parameter on every component lands on an element the library authors, so it carries
the library's scope id — a rule written in your own MyPage.razor.css will not match it. Put
Class rules in a global stylesheet, or wrap the component and reach in from your own scoped file:
.my-frame ::deep .scanner-app { border-radius: 18px; }
Hosting notes
- Camera needs a secure context (HTTPS or
localhost) in every host. - Blazor WebAssembly / MAUI — the shared
scanner.jsruns the camera loop and runs detection in a Web Worker (the DocAligner module bundled inBonoscan.Components.Client, onnxruntime-web, off the main thread); .NET handles only the capture warp + upload. - In-browser detection speed (WebAssembly / MAUI) — serving over HTTPS (a secure
context) lets onnxruntime-web use the WebGPU backend, the fastest path. To also let the
WASM fallback run multi-threaded on devices without WebGPU, serve the document
cross-origin-isolated (
Cross-Origin-Opener-Policy: same-origin+Cross-Origin-Embedder-Policy: require-corporcredentialless). Plain HTTP over a LAN IP gets the slowest single-threaded WASM path. - Blazor Server / Web App — fully supported, including prerendering. The shared
scanner.jsloop runs off the SignalR circuit (POSTing frames to the off-circuit/_bonoscan/detect//_bonoscan/captureendpoints), so per-frame work never crosses the circuit; only discrete state transitions and the final result do. Use any render mode (InteractiveServer,InteractiveWebAssembly, orInteractiveAuto). - .NET MAUI Blazor Hybrid — grant the platform camera permission yourself (Android
CAMERAin the manifest + handle the WebViewOnPermissionRequest; iOSNSCameraUsageDescription). - An interactive render mode is required. Under static SSR the components render their frame but never attach — the camera and file picker cannot run — and they log a warning saying so.
- Scanner state is per instance:
scanner.jshands each mounted scanner its own handle for the camera, loop, config and retained result blob, so several can coexist on a page. Noblob:URL is ever handed to .NET — one is minted per paint and revoked as soon as the image has decoded. - The in-browser detection worker is shared and reference-counted. Every scanner on the page,
and every engine's own detector, takes a claim on it; it is created on the first claim and
terminated when the last one is released, which happens when the scanner is disposed — not when it
is merely stopped or reset, so a retake does not pay the model-load cost again. Two scanners share
one ONNX session and therefore one inference queue: their detections interleave, so
DetectIntervalMsbounds one instance's rate, not the worker's total load. - The engine is not a DI service.
AddBonoscanClient/AddBonoscanServerregister anIScannerEngineFactory; each component creates its ownIScannerEnginefrom it and disposes it on unmount. Don't registerIScannerEngineyourself — a container tracks every disposable it creates for the lifetime of the scope (the app on WebAssembly, the circuit on Server), so a registered engine would accumulate one dead instance per mount.
Notes
Detection is the DocAligner ONNX model that localises the document's four corners directly — robust to colour, lighting, and background. Portrait phone cameras stream landscape, so you'll see letterbox bars (the whole document stays visible rather than being cropped).
Showing the top 20 packages that depend on Bonoscan.Components.
| Packages | Downloads |
|---|---|
|
Bonoscan.Components.Server
Blazor Server scan engine for Bonoscan.Components: an off-circuit HTTP
detect endpoint (DocAligner ONNX, model embedded in this package), a thin JS live loop
that keeps per-frame detection/tracking off the SignalR circuit, and a server-side C#
perspective warp at capture. Provides the IScannerEngine implementation the
ScannerView / DocumentUpload components consume on Blazor Server.
|
173 |
|
Bonoscan.Components.Server
Blazor Server scan engine for Bonoscan.Components: an off-circuit HTTP
detect endpoint (DocAligner ONNX, model embedded in this package), a thin JS live loop
that keeps per-frame detection/tracking off the SignalR circuit, and a server-side C#
perspective warp at capture. Provides the IScannerEngine implementation the
ScannerView / DocumentUpload components consume on Blazor Server.
|
13 |
|
Bonoscan.Components.Server
Blazor Server scan engine for Bonoscan.Components: an off-circuit HTTP
detect endpoint (DocAligner ONNX, model embedded in this package), a thin JS live loop
that keeps per-frame detection/tracking off the SignalR circuit, and a server-side C#
perspective warp at capture. Provides the IScannerEngine implementation the
ScannerView / DocumentUpload components consume on Blazor Server.
|
9 |
|
Bonoscan.Components.Server
Blazor Server scan engine for Bonoscan.Components: an off-circuit HTTP
detect endpoint (DocAligner ONNX, model embedded in this package), a thin JS live loop
that keeps per-frame detection/tracking off the SignalR circuit, and a server-side C#
perspective warp at capture. Provides the IScannerEngine implementation the
ScannerView / DocumentUpload components consume on Blazor Server.
|
3 |
|
Praxxme.Components.Reactive
Reactive Praxxme Blazor components built on Fluxor for state-driven UIs (loading overlays, reactive list pages).
|
1 |
|
Praxxme.Components.Reactive
Reactive Praxxme Blazor components built on Fluxor for state-driven UIs (loading overlays, reactive list pages).
|
0 |
|
Bonoscan.Components.Client
In-browser (Blazor WebAssembly / MAUI Blazor Hybrid) scan engine for
Bonoscan.Components: the shared viewfinder runs detection in a Web Worker (DocAligner on
onnxruntime-web) and a pure-C# perspective warp. Provides the
IScannerEngine implementation the ScannerView / DocumentUpload components consume.
|
0 |
.NET 10.0
- Microsoft.AspNetCore.Components.Web (>= 10.0.0)
- StbImageSharp (>= 2.30.15)
- StbImageWriteSharp (>= 1.16.7)
| Version | Downloads | Last updated |
|---|---|---|
| 0.34.0 | 8 | 11/10/2026 |
| 0.33.2 | 13 | 11/04/2026 |
| 0.33.1 | 3 | 11/04/2026 |
| 0.33.0 | 4 | 11/04/2026 |
| 0.31.0 | 221 | 06/04/2026 |
| 0.27.0 | 3 | 06/02/2026 |
| 0.26.0 | 3 | 06/02/2026 |
| 0.25.0 | 2 | 06/02/2026 |
| 0.24.0 | 2 | 06/02/2026 |
| 0.23.0 | 2 | 06/02/2026 |
| 0.22.0 | 4 | 05/31/2026 |
| 0.21.0 | 2 | 05/31/2026 |
| 0.20.0 | 3 | 05/31/2026 |
| 0.19.0 | 4 | 05/31/2026 |
| 0.18.0 | 2 | 05/31/2026 |
| 0.17.0 | 3 | 05/31/2026 |
| 0.16.0 | 2 | 05/31/2026 |
| 0.15.0 | 1 | 05/30/2026 |
| 0.14.0 | 2 | 05/30/2026 |
| 0.13.0 | 3 | 05/30/2026 |
| 0.12.0 | 5 | 05/30/2026 |
| 0.11.0 | 4 | 05/30/2026 |
| 0.10.0 | 2 | 05/29/2026 |
| 0.9.0 | 2 | 05/29/2026 |
| 0.8.0 | 1 | 05/29/2026 |
| 0.7.0 | 0 | 05/29/2026 |
| 0.6.0 | 2 | 05/29/2026 |
| 0.5.0 | 1 | 05/29/2026 |
| 0.4.0 | 2 | 05/29/2026 |
| 0.3.0 | 2 | 05/29/2026 |
| 0.2.0 | 2 | 05/29/2026 |
| 0.1.0 | 2 | 05/29/2026 |