How to Build a Web Document Scanner with Blazor

Blazor is a framework for building interactive client-side web UI with .NET1. Users can create interactive UIs in C# instead of JavaScript. Server-side and client-side app logic can both be written in .NET.

In the previous article, we’ve created a barcode reader with Blazor using Dynamsoft Barcode Reader. In this article, we are going to build a web document scanner using Dynamic Web TWAIN (DWT). Dynamic Web TWAIN is a JavaScript library designed for document scanning and document management. We can easily integrate it in a Blazor app.

Getting Started With Dynamic Web TWAIN

Build a Web Document Scanner with Blazor

There are two kinds of Blazor applications: Blazor WebAssembly and Blazor Server.

Blazor WebAssembly applications run purely on the client side. C# code files and Razor files are compiled into .NET assemblies. The assemblies and the .NET runtime are downloaded to the browser.

Blazor Server applications have a real-time connection (SignalR) to the server. The server executes the app’s C# code and updates the UI on the client side.

You can learn about the pros and cons of Blazor Server and Blazor WebAssembly at Blazor University.

In this article, we are going to create a WebAssembly version as well as a Server version.

Web Document Scanner with Blazor WebAssembly

  1. Open Visual Studio, create a Blazor WebAssembly project. Remember to select .NET 5.

    New Blazor project

  2. Download and install Dynamic Web TWAIN. Copy the Resources folder to the wwwroot folder.

  3. Modify dynamsoft.webtwain.config.js in the Resources folder. Set up ProductKey and change Dynamsoft.DWT.AutoLoad to false as we want to load DWT manually.

  4. Modify wwwroot/index.html.

    Include DWT’s JavaScript files in head:

     <script src="Resources/dynamsoft.webtwain.initiate.js"></script>
     <script src="Resources/dynamsoft.webtwain.config.js"></script>
    

    Add the following JavaScript for interoperation with C# code in body (learn more about JavaScript interoperability here):

     <script type="text/javascript">
         var DWObject = null;
    
         function CreateDWT() {
             var height = 580;
             var width = 500;
    
             if (Dynamsoft.Lib.env.bMobile) {
                 height = 350;
                 width = 270;
             }
    
             Dynamsoft.DWT.CreateDWTObjectEx({
                 WebTwainId: 'dwtcontrol'
             },
                 function (obj) {
                     DWObject = obj;
                     DWObject.Viewer.bind(document.getElementById('dwtcontrolContainer'));
                     DWObject.Viewer.height = height;
                     DWObject.Viewer.width = width;
                     DWObject.Viewer.show();
                 },
                 function (err) {
                     console.log(err);
                 }
             );
         }
    
         function Scan() {
             if (DWObject) {
                 DWObject.SelectSource(function () {
                     DWObject.OpenSource();
                     DWObject.AcquireImage();
                 },
                     function () {
                         console.log("SelectSource failed!");
                     }
                 );
             }
         }
    
         function LoadImage() {
             if (DWObject) {
                 DWObject.LoadImageEx('', 5,
                     function () {
                         console.log('success');
                     },
                     function (errCode, error) {
                         alert(error);
                     }
                 );
             }
         }
    
         function Save() {
             DWObject.IfShowFileDialog = true;
             // The path is selected in the dialog, therefore we only need the file name
             DWObject.SaveAllAsPDF("Sample.pdf",
                 function () {
                     console.log('Successful!');
                 },
                 function (errCode, errString) {
                     console.log(errString);
                 }
             );
         }
    
         function isDesktop() {
             if (Dynamsoft.Lib.env.bMobile) {
                 return false;
             } else {
                 return true;
             }
         }
    
     </script>
    
  5. Add a new Razor component Scanner.razor with the following code:

     @inject IJSRuntime JS
     @page "/scanner"
    
     <h1>Scanner</h1>
    
     @if (isDesktop)
     {
     <button class="btn btn-primary" @onclick="Scan">Scan</button>
     }
     <button class="btn btn-primary" @onclick="LoadImage">Load Image</button>
     <button class="btn btn-primary" @onclick="Save">Save</button>
    
     <div id="dwtcontrolContainer"></div>
    
     @code{
         private Boolean isDesktop;
         protected override async void OnInitialized()
         {
             await CreateDWT();
             isDesktop = await JS.InvokeAsync<Boolean>("isDesktop");
             StateHasChanged();
         }
    
         private async Task CreateDWT()
         {
             await JS.InvokeVoidAsync("CreateDWT");
         }
    
         private async Task Scan()
         {
             await JS.InvokeVoidAsync("Scan");
         }
    
         private async Task LoadImage()
         {
             await JS.InvokeVoidAsync("LoadImage");
         }
    
         private async Task Save()
         {
             await JS.InvokeVoidAsync("Save");
         }
     }
    

    Add the component to Index.razor:

     @page "/"
    
     <h1>Hello, world!</h1>
    
     Welcome to your new app.
    
     <Scanner/>
    
  6. Run the app and see the result. While the app is running, any changes made to the source code will be detected and the app will be updated.

    App

    The app can capture documents from a scanner as well as load local images. It can generate a PDF output to save the result.

  7. We can also run the app using the following command:

     dotnet watch run
    

    We can access the app on our mobile phones if the phone and the server are on the same network.

    We may also have to modify Properties/launchSettings.json so that the app is accessible not only in the local network.

    Change this line:

     "applicationUrl": "https://localhost:5001;http://localhost:5000",
    

    To this:

     "applicationUrl": "https://+:5001;http://+:5000",
    

    Mobile

    Since mobile phones cannot directly control scanners, the scan button is not loaded. Whether the device is mobile is detected using Dynamsoft.Lib.env.bMobile.

