How to Build a .NET MAUI Windows App to Read Barcodes from Scanned Documents
Document digitization is a common task across various industries. Many documents-such as exam papers, legal contracts, patient records, account statements and more-contain barcodes that store crucial information. In this article, we will demonstrate how to build a .NET MAUI Windows app to digitize documents using traditional scanners ( HP, Canon, Epson, etc.) and extract barcodes from scanned documents.
Demo Video
Prerequisites
- .NET SDK.
- Install Dynamsoft service for Windows.
- Connect Your PC to a TWAIN, WIA, or SANE compatible scanner.
- Obtain a free trial license key for Dynamsoft Barcode Reader.
Why Do Companies Still Use Traditional Scanners?
Before diving into the code, let’s explore why companies continue to use traditional scanners instead of HD USB cameras or mobile phones for document digitization:
- Image Quality and Consistency: Scanners provide uniform lighting and a fixed distance between the document and sensor, ensuring high-quality images.
- Speed and Efficiency: Scanners can quickly and automatically process multiple pages.
- OCR Accuracy: High-quality scanned images enhance OCR processing accuracy.
- Security and Compliance: Scanners offer better security and compliance with regulatory requirements than mobile devices.
Dynamsoft Service provides RESTful APIs that allow programming languages to interact with TWAIN, WIA, SANE, and ESCL compatible scanners. In the following sections, we will use Twain.Wia.Sane.Scanner, a C# wrapper of Dynamsoft Service, to interact with scanners and Dynamsoft.DotNet.BarcodeReader.Bundle to read barcodes.
Step 1: Set Up a .NET 9 MAUI Windows App
- In Visual Studio Code, press
Ctrl+Shift+P
to open the command palette and run theCreate a new .NET MAUI App
command to scaffold a new .NET MAUI project.
2. Add Dynamsoft.DotNet.BarcodeReader.Bundle
and Twain.Wia.Sane.Scanner
to the *.csproj
file.
<ItemGroup>
<PackageReference Include="Dynamsoft.DotNet.BarcodeReader.Bundle" Version="10.4.2000" />
<PackageReference Include="Twain.Wia.Sane.Scanner" Version="1.2.0" />
</ItemGroup>
Step 2: Acquire Documents from a TWAIN, WIA, or SANE-Compatible Scanner
UI Construction
Define the UI layout in MainPage.xaml
. The UI includes Picker
, CheckBox
, Button
, Label
, and Editor
components.
<?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="documentbarcode.MainPage">
<HorizontalStackLayout HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
<VerticalStackLayout Margin="20"
MaximumWidthRequest="400"
WidthRequest="400"
Spacing="20">
<StackLayout Padding="10"
BackgroundColor="#f0f0f0"
Spacing="5">
<Picker x:Name="DevicePicker"
Title="Select Source"
ItemsSource="{Binding Items}">
</Picker>
<Picker x:Name="ColorPicker"
Title="Select Pixel Type">
<Picker.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>B & W</x:String>
<x:String>Gray</x:String>
<x:String>Color</x:String>
</x:Array>
</Picker.ItemsSource>
</Picker>
<Picker x:Name="ResolutionPicker"
Title="Select Resolution">
<Picker.ItemsSource>
<x:Array Type="{x:Type x:Int32}">
<x:Int32>100</x:Int32>
<x:Int32>150</x:Int32>
<x:Int32>200</x:Int32>
<x:Int32>300</x:Int32>
</x:Array>
</Picker.ItemsSource>
</Picker>
<StackLayout Orientation="Horizontal">
<CheckBox x:Name="showUICheckbox"/>
<Label Text="Show UI"
VerticalOptions="Center"/>
</StackLayout>
<StackLayout Orientation="Horizontal">
<CheckBox x:Name="adfCheckbox"/>
<Label Text="ADF"
VerticalOptions="Center"/>
</StackLayout>
<StackLayout Orientation="Horizontal">
<CheckBox x:Name="duplexCheckbox"/>
<Label Text="Duplex"
VerticalOptions="Center"/>
</StackLayout>
<Grid RowDefinitions="*, *"
ColumnDefinitions="*, *"
Padding="10">
<Button Text="Scan Document"
Clicked="OnLoadImageClicked"
Grid.Row="0"
Grid.Column="0"
Margin="10"/>
<Button Text="Read Barcode"
Clicked="OnScanBarcodeClicked"
Grid.Row="0"
Grid.Column="1"
Margin="10"/>
<Button Text="Save Current Image"
Clicked="OnSaveClicked"
Grid.Row="1"
Grid.Column="0"
Margin="10"/>
<Button Text="Delete All"
Clicked="OnDeleteAllClicked"
Grid.Row="1"
Grid.Column="1"
Margin="10"/>
</Grid>
<Label x:Name="BarcodeResultsLabel"
Text="Barcode Results"
TextColor="blue"/>
<Editor x:Name="BarcodeResultContent"
HeightRequest="300"
WidthRequest="360"/>
</StackLayout>
</VerticalStackLayout>
<ScrollView x:Name="ImageScrollView"
WidthRequest="800"
HeightRequest="800">
<VerticalStackLayout x:Name="ImageContainer"/>
</ScrollView>
<Image x:Name="LargeImage"
Aspect="AspectFit"
MaximumWidthRequest="600"/>
</HorizontalStackLayout>
</ContentPage>
Explanation of UI Components:
DevicePicker
: Selects a TWAIN, WIA, or SANE-compatible scanner.ColorPicker
: Selects the pixel type.ResolutionPicker
: Selects the resolution.showUICheckbox
: Toggles the scanner UI.adfCheckbox
: Enables the automatic document feeder.duplexCheckbox
: Enables duplex scanning.BarcodeResultContent
: Displays barcode results.ImageScrollView
: Shows scanned image thumbnails.LargeImage
: Displays the selected scanned image.
Scanning Documents in C#
- Get the scanner source list and populate the
DevicePicker
:
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 List<byte[]> _streams = new List<byte[]>();
public ObservableCollection<string> Items { get; set; }
private int selectedIndex = -1;
private async void InitializeDevices()
{
var scanners = await scannerController.GetDevices(host, ScannerType.TWAINX64SCANNER | ScannerType.ESCLSCANNER);
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"];
if (name != null)
{
Items.Add(name.ToString());
}
}
DevicePicker.SelectedIndex = 0;
}
2. Add a button click event to handle document scanning:
private async void OnLoadImageClicked(object sender, System.EventArgs e)
{
if (DevicePicker.SelectedIndex < 0) return;
var parameters = new Dictionary<string, object>
{
{"license", licenseKey},
{"device", devices[DevicePicker.SelectedIndex]["device"]}
};
parameters["config"] = new Dictionary<string, object>
{
{"IfShowUI", showUICheckbox.IsChecked},
{"PixelType", ColorPicker.SelectedIndex},
{"Resolution", (int)ResolutionPicker.SelectedItem},
{"IfFeederEnabled", adfCheckbox.IsChecked},
{"IfDuplexEnabled", duplexCheckbox.IsChecked},
};
var data = await scannerController.ScanDocument(host, parameters);
string jobId = "";
if (data.ContainsKey(ScannerController.SCAN_SUCCESS))
{
jobId = data[ScannerController.SCAN_SUCCESS];
}
string error = "";
if (data.ContainsKey(ScannerController.SCAN_ERROR))
{
error = data[ScannerController.SCAN_ERROR];
}
if (!string.IsNullOrEmpty(jobId))
{
var images = await scannerController.GetImageStreams(host, jobId);
int start = _streams.Count;
for (int i = 0; i < images.Count; i++)
{
MemoryStream stream = new MemoryStream(images[i]);
_streams.Add(images[i]);
ImageSource imageStream = ImageSource.FromStream(() => stream);
Image image = new Image
{
WidthRequest = 200,
Aspect = Aspect.AspectFit,
Source = imageStream,
BindingContext = i + start
};
var tapGestureRecognizer = new TapGestureRecognizer();
tapGestureRecognizer.Tapped += OnImageTapped;
image.GestureRecognizers.Add(tapGestureRecognizer);
ImageContainer.Children.Add(image);
}
ScrollToLatestImage();
ShowLargeImage(_streams[_streams.Count - 1]);
}
else if (!string.IsNullOrEmpty(error))
{
await DisplayAlert("Error", error, "OK");
}
}
private void ShowLargeImage(byte[] bytes)
{
MemoryStream stream = new MemoryStream(bytes);
ImageSource imageStream = ImageSource.FromStream(() => stream);
LargeImage.Source = imageStream;
}
private void OnImageTapped(object? sender, TappedEventArgs e)
{
if (sender is Image image && image.BindingContext is int index)
{
byte[] imageData = _streams[index];
ShowLargeImage(imageData);
selectedIndex = index;
}
}
All scanned images are stored in _streams
for later operations. The OnImageTapped
method is used to display the selected image in LargeImage
.
Step 3: Read Barcodes from Scanned Documents
- Initialize the
CaptureVisionRouter
object:
private void InitializeCVR()
{
string errorMsg;
int errorCode = LicenseManager.InitLicense(licenseKey, out errorMsg);
if (errorCode != (int)Dynamsoft.Core.EnumErrorCode.EC_OK)
Console.WriteLine("License initialization error: " + errorMsg);
cvr = new CaptureVisionRouter();
}
2. Add a button click event to read barcodes from the current image:
private void OnScanBarcodeClicked(object sender, System.EventArgs e)
{
if (_streams.Count == 0)
{
DisplayAlert("Error", "Please load an image first.", "OK");
return;
}
BarcodeResultContent.Text = "";
CapturedResult result = cvr.Capture(_streams[selectedIndex], PresetTemplate.PT_READ_BARCODES);
if (result != null)
{
DecodedBarcodesResult barcodesResult = result.GetDecodedBarcodesResult();
if (barcodesResult != null)
{
BarcodeResultItem[] items = barcodesResult.GetItems();
BarcodeResultContent.Text += "Total barcode(s) found: " + items.Length + Environment.NewLine + Environment.NewLine;
int index = 1;
foreach (BarcodeResultItem barcodeItem in items)
{
BarcodeResultContent.Text += "Result " + index + Environment.NewLine;
BarcodeResultContent.Text += "Barcode Format: " + barcodeItem.GetFormatString() + Environment.NewLine;
BarcodeResultContent.Text += "Barcode Text: " + barcodeItem.GetText() + Environment.NewLine + Environment.NewLine;
index += 1;
}
}
}
}
The Capture
method reads barcodes from the image data and returns a list of barcode results. The barcode results are displayed in the BarcodeResultContent
editor.
3. Connect a scanner to your PC and press F5
to run the .NET MAUI Windows app.
Source Code
https://github.com/yushulx/dotnet-twain-wia-sane-scanner/tree/main/examples/documentbarcode
Originally published at https://www.dynamsoft.com on February 16, 2025.