How to Implement TWAIN Document Scanning in a .NET Blazor WebAssembly App

Need to scan paper documents directly from a .NET Blazor WebAssembly app? Dynamic Web TWAIN is a JavaScript-based document scanning SDK that supports TWAIN, WIA, SANE, ICA, and eSCL scanner protocols across Windows, macOS, and Linux. This tutorial shows you how to connect a Blazor WebAssembly app to physical scanners (HP, Canon, Epson, and more) via C# JavaScript Interop, scan pages, and export them to PDF — all in about 10 minutes.

What you’ll build: A .NET Blazor WebAssembly app that connects to physical scanners (HP, Canon, Epson, and more) via TWAIN, WIA, SANE, ICA, and eSCL protocols using Dynamic Web TWAIN, with scan-to-PDF export — completable in about 10 minutes using the full source code on GitHub.

Key Takeaways

  • Dynamic Web TWAIN enables Blazor WebAssembly apps to acquire images from TWAIN, WIA, SANE, ICA, and eSCL-compatible scanners on Windows, macOS, and Linux.
  • The SDK communicates with scanner hardware via a locally installed service; Blazor calls it through JavaScript Interop (IJSRuntime).
  • Scanner configuration (resolution, duplex, pixel type) is passed as a serialized JSON object from C# to JavaScript, keeping the Blazor component clean and protocol-agnostic.
  • Scanned documents can be exported directly to PDF, TIFF, or JPEG without a browser download prompt, making it suitable for enterprise document workflows.

Common Developer Questions

  • How do I implement TWAIN document scanning in a .NET Blazor WebAssembly app?
  • How do I call Dynamic Web TWAIN from C# using JavaScript Interop in Blazor?
  • Why does the Dynamic Web TWAIN Service fail to install from a CDN?
  • Can I scan to PDF from a Blazor app using TWAIN, WIA, SANE, or eSCL scanners?

Blazor TWAIN Document Scanning Demo

Try the Online Demo

https://yushulx.me/blazor-barcode-mrz-document-scanner/

Prerequisites

  • Dynamic Web TWAIN

    This package is essential for integrating scanning capabilities into your web application. It includes:

    • Cross-Platform JavaScript Libraries: Compatible with Windows, macOS, and Linux.
    • Platform-Specific Service Installers: Facilitates communication between the web app and scanner hardware.

      Dynamic Web TWAIN Service installer

    You can install Dynamic Web TWAIN via npm.

    Important Note:

    • Installation Prompt: When users access your document scanning app for the first time, they will be prompted to install the Dynamic Web TWAIN Service.

      Dynamic Web TWAIN Service installation prompt

    • CDN Limitations: The service installer exceeds jsDelivr’s size limits, leading to failed downloads if attempted through this method. Instead, host the service installer on your server or use the unpkg website to download the installer.

  • Dynamsoft Capture Vision Trial License

    To utilize the full capabilities of the Dynamic Web TWAIN SDK, obtain a 30-day free trial license.

Step 1: Set Up Dynamic Web TWAIN in Your Blazor WebAssembly Project

