How to Build a .NET MAUI Windows App to Read Barcodes from Scanned Documents

Xiao Ling
5 min read4 days ago

--

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

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

  1. In Visual Studio Code, press Ctrl+Shift+P to open the command palette and run the Create 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 &amp; 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#

  1. 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

  1. 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.

--

--

Xiao Ling
Xiao Ling

Written by Xiao Ling

Manager of Dynamsoft Open Source Projects | Tech Lover

No responses yet