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

  • 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.
  • Custom images are fed by modifying the CScanner_FreeImage constructor to read a JSON-configured image folder, cycling images on each acquire event.
  • A source.json / info.json two-file scheme handles index persistence across TWAINDS_Sample32.ds reloads without requiring write access to C:\Windows\twain_32\.
  • This approach is ideal for automated CI/CD testing of TWAIN-dependent document scanning workflows without physical hardware.

Common Developer Questions

  • How do I configure a Windows virtual TWAIN scanner to load custom images instead of the default logo?
  • How do I build and register the TWAIN DS sample as a virtual scanner on Windows?
  • How do I cycle through multiple test images in the TWAIN virtual scanner automatically?

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

Step 1: Build and Debug the TWAIN DS Sample

Build the TWAIN DS Sample

  1. Get the source code:

     git clone https://github.com/twain/twain-samples.git
    
  2. Install Qt and add system variable QTDIR: msvc2017 (x86) or msvc2017_64 (x64).

    Qt system variable

  3. 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

  4. Start Visual Studio as administrator because the output directory is C:\Windows\twain_32\sample2\. Open the project in Visual Studio and update linking options according to your OS environment.
  5. Build the project to generate a TWAINDS_Sample32.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

Step 2: Configure the Virtual Scanner to Load Custom Images

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

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

After debugging the code, I found an appropriate place for the custom image loading code is in the constructor of CScanner_FreeImage class.

When triggering the acquire image event, the TWAINDS_Sample32.ds file will be reloaded to the memory, which means all variables will be reset. Therefore, we use a JSON file to read and write the current image index in order to load different images by clicking the Acquire Image button.

Note: the C:\Windows\twain_32\sample2\ directory is not writable without administrator privileges. Thus, we create a read-only file source.json that contains the image set folder and put an info.json file in the image set folder to store the current image index and max image count for ADF. 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;

CScanner_FreeImage::CScanner_FreeImage()
{
    ...

    char sourceConfig[PATH_MAX];
    SSNPRINTF(sourceConfig, sizeof(sourceConfig), PATH_MAX, "%s%csource.json", szTWAIN_DS_DIR, PATH_SEPERATOR);
    vector<string> images;

    if (FILE_EXISTS(sourceConfig))
    {
        // Read the image folder from source.json
        ifstream stream(sourceConfig);
        json  source;
        stream >> source;
        stream.close();

        string imageFolder = source["folder"];

        if (FILE_EXISTS(imageFolder.c_str()))
        { 
            // Get the image index
            string infoPath = imageFolder + PATH_SEPERATOR + "info.json";
            ifstream infoStream(infoPath);
            json info;
            infoStream >> info;
            infoStream.close();

            int index = info["index"];

            for (const auto& entry : fs::directory_iterator(imageFolder))
            {
                std::string path{ entry.path().u8string() };
                string suffix = path.substr(path.length() - 4, 4);
                // Get JPEG or PNG files
                if (!suffix.compare(".jpg") || !suffix.compare(".png"))
                {
                    images.push_back(path);
                }
            }

            if (images.size() > 0)
            {
                if (index >= images.size()) index = 0;

                // Set a custom image
                SSNPRINTF(m_szSourceImagePath, sizeof(m_szSourceImagePath), PATH_MAX, images[index].c_str());

                // Save image index to info.json
                index += 1;
                info["index"] = index;
                std::ofstream stream(infoPath);
                stream << info << std::endl;
                stream.close();
            }
        }
    }

    // If there's no config file for custom image set, use the default image
    if (images.size() == 0)
    {
        SSNPRINTF(m_szSourceImagePath, sizeof(m_szSourceImagePath), PATH_MAX, "%s%cTWAIN_logo.png", szTWAIN_DS_DIR, PATH_SEPERATOR);
    }
    ...
}

You can download json.hpp from https://github.com/nlohmann/json.

Now we can re-build the project and test it with 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.
  • Access denied writing info.json to twain_32\sample2\: The C:\Windows\twain_32\sample2\ directory requires administrator write access. Store info.json in the user-owned image folder (as shown above) rather than the install directory.
  • Image index not advancing between acquires: Each acquire event causes TWAINDS_Sample32.ds to reload, resetting all in-memory state. The index must be persisted to disk via info.json; in-memory counters will not survive between acquire calls.

Source Code

https://github.com/yushulx/virtual-scanner/tree/main/windows