Integrating Dynamic Web TWAIN into your Blazor WebAssembly application enables robust document scanning capabilities across multiple platforms. Follow the steps below to set up Dynamic Web TWAIN effectively:

  1. Create a New Blazor WebAssembly Project: Scaffold a new Blazor WebAssembly project using Visual Studio.
  2. Download Dynamic Web TWAIN: Install the Dynamic Web TWAIN package via npm.

     npm install dwt
    
  3. Integrate Dynamic Web TWAIN into the Blazor Project: Copy the dist folder from the node_modules/dwt directory to the wwwroot folder in the Blazor project, and then include the JavaScript file in the index.html file.

     <script src="dist/dynamsoft.webtwain.min.js"></script>
    
  4. Create a JavaScript File for C# Interop: To facilitate communication between Blazor (C#) and JavaScript, create a jsInterop.js file in the wwwroot directory and include it in your index.html:

     <script src="dist/dynamsoft.webtwain.min.js"></script>
     <script src="jsInterop.js"></script>
    
  5. Configure Resource Paths and License Key: In the wwwroot/jsInterop.js file, define a setLicense function to configure the resource path and license key for Dynamic Web TWAIN:
     window.jsFunctions = {
         setLicense: async function setLicense(license) {
             if (isInitialized) return true;
            
             try {
                 Dynamsoft.DWT.ResourcesPath = "dist";
                 Dynamsoft.DWT.ProductKey = license;
            
                 isInitialized = true;
             } catch (e) {
                 alert(e);
                 return false;
             }
            
             return true;
         },
         ...
     };
    
  6. Create an EditForm for License Input: In Pages/Home.razor, add the following HTML and C# code to allow users to activate the SDK with a valid license key:

     @page "/"
     @inject IJSRuntime JSRuntime
        
     <PageTitle>Home</PageTitle>
        
     <p>Click <a href="https://www.dynamsoft.com/customer/license/trialLicense/?product=dcv&package=cross-platform" target="_blank">here</a> to obtain a Dynamsoft Capture Vision Trial License.</p>
        
     <EditForm Model="@this">
         <InputText @bind-Value="LicenseKey" placeholder="Enter your license key" />
         <button type="button" class="btn btn-primary" @onclick="SetLicenseKey">Activate the SDK</button>
     </EditForm>
        
     @code {
         Boolean initialized = false;
        
         private string LicenseKey = "LICENSE-KEY";
        
         private async Task SetLicenseKey()
         {
             initialized = await JSRuntime.InvokeAsync<Boolean>("jsFunctions.setLicense", LicenseKey);
             StateHasChanged();
         }
     }
    

Step 2: Implement TWAIN Document Scanning in Blazor WebAssembly

