Building .NET Apps for Scanning Documents from TWAIN, WIA, SANE, and eSCL Scanners
Tags: dotnet, csharp, programming, tutorial
Published: 2023-10-11 03:10:19

Having previously developed libraries for **Node.js**, **Flutter**, **Java**, and **Python** to integrate with [Dynamsoft Service](https://www.dynamsoft.com/blog/announcement/dynamsoft-service-restful-api/)'s REST API, we're now turning our attention to **.NET**. In this article, we introduce a .NET library to interface with the REST API, facilitating document scanning from TWAIN, WIA, SANE, and eSCL compatible scanners. We'll also walk through three practical implementations: a **Console app**, a **.NET WinForm app**, and a **.NET MAUI app**.

## NuGet Package
[https://www.nuget.org/packages/Twain.Wia.Sane.Scanner/](https://www.nuget.org/packages/Twain.Wia.Sane.Scanner/)


## Prerequisites
- Install Dynamsoft Service on your host machine that has one or more document scanners connected to it. The REST API's host address is set to `http://127.0.0.1:18622`
    - Windows: [Dynamsoft-Service-Setup.msi](https://demo.dynamsoft.com/DWT/DWTResources/dist/DynamsoftServiceSetup.msi)
    - macOS: [Dynamsoft-Service-Setup.pkg](https://demo.dynamsoft.com/DWT/DWTResources/dist/DynamsoftServiceSetup.pkg)
    - Linux: 
        - [Dynamsoft-Service-Setup.deb](https://demo.dynamsoft.com/DWT/DWTResources/dist/DynamsoftServiceSetup.deb)
        - [Dynamsoft-Service-Setup-arm64.deb](https://demo.dynamsoft.com/DWT/DWTResources/dist/DynamsoftServiceSetup-arm64.deb)
        - [Dynamsoft-Service-Setup-mips64el.deb](https://demo.dynamsoft.com/DWT/DWTResources/dist/DynamsoftServiceSetup-mips64el.deb)
        - [Dynamsoft-Service-Setup.rpm](https://demo.dynamsoft.com/DWT/DWTResources/dist/DynamsoftServiceSetup.rpm)
- Request a [free trial license](https://www.dynamsoft.com/customer/license/trialLicense?product=dwt&source=codepool) for Dynamsoft Service.

## Dynamsoft Service REST API

| Method | Endpoint        | Description                   | Parameters                         | Response                      |
|--------|-----------------|-------------------------------|------------------------------------|-------------------------------|
| GET    | `/DWTAPI/Scanners`    | Get a list of scanners  | None                               | `200 OK` with scanner list       |
| POST   | `/DWTAPI/ScanJobs`    | Creates a scan job      | `license`, `device`, `config`      | `201 Created` with job ID    |
| GET    | `/DWTAPI/ScanJobs/:id/NextDocument`| Retrieves a document image     | `id`: Job ID   | `200 OK` with image stream    |
| DELETE | `/DWTAPI/ScanJobs/:id`| Deletes a scan job       | `id`: Job ID                      | `200 OK`              |


## Parameter Configuration for Document Scanner
[https://www.dynamsoft.com/web-twain/docs/info/api/Interfaces.html#DeviceConfiguration](https://www.dynamsoft.com/web-twain/docs/info/api/Interfaces.html#DeviceConfiguration).

## C# Class Library for Scanning Documents from TWAIN, WIA, SANE, and eSCL Scanners
1. Scaffold a .NET class library project using the command provided below:

    ```bash
    dotnet new classlib -o Twain.Wia.Sane.Scanner
    ```
    
    To simultaneously build a DLL file and a NuGet package, add the subsequent lines to your `.csproj` file:
    
    ```xml
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
    ```

2. Create a `ScannerType.cs` file to define the scanner types:

    ```csharp
    namespace Twain.Wia.Sane.Scanner;
    
    public static class ScannerType
    {
        public const int TWAINSCANNER = 0x10;
        public const int WIASCANNER = 0x20;
        public const int TWAINX64SCANNER = 0x40;
        public const int ICASCANNER = 0x80;
        public const int SANESCANNER = 0x100;
        public const int ESCLSCANNER = 0x200;
        public const int WIFIDIRECTSCANNER = 0x400;
        public const int WIATWAINSCANNER = 0x800;
    }
    ```
    
    Specifying scanner types can accelerate the scanner discovery process in Dynamsoft Service. For instance, to exclusively detect TWAIN scanners, set the scannerType parameter to `ScannerType.TWAINSCANNER | ScannerType.TWAINX64SCANNER`.

3. Construct a `ScannerController.cs` file to implement the REST API calls:
    
    ```csharp
    namespace Twain.Wia.Sane.Scanner;

    using System;
    using System.Collections.Generic;
    using System.Net.Http;
    using System.Text.Json;
    using System.IO;

    public class ScannerController
    {
        private HttpClient _httpClient = new HttpClient();

        public async Task<List<Dictionary<string, object>>> GetDevices(string host, int? scannerType = null)
        {
            List<Dictionary<string, object>> devices = new List<Dictionary<string, object>>();
            string url = $"{host}/DWTAPI/Scanners";
            if (scannerType.HasValue)
            {
                url += $"?type={scannerType.Value}";
            }

            try
            {
                var response = await _httpClient.GetAsync(url);
                if (response.IsSuccessStatusCode)
                {
                    var responseBody = await response.Content.ReadAsStringAsync();
                    if (!string.IsNullOrEmpty(responseBody))
                    {
                        devices = JsonSerializer.Deserialize<List<Dictionary<string, object>>>(responseBody) ?? new List<Dictionary<string, object>>();
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }

            return devices;
        }

        public async Task<string> ScanDocument(string host, Dictionary<string, object> parameters)
        {
            string url = $"{host}/DWTAPI/ScanJobs";
            try
            {
                var json = JsonSerializer.Serialize(parameters);
                var response = await _httpClient.PostAsync(url, new StringContent(json, System.Text.Encoding.UTF8, "application/json"));
                if (response.IsSuccessStatusCode)
                {
                    return await response.Content.ReadAsStringAsync();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }

            return "";
        }

        public async void DeleteJob(string host, string jobId)
        {
            if (string.IsNullOrEmpty(jobId))
            {
                return;
            }
            string url = $"{host}/DWTAPI/ScanJobs/{jobId}";

            try
            {
                var response = await _httpClient.DeleteAsync(url);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }

        public async Task<string> GetImageFile(string host, string jobId, string directory)
        {
            string url = $"{host}/DWTAPI/ScanJobs/{jobId}/NextDocument";
            try
            {
                var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
                if (response.IsSuccessStatusCode)
                {
                    string timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
                    string filename = $"image_{timestamp}.jpg";
                    string imagePath = Path.Combine(directory, filename);
                    using (FileStream fs = new FileStream(imagePath, FileMode.Create))
                    {
                        await response.Content.CopyToAsync(fs);
                    }
                    return filename;
                }
            }
            catch
            {
                Console.WriteLine("No more images.");
                return "";
            }

            return "";
        }

        public async Task<List<string>> GetImageFiles(string host, string jobId, string directory)
        {
            List<string> images = new List<string>();
            while (true)
            {
                string filename = await GetImageFile(host, jobId, directory);
                if (string.IsNullOrEmpty(filename))
                {
                    break;
                }
                else
                {
                    images.Add(filename);
                }
            }

            return images;
        }

        public async Task<List<byte[]>> GetImageStreams(string host, string jobId)
        {
            var streams = new List<byte[]>();
            var url = $"{host}/DWTAPI/ScanJobs/{jobId}/NextDocument";

            while (true)
            {
                try
                {
                    var response = await _httpClient.GetAsync(url);

                    if (response.IsSuccessStatusCode)
                    {
                        byte[] bytes = await response.Content.ReadAsByteArrayAsync();
                        streams.Add(bytes);
                    }
                    else if ((int)response.StatusCode == 410)
                    {
                        break;
                    }
                }
                catch (Exception)
                {
                    break;
                }
            }

            return streams;
        }
    }
    ```

4. Build the project:

    ```bash
    dotnet build --configuration Release
    ```


## .NET Console App (Windows, macOS, Linux)
A console app serves as an ideal initial step to test the library. It's also well-suited for crafting a command-line tool to scan documents across **Windows**, **macOS**, and **Linux**. Create a Program.cs file for the console app's implementation. Reminder: Replace `LICENSE-KEY` with your personal key.

```csharp
using System;
using System.Collections.Generic;
using Twain.Wia.Sane.Scanner;

public class Program
{
    private static string licenseKey = "LICENSE-KEY";
    private static ScannerController scannerController = new ScannerController();
    private static List<Dictionary<string, object>> devices = new List<Dictionary<string, object>>();
    private static string host = "http://127.0.0.1:18622";
    private static string questions = @"
Please select an operation:
1. Get scanners
2. Acquire documents by scanner index
3. Quit
";

    public static async Task Main()
    {
        await AskQuestion();
    }

    private static async Task<int> AskQuestion()
    {
        while (true)
        {
            Console.WriteLine(".............................................");
            Console.WriteLine(questions);
            string? answer = Console.ReadLine();

            if (string.IsNullOrEmpty(answer))
            {
                continue;
            }

            if (answer == "3")
            {
                break;
            }
            else if (answer == "1")
            {
                var scanners = await scannerController.GetDevices(host, ScannerType.TWAINSCANNER | ScannerType.TWAINX64SCANNER);
                devices.Clear();
                for (int i = 0; i < scanners.Count; i++)
                {
                    var scanner = scanners[i];
                    devices.Add(scanner);
                    Console.WriteLine($"\nIndex: {i}, Name: {scanner["name"]}");
                }
            }
            else if (answer == "2")
            {
                if (devices.Count == 0)
                {
                    Console.WriteLine("Please get scanners first!\n");
                    continue;
                }

                Console.Write($"\nSelect an index (<= {devices.Count - 1}): ");
                int index;
                if (!int.TryParse(Console.ReadLine(), out index))
                {
                    Console.WriteLine("Invalid input. Please enter a number.");
                    continue;
                }

                if (index < 0 || index >= devices.Count)
                {
                    Console.WriteLine("It is out of range.");
                    continue;
                }

                var parameters = new Dictionary<string, object>
                {
                    {"license", licenseKey},
                    {"device", devices[index]["device"]}
                };

                parameters["config"] = new Dictionary<string, object>
                {
                    {"IfShowUI", false},
                    {"PixelType", 2},
                    {"Resolution", 200},
                    {"IfFeederEnabled", false},
                    {"IfDuplexEnabled", false}
                };

                string jobId = await scannerController.ScanDocument(host, parameters);

                if (!string.IsNullOrEmpty(jobId))
                {
                    var images = await scannerController.GetImageFiles(host, jobId, "./");
                    for (int i = 0; i < images.Count; i++)
                    {
                        Console.WriteLine($"Image {i}: {images[i]}");
                    }

                    scannerController.DeleteJob(host, jobId);
                }
            }
            else
            {
                continue;
            }
        }
        return 0;
    }
}
```

## .NET WinForm App (Windows)
.NET WinForm is a mature UI framework for building Windows desktop apps. Our design incorporates a `Button` to retrieve available scanners, a `ComboBox` that acts as a dropdown list for device selection, another `Button` to initiate the document scan, and a scrollable `FlowLayoutPanel` to vertically display the scanned images.

![.NET WinForm UI](https://www.dynamsoft.com/codepool/img/2023/10/dotnet-winform-ui-design.png)

When the `Get Devices` button is clicked, the `GetDevices` method retrieves the scanners, and the `ComboBox` is then populated with the scanner names.

```csharp
private async void button1_Click(object sender, EventArgs e)
{
    var scanners = await scannerController.GetDevices(host);
    devices.Clear();
    Items.Clear();
    if (scanners.Count == 0)
    {
        MessageBox.Show("No scanner found");
        return;
    }
    for (int i = 0; i < scanners.Count; i++)
    {
        var scanner = scanners[i];
        devices.Add(scanner);
        var name = scanner["name"];
        Items.Add(name.ToString() ?? "N/A");
    }

    comboBox1.DataSource = Items;
    comboBox1.SelectedIndex = 0;
}
```

Upon clicking the `Scan` button, the `ScanDocument` method initiates the document scanning, followed by the `GetImageStreams` method retrieving the scanned images. We dynamically generate `PictureBox` controls for these images and integrate them into the `FlowLayoutPanel`.

```csharp
private async void button2_Click(object sender, EventArgs e)
{
    var parameters = new Dictionary<string, object>
        {
            {"license", licenseKey},
            {"device", devices[comboBox1.SelectedIndex]["device"]}
        };

    parameters["config"] = new Dictionary<string, object>
        {
            {"IfShowUI", false},
            {"PixelType", 2},
            {"Resolution", 200},
            {"IfFeederEnabled", false},
            {"IfDuplexEnabled", false}
        };

    string jobId = await scannerController.ScanDocument(host, parameters);

    if (!string.IsNullOrEmpty(jobId))
    {
        var images = await scannerController.GetImageStreams(host, jobId);
        for (int i = 0; i < images.Count; i++)
        {
            MemoryStream stream = new MemoryStream(images[i]);
            Image image = Image.FromStream(stream);

            PictureBox pictureBox = new PictureBox()
            {
                Size = new Size(600, 600),
                SizeMode = PictureBoxSizeMode.Zoom,
            };
            pictureBox.Image = image;
            flowLayoutPanel1.Controls.Add(pictureBox);
            flowLayoutPanel1.Controls.SetChildIndex(pictureBox, 0);
        }
    }
}
```

![.NET WinForm document scanner](https://www.dynamsoft.com/codepool/img/2023/10/dotnet-winform-twain-document-scanner.png)

## .NET MAUI App (Windows, macOS, Android, iOS)
.NET MAUI offers a cross-platform UI framework tailored for crafting native applications for Windows, macOS, Android, and iOS. To interface with Dynamsoft Service across both desktop and mobile apps, navigate to `http://127.0.0.1:18625/` in a web browser and modify the host address from `127.0.0.1` to a local network IP, such as `192.168.8.72`.

![Dynamsoft Service IP config](https://user-images.githubusercontent.com/2202306/266243200-e2b1292e-dfbd-4821-bf41-70e2847dd51e.png)

In contrast to .NET WinForm, .NET MAUI employs XAML for UI design.

```xml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiAppDocScan.MainPage">

    <StackLayout
            Spacing="25"
            Margin="10, 10, 10, 10"
            VerticalOptions="Start">


        <HorizontalStackLayout HorizontalOptions="Center" >
            <Button
    x:Name="GetDeviceBtn"
    Text="Get Devices"
    Clicked="OnGetDeviceClicked"
    HorizontalOptions="Center" Margin="0, 0, 10, 0"/>

            <Picker x:Name="DevicePicker" MaximumWidthRequest="150"
                          ItemsSource="{Binding Items}">
            </Picker>

            <Button
x:Name="ScanBtn"
Text="Scan"
Clicked="OnScanClicked"
HorizontalOptions="Center" Margin="10, 0, 0, 0"/>

        </HorizontalStackLayout>
        <ScrollView MaximumHeightRequest="800">
            <StackLayout x:Name="ImageContainer" />
        </ScrollView>
    </StackLayout>

</ContentPage>

```

The `Picker` in .NET MAUI functions similarly to the `ComboBox` in .NET WinForm. To showcase the scanned images, we utilize the `ScrollView` and `StackLayout`. The underlying code logic is nearly identical to that of the .NET WinForm app.

```csharp
private static string licenseKey = "LICENSE-KEY";
private static ScannerController scannerController = new ScannerController();
private static List<Dictionary<string, object>> devices = new List<Dictionary<string, object>>();
private static string host = "http://192.168.8.72:18622"; // Change this to your server IP address
public ObservableCollection<string> Items { get; set; }

public MainPage()
{
    InitializeComponent();

    Items = new ObservableCollection<string>
    {
    };

    BindingContext = this;
}

private async void OnGetDeviceClicked(object sender, EventArgs e)
{

    var scanners = await scannerController.GetDevices(host);
    devices.Clear();
    Items.Clear();
    if (scanners.Count == 0)
    {
        await DisplayAlert("Error", "No scanner found", "OK");
        return;
    }
    for (int i = 0; i < scanners.Count; i++)
    {
        var scanner = scanners[i];
        devices.Add(scanner);
        var name = scanner["name"];
        Items.Add(name.ToString());
    }

    DevicePicker.SelectedIndex = 0;
}

private async void OnScanClicked(object sender, EventArgs e)
{
    var parameters = new Dictionary<string, object>
        {
            {"license", licenseKey},
            {"device", devices[DevicePicker.SelectedIndex]["device"]}
        };

    parameters["config"] = new Dictionary<string, object>
        {
            {"IfShowUI", false},
            {"PixelType", 2},
            {"Resolution", 200},
            {"IfFeederEnabled", false},
            {"IfDuplexEnabled", false}
        };

    string jobId = await scannerController.ScanDocument(host, parameters);

    if (!string.IsNullOrEmpty(jobId))
    {
        var images = await scannerController.GetImageStreams(host, jobId);
        for (int i = 0; i < images.Count; i++)
        {
            MemoryStream stream = new MemoryStream(images[i]);
            ImageSource imageStream = ImageSource.FromStream(() => stream);
            Image image = new Image();
            image.Source = imageStream;
            ImageContainer.Children.Insert(0, image);
        }
    }
}
```

The HTTP connection is not permitted by default on Android 9.0 and above. To resolve this issue, add the following lines to the `AndroidManifest.xml` file:

```xml
<application android:usesCleartextTraffic="true" />
```

Test the app across Windows, macOS, Android, and iOS platforms.

![.NET MAUI document scanner](https://www.dynamsoft.com/codepool/img/2023/10/dotnet-maui-twain-document-scanner.png)

## Source Code
[https://github.com/yushulx/dotnet-twain-wia-sane-scanner](https://github.com/yushulx/dotnet-twain-wia-sane-scanner)