How to Scan Documents & Images from Blazor
Product JSPrintManager for Blazor Published 07/12/2021 Updated 06/18/2024 Author Neodynamic
Overview
JSPrintManager for Blazor enables any Razor web page to scan documents and images through any TWAIN/SANE compatible devices available at the client side by writing pure and simple Blazor code!
In this walkthrough, you'll learn how to scan documents and images from Blazor through any TWAIN/SANE client devices without displaying any system dialogs at all. You'll be able to specify the target scanner device, the resolution and output quality (dpi) and desired pixel mode (Grayscale or full Color) as well as the output image format (JPG/JPEG or PNG). Advanced features like Duplex and ADF are available for Windows Clients only! This solution works with any popular browser like Chrome, Firefox, IE/Edge & Safari on Windows, Linux, Raspberry Pi and Mac systems!
Client System Requirements
- Windows: None!
- macOS/OSX: None!
- Linux: Scanner Access Now Easy (SANE) API
Follow up these steps
- Be sure you install in your dev machine JSPrintManager (JSPM) (Available for Windows, Linux, Raspberry Pi & Mac)
This small app must be installed on each client that will scan from your website!READ CAREFULLY - Windows 64-bit Clients
If you install the JSPrintManager client app in a Windows 64-bit edition through our universal installer or the one specific for Win64 and your scanner does not provide a 64-bit TWAIN driver or it supports the TWAIN 1.x specification only, then it's possible that no scanners will be detected! If this is your case, then do the following:
- Uninstall current JSPrintManager app for 64-bit
- Download and install JSPrintManager app for 32-bit instead. The JSPrintManager app for 32-bit will detect not only those missing TWAIN devices but also any other WIA devices that might be available in your system.
- In your Blazor project...
- Add a NuGet reference to the JSPrintManager Razor Component
- Add the JSPrintManager service...
- Add the following statement at the top of your
Startup
file
using Neodynamic.Blazor;
- For Blazor Server
Add the following line in theStartup's ConfigureServices
method
services.AddJSPrintManager();
- For Blazor WebAssembly
Add the following line in theProgram's Main
method
builder.Services.AddJSPrintManager();
- For Blazor Server
- Add the following statement at the top of your
- Add the following statement in the
_Imports.razor
file
@using Neodynamic.Blazor
- IMPORTANT! For Blazor Server Only: SignalR message limit is 32KB by default and scanning images that will be serialized to Base64 string will require modifying that limitation. In this sample we have removed it by setting the
MaximumReceiveMessageSize
prop tonull
in theStartup's ConfigureServices
method
services.AddServerSideBlazor().AddHubOptions(opt => {opt.MaximumReceiveMessageSize = null;});
- Add a new Razor Page and copy/paste the following code. Please read the source code comments to understand the printing logic!
@page "/" @inject JSPrintManager JSPrintManager @using System @using System.IO <div> <strong>JSPM </strong><span>WebSocket Status </span> @if (JSPrintManager.Status == JSPMWSStatus.Open) { <span class="badge badge-success"> <i class="fa fa-check" /> Open </span> } else if (JSPrintManager.Status == JSPMWSStatus.Closed) { <span class="badge badge-danger"> <i class="fa fa-exclamation-circle" /> Closed! </span> <div> <strong>JSPrintManager (JSPM) App</strong> is not installed or not running! <a href="https://neodynamic.com/downloads/jspm" target="_blank">Download JSPM Client App...</a> </div> } else if (JSPrintManager.Status == JSPMWSStatus.Blocked) { <span class="badge badge-warning"> <i class="fa fa-times-circle" /> This Website is Blocked! </span> } else if (JSPrintManager.Status == JSPMWSStatus.WaitingForUserResponse) { <span class="badge badge-warning"> <i class="fa fa-user-circle" /> Waiting for user response... </span> } </div> @if (JSPrintManager.Status == JSPMWSStatus.Open) { <div class="row"> <div class="col-md-12"> <h2 class="text-center"> <i class="fa fa-crosshairs" /> Scan Docs & Images </h2> <hr /> </div> </div> @if (NumOfScannerDevices == -1) { <div class="row"> <div class="col-md-12"> <div class="text-center"> <div class="spinner-border text-info" role="status"> <span class="sr-only">Please wait...</span> </div> <strong><em>Getting scanner devices...</em></strong> </div> </div> </div> } else if (NumOfScannerDevices == 0) { <div class="row"> <div class="col-md-12"> <div class="text-center"> <div class="alert alert-danger">No scanner devices were detected on this system.</div> </div> </div> </div> } else { <div class="row"> <div class="col-md-12"> <EditForm Model="@MyCSJ"> <div class="form-group"> <div class="row"> <div class="col-md-3"> <label>Scanner:</label> <InputSelect @bind-Value="MyCSJ.ScannerName" class="form-control form-control-sm"> <option>Select a device...</option> @foreach (var s in JSPrintManager.Scanners) { <option value="@s">@s</option> } </InputSelect> </div> <div class="col-md-2"> <label>Resolution (DPI):</label> <InputNumber @bind-Value="MyCSJ.Resolution" class="form-control form-control-sm" /> </div> <div class="col-md-2"> <label>Pixel Mode:</label> <InputSelect @bind-Value="MyCSJ.PixelMode" class="form-control form-control-sm"> @foreach (var pm in Enum.GetValues(typeof(PixelMode))) { <option value="@pm">@pm</option> } </InputSelect> </div> <div class="col-md-2"> <label>Image Format:</label> <InputSelect @bind-Value="MyCSJ.ImageFormat" class="form-control form-control-sm"> @foreach (var imgFmt in Enum.GetValues(typeof(ScannerImageFormatOutput))) { <option value="@imgFmt">@imgFmt</option> } </InputSelect> </div> <div class="col-md-3"> <span class="badge badge-info">Windows Only</span> <br /> <label> Enable Duplex <InputCheckbox @bind-Value="MyCSJ.EnableDuplex" /> </label> <br /> <label> Enable Feeder (ADF) <InputCheckbox @bind-Value="MyCSJ.EnableFeeder" /> </label> <div class="input-group input-group-sm mb-3"> <div class="input-group-prepend"> <span class="input-group-text" id="basic-addon1">Feeder Count:</span> </div> <InputNumber @bind-Value="MyCSJ.FeederCount" class="form-control" aria-label="Feeder Count" aria-describedby="basic-addon1" /> </div> </div> </div> <div class="row"> <div class="col-md-12"> <br /> <div class="text-center"> <button class="btn btn-success btn-lg" @onclick="DoScanning"> <i class="fa fa-crosshairs" /> Scan Now... </button> </div> </div> </div> <br /><br /> @if (ScanningState == 1) { <div class="row"> <div class="col-md-12"> <div class="text-center"> <div class="spinner-border text-info" role="status"> <span class="sr-only">Please wait...</span> </div> <br /> <strong>Scanning...</strong> </div> </div> </div> } else if (ScanningState == 2) { <div class="row"> <div class="col-md-12"> <div class="text-center"> <div class="alert alert-danger"><strong>ERROR:</strong> @LastError</div> </div> </div> </div> } else if (ScanningState == 0 && ScannedImages.Count > 0) { <div class="row"> <div class="col-md-12"> <div class="alert alert-secondary"> <div class="row"> <div class="col-md-4"> <button type="button" class="btn btn-info" @onclick="PrevImage"><i class="oi oi-chevron-left"></i></button> </div> <div class="col-md-4 text-center"> <h4>Scan result: @(currentImageIndex + 1) of @(ScannedImages.Count)</h4> </div> <div class="col-md-4"> <button type="button" class="btn btn-info pull-right" @onclick="NextImage"><i class="oi oi-chevron-right"></i></button> </div> </div> <hr /> <div class="row"> <div class="col-md-12 text-center"> <img src="@(ScannedImages[currentImageIndex])" style="width: @(Math.Round(96.0 / (double)MyCSJ.Resolution * 100.0))%; height: auto" /> </div> </div> </div> </div> </div> } </div> </EditForm> </div> </div> } } @code { protected override void OnAfterRender(bool firstRender) { if (firstRender) { if (JSPrintManager.Scanners != null && JSPrintManager.Scanners.Length > 0) NumOfScannerDevices = JSPrintManager.Scanners.Length; // Handle OnGetScanners event... JSPrintManager.OnGetScanners += () => { NumOfScannerDevices = 0; if (JSPrintManager.Scanners != null && JSPrintManager.Scanners.Length > 0) NumOfScannerDevices = JSPrintManager.Scanners.Length; StateHasChanged(); }; // Handle OnClientScanJobStatusChanged event JSPrintManager.OnClientScanJobStatusChanged += () => { // filter scan cache by the job id var ScanningResult = JSPrintManager.ClientScanJobStatusCache.Where(jobStatus => jobStatus.Id == MyCSJ.Id).LastOrDefault<ScanJobStatus>(); if (ScanningResult != null) { if (ScanningResult.IsError) { ScanningState = 2; LastError = ScanningResult.Data; } else if (ScanningResult.IsLast) { ScanningState = 0; } else { // add scanned image to the local buffer ScannedImages.Add(ScanningResult.Data); } StateHasChanged(); } }; // Handle OnStatusChanged event to detect any WSS status change JSPrintManager.OnStatusChanged += () => { StateHasChanged(); // Status = Open means that JSPM Client App is up and running! if (JSPrintManager.Status == JSPMWSStatus.Open) { //Try getting scanner devices... JSPrintManager.TryGetScanners(); } }; // Start WebSocket comm JSPrintManager.Start(); } base.OnAfterRender(firstRender); } // A ClientScanJob obj private ClientScanJob MyCSJ { get; set; } = new(); private void DoScanning() { if (string.IsNullOrWhiteSpace(MyCSJ.ScannerName)) return; // Clean Scan Job Status cache... JSPrintManager.ClientScanJobStatusCache.Clear(); // Clean scanned images cache... ScannedImages.Clear(); // Set scanning state ScanningState = 1; StateHasChanged(); // gen unique id for tracking job status MyCSJ.GenerateUniqueId(); // Send job to the client! JSPrintManager.SendClientScanJob(MyCSJ); } private int NumOfScannerDevices = -1; private int ScanningState = 0; // 0 = finished, 1 = scanning, 2 = error private int currentImageIndex = 0; private List<string> ScannedImages = new List<string>(); private void NextImage() { if (currentImageIndex < ScannedImages.Count - 1) { currentImageIndex++; StateHasChanged(); } } private void PrevImage() { if (currentImageIndex > 0) { currentImageIndex--; StateHasChanged(); } } private string LastError = ""; }
- That's it! Run your website and test it. Click on Scan Now... to start the scanning process without any system dialog.