The following steps outline how to implement document scanning functionality in a Blazor WebAssembly application using Dynamic Web TWAIN:

  1. Create a Razor Component: Add a new Razor component named WebTwain.razor to the Pages directory and reference it in the Layout/NavMenu.razor file.

     <div class="nav-item px-3">
         <NavLink class="nav-link" href="webtwain">
             <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Web TWAIN
         </NavLink>
     </div>
    
  2. Initialize a Document Container: In the WebTwain.razor file, create a container to display scanned documents:

     @page "/webtwain"
     @inject IJSRuntime JSRuntime
     @using System.Text.Json;
        
     <div id="@containerId"></div>
        
     @code {
         private string containerId = "document-container";
         private int containerWidth = 800;
         private int containerHeight = 800;
        
         protected override async Task OnAfterRenderAsync(bool firstRender)
         {
             if (firstRender)
             {
                 await JSRuntime.InvokeVoidAsync("jsFunctions.initWebTwain", containerId, containerWidth, containerHeight);
                 ...
             }
         }
     }
    

    Explanation:

    • @page "/webtwain" directive defines the route for this component.
    • initWebTwain is a JavaScript function that initializes the Web TWAIN container with a specified ID and dimensions.

        initWebTwain: async function (containerId, width, height) {
            if (!isInitialized) {
                alert("Please set the license first.");
                return;
            }
              
            try {
                await new Promise((resolve, reject) => {
                    Dynamsoft.DWT.CreateDWTObjectEx({ "WebTwainId": containerId }, (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();
                    });
                });
            }
            catch (e) {
                alert(e);
            }
        },
      
  3. List Available Scanners: Add a dropdown to list the available scanners connected to the computer:

     <select id="@selectId"></select>
    
     @code {
         protected override async Task OnAfterRenderAsync(bool firstRender)
         {
             if (firstRender)
             {
                 await JSRuntime.InvokeVoidAsync("jsFunctions.initWebTwain", containerId, containerWidth, containerHeight);
                 await JSRuntime.InvokeVoidAsync("jsFunctions.getDevices", selectId);
             }
         }
     }
    

    Explanation:

    • getDevices is a JavaScript function that retrieves the available scanners and populates the select element with the scanner names.

        getDevices: async function (selectId) {
            await new Promise((resolve, reject) => {
                if (!dwtObject) {
                    resolve();
                    return;
                }
              
                dwtObject.GetDevicesAsync(Dynamsoft.DWT.EnumDWT_DeviceType.TWAINSCANNER | Dynamsoft.DWT.EnumDWT_DeviceType.TWAINX64SCANNER | 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();
                });
            });
        },
      
  4. Scan Documents from a Selected Scanner: Add a button to initiate the document scanning process:

     <button @onclick="AcquireImage">Scan Documents</button>
        
     @code {
         public async Task AcquireImage()
         {
             var deviceConfiguration = new
             {
                 IfShowUI = false,
                 PixelType = Utils.PixelType.TWPT_RGB,
                 Resolution = 300,
                 IfFeederEnabled = true,
                 IfDuplexEnabled = false,
                 IfDisableSourceAfterAcquire = true,
                 IfGetImageInfo = true,
                 IfGetExtImageInfo = true,
                 extendedImageInfoQueryLevel = 0
             };
             var jsonString = JsonSerializer.Serialize(deviceConfiguration);
            
             await JSRuntime.InvokeVoidAsync("jsFunctions.acquireImage", jsonString);
            
         }
     }
    

    Explanation:

    • deviceConfiguration is a C# object that specifies the scanning settings.
    • acquireImage is a JavaScript function that triggers the document scanning process with the specified settings.

        acquireImage: async function (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();
                }
            });
        },
      
  5. Remove a Selected Document: Add a button to remove a selected document from the container:

     <button @onclick="RemoveSelected">Remove Selected</button>
        
     @code {
         public async Task RemoveSelected()
         {
             await JSRuntime.InvokeVoidAsync("jsFunctions.removeSelected");
         }
     }
    

    Explanation:

    • removeSelected is a JavaScript function that removes the selected document from the container.

        removeSelected: async function () {
            await new Promise((resolve, reject) => {
                if (!dwtObject) {
                    resolve();
                    return;
                }
              
                dwtObject.RemoveImage(dwtObject.CurrentImageIndexInBuffer);
                resolve();
            });
              
        },
      
  6. Save Documents to a PDF File: Add a button that allows users to save the scanned documents as JPEG, TIFF, or PDF format. The file will be generated directly by the Dynamic Web TWAIN Service and saved to the local file system without triggering a browser download prompt.

     <button @onclick="Save">Save Documents to PDF</button>
        
     @code {
         public async Task Save()
         {
             await JSRuntime.InvokeVoidAsync("jsFunctions.save", Utils.ImageType.PDF, "test");
         }
     }
    

    Explanation:

    • save is a JavaScript function that requests the Dynamic Web TWAIN Service to save the scanned documents as a PDF file.

        save: async function (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");
                }
              
                alert("Save successfully!");
                resolve();
            });
        }
      
  7. Run and Test the Application: Launch the Blazor app and test the scanning functionality. Ensure that a compatible scanner (HP, Canon, Epson, etc.) is connected.

    Blazor WebAssembly document scanning app

Common Issues & Edge Cases

  • Dynamic Web TWAIN Service not detected: If the browser cannot connect to the local service, users see an installation prompt. This is expected on first run. Make sure your server hosts the installer directly — do not serve it through jsDelivr, as it exceeds the CDN’s size limit. Use unpkg or self-host instead.
  • Scanner not listed in the dropdown: Ensure the scanner driver is installed on the OS and the device is powered on before calling GetDevicesAsync. On Linux, SANE must be installed; on macOS, ICA is used automatically. TWAIN 32-bit scanners require the TWAINSCANNER (not TWAINX64SCANNER) flag.
  • PDF save creates an empty file: Calling SaveAllAsPDF requires at least one image in the buffer. Guard against an empty buffer by checking dwtObject.HowManyImagesInBuffer > 0 before invoking the save function.

Source Code

https://github.com/yushulx/blazor-barcode-mrz-document-scanner