How to Build Desktop and Web Document Scanning App Using .NET MAUI and Blazor
Tags: dotnet, blazor, csharp, webdev
Published: 2023-04-26 08:29:29

A [Razor class library](https://learn.microsoft.com/en-us/aspnet/core/razor-pages/ui-class?view=aspnetcore-7.0&tabs=visual-studio) 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](https://www.dynamsoft.com/web-twain/overview/) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a7qa78tjerxrtpwhyl00.gif)

## The NuGet Package
[RazorWebTWAIN](https://www.nuget.org/packages/RazorWebTWAIN/)

## Prerequisites
- Visual Studio 2022
- [Dynamic Web TWAIN license key](https://www.dynamsoft.com/customer/license/trialLicense?product=dwt)

## 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](https://www.dynamsoft.com/codepool/img/2023/04/razor-class-library.png)

Download the latest version of Dynamic Web TWAIN from [here](https://www.dynamsoft.com/web-twain/downloads/). 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](https://www.dynamsoft.com/codepool/img/2023/04/dynamic-web-twain-resources.png)

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](https://www.dynamsoft.com/codepool/img/2023/04/razor-web-twain-first-time.png)

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](https://unpkg.com/dwt). To accomplish this, we can modify the **dynamsoft.webtwain.install.js** file:

```js
    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:
   ```js
        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:
   ```js
      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:
   ```js
      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:
   ```js
      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**:
   ```js
      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:
   ```js
      export async function removeSelected() {

          await new Promise((resolve, reject) => {
              if (!dwtObject) {
                  resolve();
                  return;
              }
              dwtObject.RemoveImage(dwtObject.CurrentImageIndexInBuffer);
              resolve();
          });
      }
   ```     
- Remove all images:
   ```js
     export async function removeAll() {

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

              dwtObject.RemoveAllImages();
              resolve();
          });
      }
   ```
- Save document images to a local file:
   ```js
      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:
```cs
    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:
```csharp
    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.

![Dynamic Web TWAIN NuGet package](https://www.dynamsoft.com/codepool/img/2023/04/dynamic-web-twain-nuget-package.png)

## 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.
    ```html
    @using RazorWebTWAIN
    ```
4. Add the following code to the `Index.razor` file. You need to replace the license key with your own.
    ```cs
    @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](https://www.dynamsoft.com/codepool/img/2023/04/dotnet-maui-windows-document-scanner.png)

    **macOS**

    ![.NET MAUI document scanning app on macOS](https://www.dynamsoft.com/codepool/img/2023/04/dotnet-maui-macos-document-scan.png)

### 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
      <?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>
      ```
  2. Add the following code to the `.csproj` file:
      ```xml
      <PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst' and '$(Configuration)' == 'Debug'">
          <CodeSignEntitlements>Platforms/MacCatalyst/Entitlements.Debug.plist</CodeSignEntitlements>
      </PropertyGroup>
      ```
  3. Run the project and open the **Safari** inspector from `Develop > {REMOTE INSPECTION TARGET} > 0.0.0.0`. You can click [here](https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/developer-tools?view=aspnetcore-7.0&pivots=macos) 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](https://www.dynamsoft.com/codepool/img/2023/04/dotnet-blazor-web-document-scanner.png)

## Source Code
[https://github.com/yushulx/Razor-Web-TWAIN](https://github.com/yushulx/Razor-Web-TWAIN)