A Razor class library project is essentially a project that includes Razor components and other related files that can be shared and distributed as a NuGet package. To develop a document scanning application that works for both desktop and web, we can package Dynamic Web TWAIN SDK into a Razor class library, which can be utilized in both .NET MAUI Blazor and Blazor WebAssembly projects. In this article, we will provide a tutorial on how to create a Razor class library that incorporates Dynamic Web TWAIN for creating a document scanning app using .NET MAUI and Blazor.
The NuGet Package
Prerequisites
- Visual Studio 2022
- Dynamic Web TWAIN license key
Razor Class Library with Dynamic Web TWAIN
Dynamic Web TWAIN is a JavaScript-based TWAIN SDK that enables you to integrate document scanning functionality into your web application. The advantage of wrapping Dynamic Web TWAIN into a Razor class library is that you can use the same C# code to quickly develop both desktop and web applications instead of writing JavaScript code.
Create a new Razor class library project named RazorWebTWAIN in Visual Studio 2022.
Download the latest version of Dynamic Web TWAIN from here. Install the downloaded file and copy addon, src, dynamsoft.webtwain.config.js, dynamsoft.webtwain.initiate.js, and dynamsoft.webtwain.install.js to the wwwroot/dist folder of the Razor class library project.
Why don't we copy the dist folder? The reason is that the dist folder only contains the Dynamsoft service installers for different platforms. Users will be prompted to install the service when they first use the document scanning app.
The size of the dist folder is about 90MB. To avoid creating an excessively large NuGet package, we recommend that users download the service installer from the unpkg website. To accomplish this, we can modify the dynamsoft.webtwain.install.js file:
Dynamsoft.OnWebTwainNotFoundOnWindowsCallback = function (ProductName, InstallerUrl, bHTML5, bIE, bSafari, bSSL, strIEVersion, bSocketSuccess) {
InstallerUrl = InstallerUrl.replace("_content/RazorWebTWAIN/dist/", "https://unpkg.com/dwt@18.1.1/dist/");
};
Dynamsoft.OnWebTwainNotFoundOnMacCallback = function (ProductName, InstallerUrl, bHTML5, bIE, bSafari, bSSL, strIEVersion, bSocketSuccess) {
InstallerUrl = InstallerUrl.replace("_content/RazorWebTWAIN/dist/", "https://unpkg.com/dwt@18.1.1/dist/");
};
Create a jsInterop.js
file under the wwwroot
folder. In this JavaScript file, export some basic functions that will be interop with C# code.
- Dynamically load JavaScript files and set the license key:
export async function loadDWT(licenseKey) {
await new Promise((resolve, reject) => {
let pdfAddon = document.createElement('script');
pdfAddon.type = 'text/javascript';
pdfAddon.src = '_content/RazorWebTWAIN/dist/addon/dynamsoft.webtwain.addon.pdf.js';
document.head.appendChild(pdfAddon);
let script = document.createElement('script');
script.type = 'text/javascript';
script.src = '_content/RazorWebTWAIN/dist/dynamsoft.webtwain.initiate.js';
script.onload = () => {
let config = document.createElement('script');
config.type = 'text/javascript';
config.src = '_content/RazorWebTWAIN/dist/dynamsoft.webtwain.config.js';
config.onload = () => {
Dynamsoft.DWT.ProductKey = licenseKey;
Dynamsoft.DWT.ResourcesPath = "_content/RazorWebTWAIN/dist/";
resolve();
}
script.onerror = () => {
resolve();
}
document.head.appendChild(config);
}
script.onerror = () => {
}
document.head.appendChild(script);
});
}
- Initialize the image containers for displaying scanned images:
export async function initContainer(containerId, width, height) {
await new Promise((resolve, reject) => {
Dynamsoft.DWT.CreateDWTObjectEx({ "WebTwainId": "container" }, (obj) => {
dwtObject = obj;
dwtObject.Viewer.bind(document.getElementById(containerId));
dwtObject.Viewer.width = width;
dwtObject.Viewer.height = height;
dwtObject.Viewer.show();
resolve();
}, (errorString) => {
console.log(errorString);
resolve();
});
});
}
- Acquire images from document scanners:
export async function acquireImage(jsonString) {
await new Promise((resolve, reject) => {
if (!dwtObject || sourceList.length == 0) {
resolve();
return;
}
if (selectSources) {
dwtObject.SelectDeviceAsync(sourceList[selectSources.selectedIndex]).then(() => {
return dwtObject.OpenSourceAsync()
}).then(() => {
return dwtObject.AcquireImageAsync(JSON.parse(jsonString))
}).then(() => {
if (dwtObject) {
dwtObject.CloseSource();
}
resolve();
}).catch(
(e) => {
console.error(e);
resolve();
}
)
}
else {
resolve();
}
});
}
- Get a list of accessible scanners:
export async function getDevices(selectId) {
await new Promise((resolve, reject) => {
if (!dwtObject) {
resolve();
return;
}
dwtObject.GetDevicesAsync(Dynamsoft.DWT.EnumDWT_DeviceType.TWAINSCANNER | Dynamsoft.DWT.EnumDWT_DeviceType.ESCLSCANNER).then((sources) => {
sourceList = sources;
selectSources = document.getElementById(selectId);
for (let i = 0; i < sources.length; i++) {
let option = document.createElement("option");
option.text = sources[i].displayName;
option.value = i.toString();
selectSources.add(option);
}
resolve();
});
});
}
- Load images from local files. The support image formats are JPEG, PNG, TIFF, and PDF:
export async function loadDocument() {
await new Promise((resolve, reject) => {
if (!dwtObject) {
resolve();
return;
}
dwtObject.Addon.PDF.SetConvertMode(Dynamsoft.DWT.EnumDWT_ConvertMode.CM_RENDERALL);
let ret = dwtObject.LoadImageEx("", Dynamsoft.DWT.EnumDWT_ImageType.IT_ALL);
resolve();
});
}
- Remove a selected image:
export async function removeSelected() {
await new Promise((resolve, reject) => {
if (!dwtObject) {
resolve();
return;
}
dwtObject.RemoveImage(dwtObject.CurrentImageIndexInBuffer);
resolve();
});
}
- Remove all images:
export async function removeAll() {
await new Promise((resolve, reject) => {
if (!dwtObject) {
resolve();
return;
}
dwtObject.RemoveAllImages();
resolve();
});
}
- Save document images to a local file:
export async function save(type, name) {
await new Promise((resolve, reject) => {
if (!dwtObject) {
resolve();
return;
}
if (type == 0) {
if (dwtObject.GetImageBitDepth(dwtObject.CurrentImageIndexInBuffer) == 1)
dwtObject.ConvertToGrayScale(dwtObject.CurrentImageIndexInBuffer);
dwtObject.SaveAsJPEG(name + ".jpg", dwtObject.CurrentImageIndexInBuffer);
}
else if (type == 1)
dwtObject.SaveAllAsMultiPageTIFF(name + ".tiff");
else if (type == 2)
dwtObject.SaveAllAsPDF(name + ".pdf");
resolve();
});
}
Create a DeviceConfiguration.cs
file to define some enum values for scanning:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RazorWebTWAIN
{
public enum PixelType
{
TWPT_BW,
TWPT_GRAY,
TWPT_RGB,
TWPT_PALLETE,
TWPT_CMY,
TWPT_CMYK,
TWPT_YUV,
TWPT_YUVK,
TWPT_CIEXYZ,
TWPT_LAB,
TWPT_SRGB,
TWPT_SCRGB,
TWPT_INFRARED
}
public enum ImageType
{
JPEG,
TIFF,
PDF
}
}
Create a JsInterop.cs
file for invoking JavaScript functions from C# code:
using Microsoft.JSInterop;
using System.Diagnostics;
namespace RazorWebTWAIN
{
public class JsInterop : IAsyncDisposable
{
private readonly Lazy<Task<IJSObjectReference>> moduleTask;
public JsInterop(IJSRuntime jsRuntime)
{
moduleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>(
"import", "./_content/RazorWebTWAIN/jsInterop.js").AsTask());
}
public async ValueTask<string> Prompt(string message)
{
var module = await moduleTask.Value;
return await module.InvokeAsync<string>("showPrompt", message);
}
public async Task LoadDWT(String licenseKey)
{
var module = await moduleTask.Value;
try
{
await module.InvokeVoidAsync("loadDWT", licenseKey);
}
catch (JSException e)
{
Debug.WriteLine($"Error Message: {e.Message}");
}
}
public async ValueTask DisposeAsync()
{
if (moduleTask.IsValueCreated)
{
var module = await moduleTask.Value;
await module.DisposeAsync();
}
}
public async Task AcquireImage(string jsonString)
{
var module = await moduleTask.Value;
try
{
await module.InvokeVoidAsync("acquireImage", jsonString);
}
catch (JSException e)
{
Debug.WriteLine($"Error Message: {e.Message}");
}
}
public async Task InitContainer(string containerId, int width, int height)
{
var module = await moduleTask.Value;
try
{
await module.InvokeVoidAsync("initContainer", containerId, width, height);
}
catch (JSException e)
{
Debug.WriteLine($"Error Message: {e.Message}");
}
}
public async Task GetDevices(string selectId)
{
var module = await moduleTask.Value;
try
{
await module.InvokeVoidAsync("getDevices", selectId);
}
catch (JSException e)
{
Debug.WriteLine($"Error Message: {e.Message}");
}
}
public async Task LoadDocument()
{
var module = await moduleTask.Value;
try
{
await module.InvokeVoidAsync("loadDocument");
}
catch (JSException e)
{
Debug.WriteLine($"Error Message: {e.Message}");
}
}
public async Task RemoveSelected()
{
var module = await moduleTask.Value;
try
{
await module.InvokeVoidAsync("removeSelected");
}
catch (JSException e)
{
Debug.WriteLine($"Error Message: {e.Message}");
}
}
public async Task RemoveAll()
{
var module = await moduleTask.Value;
try
{
await module.InvokeVoidAsync("removeAll");
}
catch (JSException e)
{
Debug.WriteLine($"Error Message: {e.Message}");
}
}
public async Task Save(ImageType type, string name)
{
var module = await moduleTask.Value;
try
{
await module.InvokeVoidAsync("save", type, name);
}
catch (JSException e)
{
Debug.WriteLine($"Error Message: {e.Message}");
}
}
}
}
Build and pack the project to generate a NuGet package.
Building Document Scanning App with .NET MAUI Blazor
- Create a new .NET MAUI Blazor project in Visual Studio 2022.
- Install the
RazorWebTWAIN
NuGet package. -
Import the
RazorWebTWAIN
namespace in the_Imports.razor
file.
@using RazorWebTWAIN
-
Add the following code to the
Index.razor
file. You need to replace the license key with your own.
@page "/" @inject IJSRuntime JSRuntime @using System.Text.Json; <h1> Dynamic Web TWAIN Sample</h1> <select id="sources"></select> <br /> <button @onclick="AcquireImage">Scan Documents</button> <button @onclick="LoadDocument">Load Documents</button> <button @onclick="RemoveSelected">Remove Selected</button> <button @onclick="RemoveAll">Remove All</button> <button @onclick="Save">Download Documents</button> <div id="document-container"></div> @code { JsInterop jsInterop; protected override void OnInitialized() { jsInterop = new JsInterop(JSRuntime); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await jsInterop.LoadDWT("LICENSE-KEY"); await jsInterop.InitContainer("document-container", 640, 640); await jsInterop.GetDevices("sources"); } } public async Task AcquireImage() { var deviceConfiguration = new { IfShowUI = false }; var jsonString = JsonSerializer.Serialize(deviceConfiguration); await jsInterop.AcquireImage(jsonString); } public async Task LoadDocument() { await jsInterop.LoadDocument(); } public async Task RemoveSelected() { await jsInterop.RemoveSelected(); } public async Task RemoveAll() { await jsInterop.RemoveAll(); } public async Task Save() { await jsInterop.Save(ImageType.PDF, "test"); } }
-
Run the app on Windows or macOS.
Windows
macOS
How to Debug .NET MAUI Blazor App on Windows and macOS
- Windows
Use the keyboard shortcut Ctrl+Shift+I
to open browser developer tools.
-
macOS
- Create a
Platforms/MacCatalyst/Entitlements.Debug.plist
file:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>com.apple.security.get-task-allow</key> <true/> </dict> </plist>
- Create a
-
Add the following code to the
.csproj
file:
<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst' and '$(Configuration)' == 'Debug'"> <CodeSignEntitlements>Platforms/MacCatalyst/Entitlements.Debug.plist</CodeSignEntitlements> </PropertyGroup>
Run the project and open the Safari inspector from
Develop > {REMOTE INSPECTION TARGET} > 0.0.0.0
. You can click here for more details.
Blazor WebAssembly Document Scanning App
Once your .NET MAUI Blazor app is ready, you can easily convert it to a Blazor WebAssembly app. The Razor pages are sharable between the two apps.
- Create a new Blazor WebAssembly project in Visual Studio 2022.
- Install the
RazorWebTWAIN
NuGet package and import theRazorWebTWAIN
namespace in the_Imports.razor
file. - Copy the
Index.razor
file from the .NET MAUI Blazor app to the Blazor WebAssembly app. -
Run the web project.