Bonoscan.Components 0.27.0
Bonoscan.Components
A reusable Blazor document-scanner component. It opens the camera, finds the document in real time (an on-device ONNX corner-detection model + Rust/WebAssembly deskew — no OCR), auto-captures when you hold it steady inside the frame, deskews it, and hands you the result. Also does single-image upload and torch/flashlight where supported. The detection model and its runtime are bundled inside the package — nothing is fetched at runtime.
Works in Blazor WebAssembly, Blazor Server (including prerendering), and .NET MAUI Blazor Hybrid — the same package, unchanged.
Install
dotnet add package Bonoscan.Components
That's all you need. The component's JavaScript, Web Worker, and .wasm ship
inside the package as static web assets and are served automatically from
_content/Bonoscan.Components/scanner/… — nothing to copy, no CDN, no build step.
The only dependency is the standard ASP.NET Core Blazor framework you already
have.
Use it
1. Register the service (in Program.cs, or MauiProgram.cs for Hybrid):
using Bonoscan.Components;
builder.Services.AddDocumentScanner(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
});
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 (see below).
// r.Mime is the encoded content type (e.g. "image/png"); r.FileName a suggested name.
// r.ImageUrl is a transient blob: URL for an immediate <img src> / download only.
// r.FromUpload is true if it came from the file picker rather than the camera.
}
}
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. The source image is read and processed in the
browser, so on Blazor Server the original photo never crosses the SignalR circuit — only the
cropped result does, and the preview appears immediately (the owned Bytes stream in just after).
Same Options, Labels, OnCaptured/OnError/OnConfirm, Layout, and result-view slots as
DocumentScanner.
The scanner engine is a single active instance: don't render
DocumentUploadandDocumentScanneron the same page at the same time — use one or the other.
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.ImageUrl is a transient blob: URL for an immediate <img>/download only.
}
}
r.Bytes is materialized at capture (chunked via IJSStreamReference, so not subject to
SignalR's per-message size cap) and is identical across WebAssembly, Server, and MAUI.
r.ImageUrl is a blob: URL owned by the live scanner, valid only until the next
capture/rescan; for a result you keep, build a URL from r.Bytes.
3. Make sure your host links the scoped-CSS bundle. The component's styles ride
along in your app's {AppName}.styles.css bundle — the Blazor templates already
include this line, but if you removed it, add it back:
<link href="YourApp.styles.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.ImageUrl,Result.Bytes) andcontext.Retake/context.Confirmfor the actions. Everything is driven from inside the slot — no@ref.
<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, …) plusStartAsync,StopAsync,RescanAsync,ResetAsync,UploadAsync,ToggleTorchAsync,GetCornersAsync.<ResultView>— the result display only. Takes aResultand raisesOnRetake/OnConfirm.
Neither building block forces page layout — they fill the box you give them (the premade
DocumentScanner makes that box full-screen / inline; you can size and position them however
you like). You orchestrate them from your own state — no @ref:
@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/worker 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)
Link the token stylesheet once (it defines every --scanner-* default at zero specificity, for
both the DOM chrome and the <canvas> viewfinder):
<link rel="stylesheet" href="_content/Bonoscan.Components/bonoscan.css" />
Then override any token from your own :root (it always wins, regardless of load order):
The component is self-contained and themed entirely through CSS custom properties
with sensible fallbacks — set them in your app's global CSS (e.g. app.css):
: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. - Effects —
--scanner-blur,--scanner-primary-shadow,--scanner-brand-shadow,--scanner-torch--scanner-torch-glow, and the upload dropzone's--scanner-dropzone-border.
This matters most for the scoped component chrome (the live hint pill, the torch button, the result
bar, the upload dropzone): Blazor scopes those rules to the component and the library uses no ::deep,
so your own CSS can't target them — these tokens are the only way to retheme them, and every colour
and size in them is now a token.
The live viewfinder drawn on the <canvas> (the detected-document quad, framing guide,
guidance banner, progress ring) themes from the same variables — --scanner-accent
(lock-on quad / ring / searching), --scanner-amber (un-framed quad), --scanner-text (hint
text), --scanner-font — 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). Set any of them and the on-camera overlay follows; unset, it keeps the default look. (The canvas reads these once when a scan starts.)
Hosting notes
- Camera needs a secure context (HTTPS or
localhost) in every host. - Blazor Server / Web App — fully supported, including prerendering. The
camera and WASM run in the browser; only tiny status strings cross SignalR during
scanning. On capture, the image bytes (
ScannerResult.Bytes) are delivered chunked viaIJSStreamReference, so they are not subject to SignalR's per-message size cap;ImageUrlis ablob:URL (not a megabyte data URL) for cheap display. 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). - One scanner instance is active at a time.
Notes
Detection is an on-device ONNX model (DocAligner) 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.
|
149 |
|
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 |
.NET 10.0
- Microsoft.AspNetCore.Components.Web (>= 10.0.0)
| Version | Downloads | Last updated |
|---|---|---|
| 0.31.0 | 197 | 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 |