How to Build a .NET Document Scanner with C# and Windows OCR API

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

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

  1. Set the license key in Form1.cs:

    private static string licenseKey = "LICENSE-KEY";
    
  2. Run the application:

    dotnet run
    

    .NET Document Scanner with OCR

Source Code

https://github.com/yushulx/dotnet-twain-wia-sane-scanner/tree/main/examples/document-ocr