In today's digital workplace, document scanning and text recognition are vital capabilities for many business applications. In this tutorial, you'll learn how to build a Windows document scanner application with Optical Character Recognition (OCR) using:
- .NET 8
- C#
- Dynamic Web TWAIN REST API
- Windows.Media.Ocr API (Windows built-in OCR engine)
By the end, you'll have a fully functional desktop app that can scan documents, manage images, and recognize text in multiple languages.
Demo - .NET Document Scanner with Free OCR
Prerequisites
- Install Dynamic Web TWAIN Service for Windows
- Get a free trial license for Dynamic Web TWAIN
What We'll Build
Your application will include:
- TWAIN Scanner Integration for professional document scanning
- Image Management with gallery view and delete operations
- Multi-language OCR powered by Windows built-in OCR engine
- File Operations for loading existing images
Project Setup
1. Create the Project
dotnet new winforms -n DocumentScannerOCR
cd DocumentScannerOCR
2. Add Dependencies
Edit your .csproj
file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.17763.0</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<UseWinRT>true</UseWinRT>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Twain.Wia.Sane.Scanner" Version="2.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
Why these packages?
- Twain.Wia.Sane.Scanner: Wrapper for the Dynamic Web TWAIN REST API
- Newtonsoft.Json: High-performance JSON serialization/deserialization
3. Import Namespaces
In Form1.cs
:
using Newtonsoft.Json;
using System.Collections.ObjectModel;
using Twain.Wia.Sane.Scanner;
using Windows.Media.Ocr;
using Windows.Graphics.Imaging;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Storage.Streams;
using System.Diagnostics;
Core Implementation
1. Main Form Class Structure
public partial class Form1 : Form
{
private static string licenseKey = "YOUR_DYNAMSOFT_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<Image> scannedImages = new List<Image>();
private Image? selectedImage = null;
private int selectedImageIndex = -1;
public ObservableCollection<string> Items { get; set; }
public ObservableCollection<string> OcrLanguages { get; set; }
public Form1()
{
InitializeComponent();
SetupUI();
InitializeOcrLanguages();
}
}
2. OCR Language Initialization
The Windows.Media.Ocr API provides access to system-installed OCR (C:\Windows\OCR
) languages:
private void InitializeOcrLanguages()
{
try
{
var supportedLanguages = OcrEngine.AvailableRecognizerLanguages;
foreach (var language in supportedLanguages)
{
OcrLanguages.Add($"{language.DisplayName} ({language.LanguageTag})");
}
languageComboBox.DataSource = OcrLanguages;
if (OcrLanguages.Count > 0)
{
// Try to select English as default
var englishIndex = OcrLanguages.ToList().FindIndex(lang => lang.Contains("English"));
languageComboBox.SelectedIndex = englishIndex >= 0 ? englishIndex : 0;
}
}
catch (Exception ex)
{
MessageBox.Show($"Error initializing OCR languages: {ex.Message}",
"OCR Error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
3. Scanner Device Detection
Use the Dynamic Web TWAIN REST API to discover available scanners:
private async void GetDevicesButton_Click(object sender, EventArgs e)
{
var scannerInfo = await scannerController.GetDevices(host,
ScannerType.TWAINSCANNER | ScannerType.TWAINX64SCANNER);
devices.Clear();
Items.Clear();
var scanners = new List<Dictionary<string, object>>();
try
{
scanners = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(scannerInfo)
?? new List<Dictionary<string, object>>();
}
catch (Exception ex)
{
Debug.WriteLine($"Error parsing scanner data: {ex.Message}");
MessageBox.Show("Error detecting scanners. Please ensure TWAIN service is running.");
return;
}
if (scanners.Count == 0)
{
MessageBox.Show("No scanners found. Please check your scanner connection.");
return;
}
foreach (var scanner in scanners)
{
devices.Add(scanner);
if (scanner.ContainsKey("name"))
{
Items.Add(scanner["name"].ToString() ?? "Unknown Scanner");
}
}
comboBox1.DataSource = Items;
}
4. Document Scanning Implementation
Add a button click event handler for triggering the scan and inserting the scanned image into the UI:
private async void ScanButton_Click(object sender, EventArgs e)
{
if (comboBox1.SelectedIndex < 0)
{
MessageBox.Show("Please select a scanner first.");
return;
}
try
{
var device = devices[comboBox1.SelectedIndex];
var parameters = new
{
license = licenseKey,
device = device,
config = new
{
IfShowUI = false,
PixelType = 2,
Resolution = 300,
IfFeederEnabled = false,
IfDuplexEnabled = false
}
};
string jobId = await scannerController.ScanDocument(host, parameters);
if (!string.IsNullOrEmpty(jobId))
{
await ProcessScanResults(jobId);
}
}
catch (Exception ex)
{
MessageBox.Show($"Scanning failed: {ex.Message}", "Scan Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private async Task ProcessScanResults(string jobId)
{
while (true)
{
byte[] imageBytes = await scannerController.GetImageStream(host, jobId);
if (imageBytes.Length == 0)
break;
using var stream = new MemoryStream(imageBytes);
var image = Image.FromStream(stream);
scannedImages.Add(image);
var pictureBox = CreateImagePictureBox(image, scannedImages.Count - 1);
flowLayoutPanel1.Controls.Add(pictureBox);
flowLayoutPanel1.Controls.SetChildIndex(pictureBox, 0);
}
await scannerController.DeleteJob(host, jobId);
}
5. Responsive Image Display
Create picture boxes optimized for document viewing:
private PictureBox CreateImagePictureBox(Image image, int index)
{
int panelWidth = Math.Max(flowLayoutPanel1.Width, 400);
int pictureBoxWidth = Math.Max(280, panelWidth - 60);
double aspectRatio = (double)image.Width / image.Height;
int pictureBoxHeight;
if (aspectRatio > 1.0)
{
pictureBoxHeight = Math.Min(350, (int)(pictureBoxWidth / aspectRatio));
}
else
{
pictureBoxHeight = Math.Min(500, (int)(pictureBoxWidth / aspectRatio));
}
pictureBoxHeight = Math.Max(300, pictureBoxHeight);
var pictureBox = new PictureBox
{
Image = image,
SizeMode = PictureBoxSizeMode.Zoom,
Size = new Size(pictureBoxWidth, pictureBoxHeight),
Margin = new Padding(10),
BorderStyle = BorderStyle.FixedSingle,
Cursor = Cursors.Hand,
Tag = index
};
pictureBox.Click += (s, e) => SelectImage(index);
return pictureBox;
}
6. OCR Processing with Windows.Media.Ocr
Implement text recognition using the Windows built-in OCR engine:
private async void OcrButton_Click(object sender, EventArgs e)
{
if (selectedImage == null)
{
MessageBox.Show("Please select an image first by clicking on it.",
"No Image Selected", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (languageComboBox.SelectedIndex < 0)
{
MessageBox.Show("Please select an OCR language.",
"No Language Selected", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
try
{
ocrButton.Enabled = false;
ocrButton.Text = "Processing...";
var selectedLanguageText = languageComboBox.SelectedItem?.ToString() ?? "";
var languageTag = ExtractLanguageTag(selectedLanguageText);
var language = new Windows.Globalization.Language(languageTag);
var ocrEngine = OcrEngine.TryCreateFromLanguage(language);
if (ocrEngine == null)
{
MessageBox.Show($"OCR engine could not be created for language: {languageTag}",
"OCR Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
var softwareBitmap = await ConvertImageToSoftwareBitmap(selectedImage);
var ocrResult = await ocrEngine.RecognizeAsync(softwareBitmap);
if (string.IsNullOrWhiteSpace(ocrResult.Text))
{
ocrTextBox.Text = "No text was recognized in the selected image.";
}
else
{
ocrTextBox.Text = ocrResult.Text;
}
}
catch (Exception ex)
{
MessageBox.Show($"OCR processing failed: {ex.Message}",
"OCR Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
ocrButton.Enabled = true;
ocrButton.Text = "Run OCR";
}
}
UI Layout Implementation
1. Three-Panel Layout Design
Create a responsive layout optimized for document workflows:
private void SetupUI()
{
mainSplitContainer.Dock = DockStyle.Fill;
mainSplitContainer.Orientation = Orientation.Vertical;
mainSplitContainer.FixedPanel = FixedPanel.Panel2;
mainSplitContainer.SplitterDistance = 800;
rightSplitContainer.Dock = DockStyle.Fill;
rightSplitContainer.Orientation = Orientation.Horizontal;
rightSplitContainer.FixedPanel = FixedPanel.Panel1;
rightSplitContainer.SplitterDistance = 450;
flowLayoutPanel1.FlowDirection = FlowDirection.TopDown;
flowLayoutPanel1.AutoScroll = true;
flowLayoutPanel1.WrapContents = false;
flowLayoutPanel1.Padding = new Padding(15, 20, 15, 15);
ocrButton.Enabled = false;
imagePanel.SizeChanged += ImagePanel_SizeChanged;
flowLayoutPanel1.SizeChanged += FlowLayoutPanel1_SizeChanged;
}
2. Image Management Features
And and delete image files:
private void LoadImageButton_Click(object sender, EventArgs e)
{
using var openFileDialog = new OpenFileDialog
{
Filter = "Image Files|*.jpg;*.jpeg;*.png;*.bmp;*.tiff;*.tif;*.gif",
Multiselect = true,
Title = "Select Image Files"
};
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
foreach (string fileName in openFileDialog.FileNames)
{
try
{
var image = Image.FromFile(fileName);
scannedImages.Add(image);
var pictureBox = CreateImagePictureBox(image, scannedImages.Count - 1);
flowLayoutPanel1.Controls.Add(pictureBox);
flowLayoutPanel1.Controls.SetChildIndex(pictureBox, 0);
}
catch (Exception ex)
{
MessageBox.Show($"Error loading {fileName}: {ex.Message}",
"Load Error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
UpdateDeleteButtonStates();
}
}
private void DeleteSelectedButton_Click(object sender, EventArgs e)
{
if (selectedImageIndex < 0) return;
var result = MessageBox.Show("Are you sure you want to delete the selected image?",
"Confirm Delete", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
scannedImages[selectedImageIndex]?.Dispose();
scannedImages.RemoveAt(selectedImageIndex);
RefreshImageDisplay();
ClearSelection();
}
}
private void DeleteAllButton_Click(object sender, EventArgs e)
{
if (scannedImages.Count == 0) return;
var result = MessageBox.Show($"Are you sure you want to delete all {scannedImages.Count} images?",
"Confirm Delete All", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
foreach (var image in scannedImages)
{
image?.Dispose();
}
scannedImages.Clear();
flowLayoutPanel1.Controls.Clear();
ClearSelection();
ocrTextBox.Clear();
}
}
Running the Application
- Set the license key in
Form1.cs
:
private static string licenseKey = "LICENSE-KEY";
- Run the application:
dotnet run
Source Code
https://github.com/yushulx/dotnet-twain-wia-sane-scanner/tree/main/examples/document-ocr
Which language is supported by this OCR? And how about handwritten files?