How to Configure a Windows Virtual TWAIN Scanner to Load Custom Images

When building a TWAIN-compatible application and there is no physical scanner available, you can use a virtual scanner application for development and testing. TWAIN working group has released a TWAIN DS sample on GitHub. In this article, I will detail how to set up the development environment, as well as how to substitute the default TWAIN logo with custom images.

Windows Virtual TWAIN Scanner with Custom Images

What you’ll build: A modified Windows TWAIN data source (.ds file) that cycles through a folder of custom JPEG/PNG images instead of the default TWAIN logo, enabling repeatable hardware-free scanner testing using Visual Studio and C++.

Key Takeaways

  • Since v1.0.1, a prebuilt installer (VirtualScannerSetup-1.0.5.exe) ships the virtual scanner together with 11 sample images — no Visual Studio or Qt needed to start scanning.
  • The TWAIN DS sample from the TWAIN working group can be compiled into a .ds dynamic library and registered as a virtual scanner on Windows. The fork ships a one-click build_and_install.cmd that builds the data source, deploys it to C:\Windows\twain_64\sample2\ and copies the 11 sample images.
  • Custom images are fed by CScanner_FreeImage::initCustomDataSet(), which runs on every acquire event: it reads a JSON-configured image folder (or the bundled images folder), sorts it in natural order (2.png before 10.png) and hands out the next picture per scan.
  • A source.json / info.json two-file scheme handles index persistence across .ds reloads; folders that are read-only (like C:\Windows\twain_64\sample2\images) fall back to a per-user %LOCALAPPDATA%\VirtualScanner\info.json.
  • This approach is ideal for automated CI/CD testing of TWAIN-dependent document scanning workflows without physical hardware.

Common Developer Questions

Modify the TWAIN DS sample so its image-loading path reads from a configurable folder rather than always falling back to the built-in TWAIN logo. In this guide, CScanner_FreeImage::initCustomDataSet() is the hook that swaps in JPEG or PNG files from your own test set.

How do I build and register the TWAIN DS sample as a virtual scanner on Windows?

Clone the TWAIN sample project, configure the required Qt toolchain in Visual Studio, and build the project as a .ds data source into the TWAIN installation path. Once built, tools like twacker or Dynamic Web TWAIN can enumerate it like a normal scanner source.

How do I cycle through multiple test images in the TWAIN virtual scanner automatically?

Persist the current image index in a JSON file on disk and update that index each time the acquire path runs. That disk-backed counter is necessary because the .ds file is reloaded on each acquire event, which resets any in-memory state.

Quick Start: Install the Prebuilt Virtual Scanner (v1.0.5)

Building the sample yourself is only necessary if you want to customize the code. Since v1.0.5, the release page ships ready-to-use Windows packages:

  • VirtualScannerSetup-1.0.5.exe — run the installer (it asks for administrator rights) and the Virtual Scanner data source appears in twacker and Dynamic Web TWAIN.
  • VirtualScanner-1.0.5-win64-portable.zip — unpack it into C:\Windows\twain_64\sample2 and run configure-samples.cmd.

Both packages install 11 sample images next to the data source (C:\Windows\twain_64\sample2\images), so every flatbed scan returns a different picture and a document-feeder (ADF) scan returns all 11 pictures as one multi-page document — no configuration file needed. The scan position is remembered between scans in %LOCALAPPDATA%\VirtualScanner\info.json because the C:\Windows folder is not writable for normal users.

To scan your own images, replace the files in C:\Windows\twain_64\sample2\images (administrator rights required) or point a source.json file next to the data source at your own folder — the same mechanism Step 2 below explains in depth.

The rest of this article shows how the data source works under the hood and how to build a customized version from source.

How the TWAIN Document Acquisition Process Works

Before getting started, let’s take a glimpse of the workflow of the document acquisition process.

TWAIN data source

We are going to do something in the source layer.

Prerequisites

  • Visual Studio 2017 or above with the Desktop development with C++ workload (only needed when building from source)
  • Qt 5.12.11 msvc2017 (64-bit builds: msvc2017_64) — or pass -AutoInstallQt to build_and_install.cmd and let it download Qt for you
  • Get a 30-day free trial license for Dynamic Web TWAIN to test scanning from the browser.
  • Test tools:

Step 1: Build and Deploy the TWAIN DS Sample

The modified sample lives in the yushulx/virtual-scanner fork (windows/TWAIN-Samples), so clone that repository instead of the upstream twain/twain-samples:

git clone https://github.com/yushulx/virtual-scanner.git

One-Command Build and Install

windows\build_and_install.cmd wraps the whole manual flow in a single command (right-click it or run it from a console):

  • it detects the installed Visual Studio toolset and Windows SDK automatically,
  • locates Qt 5.12.11 (or downloads it with -AutoInstallQt, ~1 GB, via aqtinstall),
  • compiles the data source and deploys it to C:\Windows\twain_64\sample2\, and
  • copies the 11 sample pictures into C:\Windows\twain_64\sample2\images\.

It asks for administrator rights (a UAC prompt) because the data source has to live under C:\Windows. The most useful options:

Option Meaning
-Platform x64 64-bit data source (default); Win32 or All for 32-bit / both
-QTDir <dir> Qt kit folder (the one containing bin\windeployqt.exe)
-AutoInstallQt download Qt 5.12.11 when it is missing
-StageDir <dir> assemble a package without touching C:\Windows (no admin needed)
-Uninstall remove C:\Windows\twain_64\sample2 (and twain_32\sample2)

The default build produces TWAINDS_Sample64.ds in C:\Windows\twain_64\sample2\. Building the upstream sample for 32-bit produces the TWAINDS_Sample32.ds under C:\Windows\twain_32\sample2\ mentioned in older guides — same code, different bitness.

Building in Visual Studio by Hand

If you prefer the IDE:

  1. Install Qt and set the QTDIR environment variable to the kit: msvc2017 (x86) or msvc2017_64 (x64).

    Qt system variable

  2. If you have installed multiple Qt kits, such as MinGW, ARM64, MSVC, you have to move MSVC ahead of others in PATH to avoid deploying incompatible DLL files with windeployqt.exe.
  3. Start Visual Studio as administrator because the project copies its output to C:\Windows\twain_64\sample2\. Open TWAIN-Samples\Twain_DS_sample01\visual_studio\TWAINDS_VS2017.sln and build it to generate the .ds file, which is a dynamic library.

Debug the TWAIN DS Sample

  1. Run twacker (32-bit/64-bit) application.
  2. In Visual Studio, set a breakpoint in DS_Entry() and then go to Debug > Attach to Process to find the twacker process.

    attach TWAIN DS to twacker

  3. Click the Attach button and then select the virtual scanner to trigger debugging.

    Debug TWAIN virtual scanner

For quick diagnostics without a debugger, set the environment variable TWAIN_SHOWCONSOLEMESSAGES=1 before scanning: the data source then prints which image folder was picked, how many images were found, and where the cycle position is.

Step 2: Configure the Virtual Scanner to Load Custom Images

Instead of always serving the default TWAIN logo, the TWAIN DS sample source can load any JPEG or PNG from a folder.

The DS_Entry() function is the entry point of the TWAIN data source. We can set a breakpoint in this function to see the stack calls.

After debugging the code, the right place for the image-set logic turned out to be CScanner_FreeImage::initCustomDataSet() — the constructor only sets the TWAIN_logo.png fallback. initCustomDataSet() is called every time the data source is opened, which matters because the .ds file (TWAINDS_Sample64.ds for the 64-bit build) is reloaded into memory on each acquire event, resetting all variables. Re-reading the folder and persisting the position at that moment is what makes every scan return a different image.

Note: C:\Windows\twain_64\sample2\ is not writable without administrator privileges. The image folder and the scan position are therefore resolved like this:

  1. the folder entry of a source.json file next to the data source wins (kept for backward compatibility); otherwise the images folder that ships with the installer is used;
  2. if the image folder contains an info.json file, it stores the current image index and the max page count for ADF;
  3. if it does not (the read-only sample folder under C:\Windows), the position is remembered per user in %LOCALAPPDATA%\VirtualScanner\info.json.

For example:

source.json

{
    "folder": "C:/Users/admin/Pictures/barcode"
}

info.json

{
    "index": 0,
    "maxcount": 10
}

The code is as follows:

#include "json.hpp"
using json = nlohmann::json;

#include <filesystem>
#include <fstream>
namespace fs = std::experimental::filesystem;

