DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

How to Build Desktop and Web Document Scanning App Using .NET MAUI and Blazor

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.

.NET MAUI Blazor document scanning

The NuGet Package

RazorWebTWAIN

Prerequisites

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.

Razor class library project

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.

Dynamic Web TWAIN folder

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.

Dynamic Web TWAIN first time

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/");
    };
Enter fullscreen mode Exit fullscreen mode

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);
            });
        }
Enter fullscreen mode Exit fullscreen mode
  • 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();
              });
          });

      }
Enter fullscreen mode Exit fullscreen mode
  • 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();
              }
          });
      }
Enter fullscreen mode Exit fullscreen mode
  • 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();
              });
          });
      }
Enter fullscreen mode Exit fullscreen mode
  • 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();
          });
      }
Enter fullscreen mode Exit fullscreen mode
  • Remove a selected image:
      export async function removeSelected() {

          await new Promise((resolve, reject) => {
              if (!dwtObject) {
                  resolve();
                  return;
              }
              dwtObject.RemoveImage(dwtObject.CurrentImageIndexInBuffer);
              resolve();
          });
      }
Enter fullscreen mode Exit fullscreen mode
  • Remove all images:
     export async function removeAll() {

          await new Promise((resolve, reject) => {
              if (!dwtObject) {
                  resolve();
                  return;
              }

              dwtObject.RemoveAllImages();
              resolve();
          });
      }
Enter fullscreen mode Exit fullscreen mode
  • 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();
          });
      }
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
Enter fullscreen mode Exit fullscreen mode

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}");
                }
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

Build and pack the project to generate a NuGet package.

Dynamic Web TWAIN NuGet package

Building Document Scanning App with .NET MAUI Blazor

  1. Create a new .NET MAUI Blazor project in Visual Studio 2022.
  2. Install the RazorWebTWAIN NuGet package.
  3. Import the RazorWebTWAIN namespace in the _Imports.razor file.

    @using RazorWebTWAIN
    
  4. 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");
        }
    }
    
  5. Run the app on Windows or macOS.

    Windows

    .NET MAUI document scanning app on Windows

    macOS

    .NET MAUI document scanning app on 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

    1. 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>
    
  1. 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>
    
  2. 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.

  1. Create a new Blazor WebAssembly project in Visual Studio 2022.
  2. Install the RazorWebTWAIN NuGet package and import the RazorWebTWAIN namespace in the _Imports.razor file.
  3. Copy the Index.razor file from the .NET MAUI Blazor app to the Blazor WebAssembly app.
  4. Run the web project.

    Blazor WebAssembly document scanning app

Source Code

https://github.com/yushulx/Razor-Web-TWAIN

Top comments (0)