Building desktop applications that interact with hardware devices like scanners and cameras has traditionally been challenging, often requiring platform-specific driver installations, complex configuration, and dealing with vendor-specific SDKs. Modern .NET development, however, provides us with powerful frameworks and libraries that simplify these interactions significantly.
In this comprehensive tutorial, we'll walk through creating a production-ready Document Scanner application using .NET and Windows Forms. This application features scanner integration through the Dynamic Web TWAIN REST API, real-time webcam capture, sophisticated PDF import/export capabilities, and an intuitive drag-and-drop interface for document management.
We will cover how to:
- Scan documents from TWAIN scanners using the Dynamic Web TWAIN REST API.
- Capture images from a Webcam using OpenCvSharp.
- Import and Export PDFs using PdfSharp and PDFtoImage.
- Implement a modern Drag-and-Drop UI for image management.
- Provide responsive user feedback with progress indicators.
Demo: Scanning Documents with a TWAIN Scanner, Webcam, and PDF Handling
Prerequisites
- OS: Windows 10 or Windows 11
- Development Environment: Visual Studio 2022 or VS Code
- .NET SDK: .NET 8.0 or later
- Hardware: A TWAIN-compatible scanner and a USB webcam
- Dynamic Web TWAIN Service: Install the Dynamic Web TWAIN service for scanner communication.
- 30-day free trial license for Dynamic Web TWAIN.
What You'll Build
By the end of this tutorial, you'll have a fully functional document scanner with:
- TWAIN scanner support on Windows
- Webcam capture with live preview
- PDF import (multi-page) and export capabilities
- Image gallery with drag-and-drop reordering
- Full-screen image viewer
- Batch operations (delete, clear all)
Step 1: Project Setup
First, create a new Windows Forms application.
dotnet new winforms -n WinFormsDocScan
cd WinFormsDocScan
Next, install the necessary NuGet packages. We need libraries for scanning, JSON handling, PDF operations, and computer vision.
dotnet add package Newtonsoft.Json
dotnet add package Twain.Wia.Sane.Scanner
dotnet add package OpenCvSharp4
dotnet add package OpenCvSharp4.runtime.win
dotnet add package OpenCvSharp4.Extensions
dotnet add package PdfSharp
dotnet add package PDFtoImage
dotnet add package SkiaSharp.Views.Desktop.Common
Note: The Twain.Wia.Sane.Scanner package is a .NET wrapper for the **Dynamic Web TWAIN REST API, which runs as a local Windows service and provides a unified interface for communicating with TWAIN scanners. This architecture allows your .NET application to scan documents via simple HTTP requests.
Step 2: Designing the UI
We need a clean interface to display scanned images and provide intuitive controls.
Key UI Components
- Main Container: Use a
FlowLayoutPanel(flowLayoutPanel1) to create an auto-scrolling gallery for scanned images. - Control Panel: Add buttons for core operations:
- 🔍 Get Devices: Detect available scanners
- 🖨️ Scan: Start scanning documents
- 📷 Webcam: Open webcam capture window
- 📁 Load Files: Import images/PDFs from disk
- 💾 Save PDF: Export all images as a single PDF
- 👁️ View: View selected image at full size
- 🗑️ Delete: Remove selected image
- 🗑️ Clear All: Remove all images
- Progress Bar: Add a
ProgressBar(progressBar1) for visual feedback during long operations like scanning and PDF generation. - Scanner Selector: Use a
ComboBoxto let users choose from detected scanners.
Drag-and-Drop Setup
Enable drag-and-drop for file loading and image reordering:
this.AllowDrop = true;
flowLayoutPanel1.AllowDrop = true;
this.DragEnter += Form1_DragEnter;
this.DragDrop += Form1_DragDrop;
Step 3: Implementing Scanner Discovery and Communication
The Dynamic Web TWAIN REST API provides a clean, asynchronous interface for scanner operations. Here's how to integrate it:
Discovering Scanners
First, query the service to detect available scanners:
private static ScannerController scannerController = new ScannerController();
private static string host = "http://127.0.0.1:18622"; // Local service
private static List<Dictionary<string, object>> devices = new List<Dictionary<string, object>>();
private async void btnGetDevices_Click(object sender, EventArgs e)
{
// Show progress indicator
progressBar1.Style = ProgressBarStyle.Marquee;
progressBar1.Visible = true;
// Query REST API for available scanners
var scannerInfo = await scannerController.GetDevices(
host,
ScannerType.TWAINSCANNER | ScannerType.TWAINX64SCANNER
);
progressBar1.Visible = false;
progressBar1.Style = ProgressBarStyle.Blocks;
// Parse JSON response
var scanners = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(scannerInfo);
// Populate dropdown
foreach (var scanner in scanners)
{
devices.Add(scanner);
comboBox1.Items.Add(scanner["name"].ToString());
}
}
Scanning Documents
To scan a document, create a scan job with configuration parameters:
private async void btnScan_Click(object sender, EventArgs e)
{
// Configure scan job
var parameters = new Dictionary<string, object>
{
{"license", licenseKey},
{"device", devices[comboBox1.SelectedIndex]["device"]}
};
parameters["config"] = new Dictionary<string, object>
{
{"IfShowUI", false}, // Hide scanner UI
{"PixelType", 2}, // Color mode (0=BW, 1=Gray, 2=Color)
{"Resolution", 200}, // DPI
{"IfFeederEnabled", true}, // Use document feeder
{"IfDuplexEnabled", false} // Single-sided
};
// Create scan job
var jobInfo = await scannerController.CreateJob(host, parameters);
var job = JsonConvert.DeserializeObject<Dictionary<string, object>>(jobInfo);
string jobId = (string)job["jobuid"];
// Retrieve images one by one (for unknown quantity)
progressBar1.Style = ProgressBarStyle.Marquee;
progressBar1.Visible = true;
while (true)
{
byte[] imageBytes = await scannerController.GetImageStream(host, jobId);
if (imageBytes.Length == 0) break; // No more images
// Display in gallery
using (var ms = new MemoryStream(imageBytes))
{
var image = System.Drawing.Image.FromStream(ms);
AddImageToPanel(image);
}
Application.DoEvents(); // Keep UI responsive
}
progressBar1.Visible = false;
// Clean up job
await scannerController.DeleteJob(host, jobId);
}
Step 4: Webcam Capture with OpenCvSharp
For webcam support, we use the modern OpenCvSharp4 library, which provides .NET bindings for OpenCV.
Opening the Webcam
private VideoCapture? videoCapture;
private System.Windows.Forms.Timer? webcamTimer;
private void btnWebcam_Click(object sender, EventArgs e)
{
videoCapture = new VideoCapture(0); // 0 = default camera
if (!videoCapture.IsOpened())
{
MessageBox.Show("No webcam detected!");
return;
}
// Create webcam preview window
var webcamForm = new Form()
{
Text = "Webcam Capture",
Size = new Size(800, 650),
StartPosition = FormStartPosition.CenterScreen
};
var previewBox = new PictureBox()
{
Size = new Size(760, 520),
Location = new Point(20, 20),
SizeMode = PictureBoxSizeMode.Zoom
};
webcamForm.Controls.Add(previewBox);
// Update preview at ~30 FPS
webcamTimer = new System.Windows.Forms.Timer { Interval = 33 };
webcamTimer.Tick += (s, args) =>
{
Mat frame = new Mat();
if (videoCapture.Read(frame) && !frame.Empty())
{
previewBox.Image?.Dispose();
previewBox.Image = BitmapConverter.ToBitmap(frame);
frame.Dispose();
}
};
webcamTimer.Start();
webcamForm.ShowDialog();
}
Capturing Images
Add a "Capture" button to the webcam window that saves the current frame to the gallery:
btnCapture.Click += (s, args) =>
{
if (previewBox.Image != null)
{
var capturedImage = (Image)previewBox.Image.Clone();
AddImageToPanel(capturedImage);
// Silent capture - no popup message for better UX
}
};
Step 5: PDF Import and Export
PDF handling is crucial for document management workflows. We use two complementary libraries:
Loading PDFs: PDFtoImage
The PDFtoImage library converts PDF pages into raster images that can be displayed in our gallery.
private void LoadPdfFile(string pdfPath)
{
try
{
// Read PDF as byte array (avoids base64 encoding issues)
byte[] pdfBytes = File.ReadAllBytes(pdfPath);
using (var pdfStream = new MemoryStream(pdfBytes))
{
// Convert all pages to SkiaSharp bitmaps
var skBitmaps = PDFtoImage.Conversion.ToImages(pdfStream);
foreach (var skBitmap in skBitmaps)
{
// Convert to System.Drawing.Bitmap
using (var image = skBitmap.ToBitmap())
{
// Clone to avoid disposal issues
var clonedImage = new Bitmap(image);
AddImageToPanel(clonedImage);
}
skBitmap.Dispose();
}
}
}
catch (Exception ex)
{
MessageBox.Show($"Error loading PDF: {ex.Message}");
}
}
Saving PDFs: PdfSharp
PdfSharp enables us to create multi-page PDFs with images scaled to fit standard page sizes.
private void btnSaveToPDF_Click(object sender, EventArgs e)
{
using (var saveDialog = new SaveFileDialog())
{
saveDialog.Filter = "PDF files (*.pdf)|*.pdf";
saveDialog.FileName = $"ScannedDocument_{DateTime.Now:yyyyMMdd_HHmmss}.pdf";
if (saveDialog.ShowDialog() == DialogResult.OK)
{
PdfDocument document = new PdfDocument();
// Show progress for large documents
progressBar1.Visible = true;
progressBar1.Maximum = flowLayoutPanel1.Controls.Count;
progressBar1.Value = 0;
for (int i = 0; i < flowLayoutPanel1.Controls.Count; i++)
{
var panel = flowLayoutPanel1.Controls[i] as Panel;
var pictureBox = panel?.Tag as PictureBox;
if (pictureBox?.Image != null)
{
// Create A4 page
PdfPage page = document.AddPage();
page.Width = XUnit.FromPoint(595); // A4 width
page.Height = XUnit.FromPoint(842); // A4 height
using (XGraphics gfx = XGraphics.FromPdfPage(page))
using (var ms = new MemoryStream())
{
// Save image to stream
pictureBox.Image.Save(ms, ImageFormat.Png);
ms.Position = 0;
XImage img = XImage.FromStream(ms);
// Calculate scaling to fit with margins
double margin = 40;
double maxWidth = page.Width.Point - (2 * margin);
double maxHeight = page.Height.Point - (2 * margin);
double scale = Math.Min(
maxWidth / img.PixelWidth,
maxHeight / img.PixelHeight
);
double width = img.PixelWidth * scale;
double height = img.PixelHeight * scale;
// Center image on page
double x = (page.Width.Point - width) / 2;
double y = (page.Height.Point - height) / 2;
gfx.DrawImage(img, x, y, width, height);
}
}
progressBar1.Value = i + 1;
Application.DoEvents(); // Keep UI responsive
}
document.Save(saveDialog.FileName);
document.Close();
progressBar1.Visible = false;
MessageBox.Show($"PDF saved successfully!\n{saveDialog.FileName}");
}
}
}
Important Considerations:
-
Memory Management: Always dispose of
XImageandMemoryStreamobjects to prevent memory leaks. - Image Scaling: Maintain aspect ratio to prevent distortion.
-
Progress Feedback: Use
Application.DoEvents()to update UI during long operations.
Step 6: Drag-and-Drop Image Reordering
A polished document scanner needs intuitive image management. Users should be able to reorder scanned pages by dragging them to new positions.
Implementing Drag Source
First, detect when a user starts dragging an image panel:
private Panel? draggedPanel;
private Point dragStartPoint;
private void ContainerPanel_MouseDown(object? sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
// Handle dragging from either the panel or the image inside it
draggedPanel = sender is Panel p ? p : (sender as PictureBox)?.Parent as Panel;
if (draggedPanel != null)
{
dragStartPoint = e.Location;
draggedPanel.Cursor = Cursors.SizeAll;
// Select the dragged panel immediately
SelectPanel(draggedPanel);
}
}
}
private void ContainerPanel_MouseMove(object? sender, MouseEventArgs e)
{
if (draggedPanel == null) return;
// Check if mouse moved enough to start drag operation (prevents accidental drags)
if (Math.Abs(e.X - dragStartPoint.X) > 10 || Math.Abs(e.Y - dragStartPoint.Y) > 10)
{
draggedPanel.BorderStyle = BorderStyle.Fixed3D; // Visual feedback
draggedPanel.DoDragDrop(draggedPanel, DragDropEffects.Move);
draggedPanel.BorderStyle = BorderStyle.FixedSingle;
draggedPanel.Cursor = Cursors.Hand;
draggedPanel = null;
}
}
Implementing Drop Target
Handle the drop event to reorder controls in the FlowLayoutPanel:
private void ContainerPanel_DragOver(object? sender, DragEventArgs e)
{
if (e.Data?.GetDataPresent(typeof(Panel)) == true)
{
e.Effect = DragDropEffects.Move;
// Highlight drop target
Panel? targetPanel = sender is Panel p ? p : (sender as PictureBox)?.Parent as Panel;
if (targetPanel != null)
{
targetPanel.BackColor = Color.FromArgb(220, 235, 255);
}
}
}
private void ContainerPanel_DragDrop(object? sender, DragEventArgs e)
{
if (e.Data?.GetData(typeof(Panel)) is Panel sourcePanel)
{
Panel? targetPanel = sender is Panel p ? p : (sender as PictureBox)?.Parent as Panel;
if (targetPanel != null && sourcePanel != targetPanel)
{
// Swap positions in FlowLayoutPanel
int sourceIndex = flowLayoutPanel1.Controls.GetChildIndex(sourcePanel);
int targetIndex = flowLayoutPanel1.Controls.GetChildIndex(targetPanel);
flowLayoutPanel1.Controls.SetChildIndex(sourcePanel, targetIndex);
// Update image labels (Image 1, Image 2, etc.)
UpdateImageLabels();
}
// Restore colors
targetPanel.BackColor = (selectedPictureBox?.Parent == targetPanel)
? Color.FromArgb(230, 240, 255)
: Color.White;
}
}
private void ContainerPanel_DragLeave(object? sender, EventArgs e)
{
// Restore color when drag leaves
Panel? panel = sender is Panel p ? p : (sender as PictureBox)?.Parent as Panel;
if (panel != null)
{
bool isSelected = (selectedPictureBox?.Parent == panel);
panel.BackColor = isSelected ? Color.FromArgb(230, 240, 255) : Color.White;
}
}
Step 7: Additional Features
Full-Screen Image Viewer
Allow users to view selected images at full resolution:
private void btnViewImage_Click(object sender, EventArgs e)
{
if (selectedPictureBox?.Image == null)
{
MessageBox.Show("Please select an image to view!");
return;
}
var viewerForm = new Form()
{
Text = "Image Viewer",
WindowState = FormWindowState.Maximized,
BackColor = Color.Black
};
var pictureBox = new PictureBox()
{
Dock = DockStyle.Fill,
SizeMode = PictureBoxSizeMode.Zoom,
Image = selectedPictureBox.Image
};
viewerForm.Controls.Add(pictureBox);
viewerForm.ShowDialog();
}
Drag-and-Drop File Loading
Enable users to drag files from Windows Explorer directly into the app:
private void Form1_DragEnter(object? sender, DragEventArgs e)
{
if (e.Data?.GetDataPresent(DataFormats.FileDrop) == true)
{
e.Effect = DragDropEffects.Copy;
}
}
private void Form1_DragDrop(object? sender, DragEventArgs e)
{
if (e.Data?.GetData(DataFormats.FileDrop) is string[] files)
{
foreach (string file in files)
{
string ext = Path.GetExtension(file).ToLower();
if (ext == ".pdf")
{
LoadPdfFile(file);
}
else if (new[] {".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff"}.Contains(ext))
{
var image = Image.FromFile(file);
AddImageToPanel(image);
}
}
}
}
Source Code
https://github.com/yushulx/dotnet-twain-wia-sane-scanner/tree/main/examples/WinFormsDocScan

Top comments (0)