// Runs on every acquire event, before pages are handed out.
void CScanner_FreeImage::initCustomDataSet()
{
    images.clear();
    m_nJobPageIndex = 0;

    // 1. The image folder: source.json wins, otherwise <data source>\images
    char szTWAIN_DS_DIR[PATH_MAX];
    GetModuleFileName(g_hinstance, szTWAIN_DS_DIR, PATH_MAX);
    // ... strip filename from path ...

    std::string imageFolder;

    char sourceConfig[PATH_MAX];
    SSNPRINTF(sourceConfig, sizeof(sourceConfig), PATH_MAX, "%s%csource.json", szTWAIN_DS_DIR, PATH_SEPERATOR);

    if (FILE_EXISTS(sourceConfig))
    {
        json source = readJsonFile(sourceConfig);

        if (source.is_object() && source["folder"].is_string())
            imageFolder = source["folder"].get<std::string>();
    }

    if (!directoryExists(imageFolder))
    {
        // Fall back to the images folder that ships with the installer:
        //   C:\Windows\twain_64\sample2\images
        char defaultFolder[PATH_MAX];
        SSNPRINTF(defaultFolder, sizeof(defaultFolder), PATH_MAX, "%s%cimages", szTWAIN_DS_DIR, PATH_SEPERATOR);

        if (directoryExists(defaultFolder))
            imageFolder = defaultFolder;
    }

    if (!directoryExists(imageFolder))
        return; // no image set at all: keep TWAIN_logo.png

    // 2. Collect the .png/.jpg files of that folder and sort them in
    //    natural order ("2.png" before "10.png")
    WIN32_FIND_DATAA data;
    HANDLE handle = FindFirstFileA((imageFolder + "\\*").c_str(), &data);

    do
    {
        if (isImageFile(data.cFileName, data.dwFileAttributes))
            images.push_back(imageFolder + PATH_SEPERATOR + data.cFileName);
    }
    while (0 != FindNextFileA(handle, &data));

    FindClose(handle);

    std::sort(images.begin(), images.end(), naturalCompare);

    // 3. Where to continue: <image folder>\info.json, or a per-user file
    //    (%LOCALAPPDATA%\VirtualScanner\info.json) when the folder cannot
    //    be written to
    const std::string folderState = imageFolder + PATH_SEPERATOR + "info.json";
    std::string       statePath   = folderState;

    if (!FILE_EXISTS(statePath.c_str()))
        statePath = userStateFile();

    json state = readJsonFile(statePath);

    int index    = state["index"].is_number_integer() ? state["index"].get<int>() : 0;
    int maxCount = state["maxcount"].is_number_integer() ? state["maxcount"].get<int>() : (int)images.size();

    m_nStartIndex = index;
    m_nDocCount = m_nMaxDocCount = (short)maxCount; // pages of one ADF job

    // 4. The first page of this scan job
    SSNPRINTF(m_szSourceImagePath, sizeof(m_szSourceImagePath), PATH_MAX, images[index].c_str());

    // 5. The next scan job continues at the following image
    state["index"]    = (index + 1) % (int)images.size();
    state["maxcount"] = maxCount;

    std::ofstream stream(statePath.c_str());
    stream << state << std::endl;
}

json.hpp from nlohmann/json is bundled in the fork’s src folder, so no extra download is needed. A flatbed scan hands out one image per job; an ADF scan returns maxcount pages starting at index, so checking Use document feeder in the Dynamic Web TWAIN online demo gives you the whole folder as one multi-page document.

Rebuild the project (or rerun build_and_install.cmd) and test it with the Dynamic Web TWAIN online demo.

Before

scan images from default virtual scanner

After

scan images from modified virtual scanner

Common Issues & Edge Cases

  • windeployqt.exe deploys wrong Qt DLLs: When multiple Qt kits (MinGW, ARM64, MSVC) are installed, ensure the MSVC kit appears first in PATH. Mismatched DLLs cause silent load failures when the .ds is invoked by the TWAIN manager. (build_and_install.cmd detects the toolset itself, so this only bites manual Visual Studio builds.)
  • Access denied writing info.json to sample2\: Folders under C:\Windows\twain_64\sample2\ require administrator write access. When the image folder is read-only, the data source automatically falls back to a per-user state file (%LOCALAPPDATA%\VirtualScanner\info.json); writable custom folders keep their info.json in place.
  • Image index not advancing between acquires: Each acquire event reloads the .ds file (TWAINDS_Sample64.ds for the 64-bit build), resetting all in-memory state. The index must be persisted to disk — info.json in the image folder, or the per-user fallback — so it survives between acquire calls.

Source Code

Get the complete sample project source code on GitHub