Web Document Scanner with Blazor Server

Creating a Blazor server version is much the same.

The difference is that there is no Index.html and instead, there is _Host.cshtml. We need to add the DWT JavaScript to this file.

The web page is connected to the server. If it loses connection to the server, a modal will appear and the page will not be functional.

Reconnect

JavaScript Isolation

.NET 5 has a new feature called JavaScript isolation.

JS isolation provides the following benefits2:

  • Imported JS no longer pollutes the global namespace.
  • Consumers of a library and components aren’t required to import the related JS.

PS: Using JavaScript isolation may cause the web page not functional on old browsers.

Let’s use JavaScript isolation in the WebAssembly project we just made.

  1. Create a new JS file named DWT.js under wwwroot/js with the following content:

     var DWObject = null;
    
     export function CreateDWT() {
         var height = 580;
         var width = 500;
    
         if (Dynamsoft.Lib.env.bMobile) {
             height = 350;
             width = 270;
         }
    
         Dynamsoft.DWT.CreateDWTObjectEx({
             WebTwainId: 'dwtcontrol'
         },
             function (obj) {
                 DWObject = obj;
                 DWObject.Viewer.bind(document.getElementById('dwtcontrolContainer'));
                 DWObject.Viewer.height = height;
                 DWObject.Viewer.width = width;
                 DWObject.Viewer.show();
             },
             function (err) {
                 console.log(err);
             }
         );
     }
    
     export function Scan() {
         if (DWObject) {
             DWObject.SelectSource(function () {
                 DWObject.OpenSource();
                 DWObject.AcquireImage();
             },
                 function () {
                     console.log("SelectSource failed!");
                 }
             );
         }
     }
    
     export function LoadImage() {
         if (DWObject) {
             DWObject.LoadImageEx('', 5,
                 function () {
                     console.log('success');
                 },
                 function (errCode, error) {
                     alert(error);
                 }
             );
         }
     }
    
     export function Save() {
         DWObject.IfShowFileDialog = true;
         // The path is selected in the dialog, therefore we only need the file name
         DWObject.SaveAllAsPDF("Sample.pdf",
             function () {
                 console.log('Successful!');
             },
             function (errCode, errString) {
                 console.log(errString);
             }
         );
     }
    
     export function isDesktop() {
         if (Dynamsoft.Lib.env.bMobile) {
             return false;
         } else {
             return true;
         }
     }
    

    The JavaScript we added in the wwwroot/index.html can now be removed.

  2. Modify Scanner.razor to use the JS file.

     @page "/scanner"
     @implements IAsyncDisposable
     @inject IJSRuntime JS
    
     <h1>Scanner</h1>
    
     @if (isDesktop)
     {
     <button class="btn btn-primary" @onclick="Scan">Scan</button>
     }
     <button class="btn btn-primary" @onclick="LoadImage">Load Image</button>
     <button class="btn btn-primary" @onclick="Save">Save</button>
    
     <div id="dwtcontrolContainer"></div>
    
    
    
     @code{ 
         private Boolean isDesktop;
         private IJSObjectReference module;
         protected override async void OnAfterRender(bool firstRender)
         {
             if (firstRender)
             {
                 module = await JS.InvokeAsync<IJSObjectReference>("import", "./js/DWT.js");
                 isDesktop = await IsDesktop();
                 StateHasChanged();
                 await CreateDWT();
             }
         }
            
         private async Task CreateDWT()
             {
                 await module.InvokeVoidAsync("CreateDWT");
         }
    
         private async Task Scan()
         {
             await module.InvokeVoidAsync("Scan");
         }
    
         private async Task LoadImage()
         {
             await module.InvokeVoidAsync("LoadImage");
         }
    
         private async Task Save()
         {
             await module.InvokeVoidAsync("Save");
         }
    
         private async Task<Boolean> IsDesktop()
         {
             return await module.InvokeAsync<Boolean>("isDesktop");
         }
    
         async ValueTask IAsyncDisposable.DisposeAsync()
         {
             await module.DisposeAsync();
         }
     }
    

