DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Running the Application

  1. Set the license key in Form1.cs:
   private static string licenseKey = "LICENSE-KEY";
Enter fullscreen mode Exit fullscreen mode
  1. Run the application:
   dotnet run
Enter fullscreen mode Exit fullscreen mode

.NET Document Scanner with OCR

Source Code

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

Top comments (2)

Collapse
 
auyeungdavid_2847435260 profile image
David Au Yeung • Edited

Which language is supported by this OCR? And how about handwritten files?

Collapse
 
yushulx profile image
Xiao Ling

The number of languages supported by the Windows OCR API is limited and depends on language packs installed on the device. You can install OCR language packs directly from Windows Settings → Time & Language → Language & Region.