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
- 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
- Request a free trial license 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.
C# Class Library for Scanning Documents from TWAIN, WIA, SANE, and eSCL Scanners
-
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>
-
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
. -
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; } }
-
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;
}
}
.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.
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;
}
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);
}
}
}
.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
.
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>
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);
}
}
}
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" />
Test the app across Windows, macOS, Android, and iOS platforms.