Create a C# Wrapper

We can take a step further by creating a wrapper to make the usage easier.

  1. Create a C# class file called DWT.

  2. Add the following code to the class file:

     public class DWT
     {
         private IJSObjectReference module;
         [Inject]
         IJSRuntime JS { get; set; }
         private DWT()
         {
         }
         public static async Task<DWT> CreateAsync(IJSRuntime JS)
         {
             DWT dwt = new DWT();
             dwt.JS = JS;
             await dwt.LoadJSAsync();
             return dwt;
         }
    
         private async Task LoadJSAsync()
         {
             module = await JS.InvokeAsync<IJSObjectReference>("import", "./js/DWT.js");
         }
    
         public async Task CreateDWT()
         {
             await module.InvokeVoidAsync("CreateDWT");
         }
    
         public async Task Scan()
         {
             await module.InvokeVoidAsync("Scan");
         }
    
         public async Task LoadImage()
         {
             await module.InvokeVoidAsync("LoadImage");
         }
    
         public async Task Save()
         {
             await module.InvokeVoidAsync("Save");
         }
    
         public async Task<Boolean> IsDesktop()
         {
             return await module.InvokeAsync<Boolean>("isDesktop");
         }
    
         protected virtual async ValueTask DisposeAsync()
         {
             await module.DisposeAsync();
         }
     }
    

    Since the constructor cannot be asynchronous, we create a CreateAsync method to create instances3.

  3. Update Scanner.razor to use the DWT class:

     @inject IJSRuntime JS
     @page "/scanner"
    
     <h1>Scanner</h1>
    
     @if (isDesktop)
     {
     <button class="btn btn-primary" @onclick="Scan">Scan</button>}
     <button class="btn btn-primary" @onclick="LoadImage">Load Image</button>
     <button class="btn btn-primary" @onclick="Save">Save</button>
    
     <div id="dwtcontrolContainer"></div>
    
     @code{ 
         private Boolean isDesktop;
         private DWT dwt;
         protected override async void OnAfterRender(bool firstRender)
         {
             if (firstRender)
             {
                 dwt = await DWT.CreateAsync(JS);
                 isDesktop = await dwt.IsDesktop();
                 StateHasChanged();
                 await dwt.CreateDWT();
             }
         }
    
         private async Task Scan()
         {
             await dwt.Scan();
         }
    
         private async Task LoadImage()
         {
             await dwt.LoadImage();
         }
    
         private async Task Save()
         {
             await dwt.Save();
         }
     }
    

Source Code

https://github.com/xulihang/BlazorTWAIN

References