Building .NET Apps for Scanning Documents from TWAIN, WIA, SANE, and eSCL Scanners

Xiao Ling - Oct 11 '23 - - Dev Community

Having previously developed libraries for Node.js, Flutter, Java, and Python to integrate with Dynamsoft Service'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/

Prerequisites

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.

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:

    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:

    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
    
  2. Create a ScannerType.cs file to define the scanner types:

    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:

    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:

    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.

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

.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

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

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

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.

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

.NET WinForm document scanner

.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

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

<?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>

Enter fullscreen mode Exit fullscreen mode

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.

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

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:

<application android:usesCleartextTraffic="true" />
Enter fullscreen mode Exit fullscreen mode

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

.NET MAUI document scanner

Source Code

https://github.com/yushulx/dotnet-twain-wia-sane-scanner

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player