DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

How to Build a SANE Backend as a Virtual Scanner for Linux

A virtual scanner is a software-based solution that mimics the functionality of a physical document scanner. It allows developers and testers to simulate scanning operations without requiring actual hardware, making it an essential tool for testing and development. In this tutorial, we'll explore how to create a virtual scanner device for Linux using the SANE (Scanner Access Now Easy) framework. By the end of this guide, you'll have a fully functional virtual scanner capable of loading custom images, which can be used to test applications like the Dynamic Web TWAIN online demo.

Linux Virtual Scanner Demo Video

Prerequisites

Before getting started, install the necessary packages by running the following command:

sudo apt install build-essential autoconf libtool libsane-dev sane sane-utils imagemagick libmagickcore-dev
Enter fullscreen mode Exit fullscreen mode

The Prebuilt Virtual Scanner Provided by SANE

The SANE framework includes a prebuilt backend designed for testing purposes. To use it, follow these steps:

  1. Uncomment the line #test in the /etc/sane.d/dll.conf file and save the changes.

    SANE test

  2. Run the following command to list all available scanners:

    $ scanimage -L
    device `test:0' is a Noname frontend-tester virtual device
    device `test:1' is a Noname frontend-tester virtual device
    
  3. Use the following command to simulate a scan and generate a black image:

    $ scanimage -d test > test.pnm
    

To explore additional capabilities of the virtual scanner, you can view all available options by running:

scanimage -d test --help
Enter fullscreen mode Exit fullscreen mode

For example, you can change the test picture to a color image with the following command:

$ scanimage -d test --test-picture Color --mode Color > color.pnm
Enter fullscreen mode Exit fullscreen mode

The test picture supports the following patterns:

  • Solid black: Fills the entire scan area with black.
  • Solid white: Fills the entire scan area with white.
  • Color pattern: Generates various color test patterns based on the selected mode.
  • Grid: Draws a black-and-white grid with squares measuring 10 mm in width and height.

Feeding Custom Images

To feed a custom image into the virtual scanner, you have two options:

  • Modify the test.c file and rebuild the SANE backend.
  • Create a simple custom backend from scratch.

In this tutorial, we'll focus on the second approach: building a custom backend.

Preparing a Custom Image

PNM is frequently used as the default output format by many SANE frontends (such as scanimage). If you have a JPEG image, you can convert it to PNM using ImageMagick:

convert test.jpg test.pnm
Enter fullscreen mode Exit fullscreen mode

Steps to Create a Simple Virtual Scanner for Linux

The simple Linux virtual scanner supports two key features: simulating a document scanner and feeding custom images. No additional settings are supported in this implementation.

Step 1: Create a New Backend in C

Create a new C file named custom_scanner.c. This file will contain functions that are invoked by SANE frontends. The function names must adhere to the SANE naming convention: sane_<backend_name>_<function_name>. In this case, the backend name is custom_scanner.

SANE_Status sane_custom_scanner_init(SANE_Int *version_code, SANE_Auth_Callback authorize)
SANE_Status sane_custom_scanner_get_devices(const SANE_Device ***devices, SANE_Bool local_only)
SANE_Status sane_custom_scanner_open(SANE_String_Const name, SANE_Handle *handle)
void sane_custom_scanner_close(SANE_Handle handle)
SANE_Status sane_custom_scanner_control_option(SANE_Handle handle, SANE_Int option, SANE_Action action, void *value, SANE_Int *info)
const SANE_Option_Descriptor *sane_custom_scanner_get_option_descriptor(SANE_Handle handle, SANE_Int option)
SANE_Status sane_custom_scanner_get_parameters(SANE_Handle handle, SANE_Parameters *params)
SANE_Status sane_custom_scanner_start(SANE_Handle handle)
SANE_Status sane_custom_scanner_read(SANE_Handle handle, SANE_Byte *data, SANE_Int max_length, SANE_Int *length)
void sane_custom_scanner_cancel(SANE_Handle handle)
void sane_custom_scanner_exit()
Enter fullscreen mode Exit fullscreen mode

Creating a Mock Device

We can define a static mock device to represent the virtual scanner. This device information will be returned when the frontend requests available devices.

static SANE_Device mock_device = {
    .name = "my-scanner",
    .vendor = "Custom",
    .model = "Virtual Scanner",
    .type = "Test"};

SANE_Status sane_custom_scanner_get_devices(const SANE_Device ***devices, SANE_Bool local_only)
{
    static const SANE_Device *devs[] = {&mock_device, NULL};
    *devices = devs;
    return SANE_STATUS_GOOD;
}

SANE_Status sane_custom_scanner_open(SANE_String_Const name, SANE_Handle *handle)
{
    *handle = (SANE_Handle)&mock_device;
    return SANE_STATUS_GOOD;
}
Enter fullscreen mode Exit fullscreen mode

Initializing SANE Options

The virtual scanner supports only one option: the number of options. The frontend can query the number of options and retrieve the corresponding option descriptor.

#define OPTION_NUM 0

static const SANE_Option_Descriptor options[] = {
    {.name = SANE_TITLE_NUM_OPTIONS,
     .title = "Number of options",
     .type = SANE_TYPE_INT,
     .size = sizeof(SANE_Int),
     .cap = SANE_CAP_SOFT_DETECT,
     .constraint_type = SANE_CONSTRAINT_NONE}};

SANE_Status sane_custom_scanner_control_option(SANE_Handle handle, SANE_Int option, SANE_Action action, void *value, SANE_Int *info)
{
    if (option == 0 && action == SANE_ACTION_GET_VALUE)
    {
        *(SANE_Int *)value = OPTION_NUM;
        return SANE_STATUS_GOOD;
    }
    return SANE_STATUS_UNSUPPORTED;
}

const SANE_Option_Descriptor *sane_custom_scanner_get_option_descriptor(SANE_Handle handle, SANE_Int option)
{
    if (option == 0)
        return &options[0];
    return NULL;
}
Enter fullscreen mode Exit fullscreen mode

Generating Custom Images

The virtual scanner reads a custom image file and returns the image data to the frontend:

#define CUSTOM_IMAGE_PATH "/home/xiao/Desktop/dwt/test.pnm"

SANE_Status sane_custom_scanner_get_parameters(SANE_Handle handle, SANE_Parameters *params)
{
    if (params)
    {
        FILE *fp = fopen(CUSTOM_IMAGE_PATH, "rb");
        if (fp)
        {
            char magic[3];
            int width, height, maxval;

            fscanf(fp, "%2s", magic);

            char ch;
            while ((ch = fgetc(fp)) == '#')
            {
                while (fgetc(fp) != '\n')
                    ;
            }
            ungetc(ch, fp);

            fscanf(fp, "%d %d %d", &width, &height, &maxval);
            fclose(fp);

            params->pixels_per_line = width;
            params->lines = height;

            if (strcmp(magic, "P6") == 0)
            {
                params->format = SANE_FRAME_RGB;
                params->depth = 8;
                params->bytes_per_line = width * 3;
            }
            else
            {
                params->format = SANE_FRAME_GRAY;
                params->depth = 8;
                params->bytes_per_line = width;
            }
        }
    }
    else
    {
        params->format = SANE_FRAME_RED;
        params->depth = 1;
        return SANE_STATUS_EOF;
    }
    return SANE_STATUS_GOOD;
}

SANE_Status sane_custom_scanner_start(SANE_Handle handle)
{
    static int start_count = 0;

    if (start_count == 1)
        return SANE_STATUS_EOF;

    start_count++;
    return SANE_STATUS_GOOD;
}

SANE_Status sane_custom_scanner_read(SANE_Handle handle, SANE_Byte *data, SANE_Int max_length, SANE_Int *length)
{
    static FILE *fp = NULL;
    static long pixel_data_offset = 0;
    static long total_pixels_bytes = 0;
    static int width = 0, height = 0;
    static int bytes_per_line = 0;
    static int header_sent = 0;
    static long total_read = 0;

    if (!fp)
    {

        fp = fopen(CUSTOM_IMAGE_PATH, "rb");
        if (!fp)
            return SANE_STATUS_IO_ERROR;

        char magic[3];
        int width, height, max_val;
        fscanf(fp, "%2s", magic);
        fscanf(fp, "%d %d", &width, &height);
        fscanf(fp, "%d", &max_val);
        fgetc(fp);
        pixel_data_offset = ftell(fp);

        fseek(fp, 0, SEEK_END);
        total_read = ftell(fp);
        rewind(fp);

        size_t actual_read = fread(data, 1, pixel_data_offset, fp);

        total_read -= actual_read;

        *length = actual_read;

        return SANE_STATUS_GOOD;
    }

    if (total_read <= 0)
    {
        *length = 0;
        fclose(fp);
        fp = NULL;
        return SANE_STATUS_EOF;
    }

    size_t rgb_block_size = (max_length / 3) * 3;
    size_t read_size = fmin(rgb_block_size, total_read);
    size_t actual_read = fread(data, 1, read_size, fp);
    total_read -= actual_read;

    *length = actual_read;

    // Swap R, G, B channels
    for (int i = 0; i < *length; i += 3)
    {
        SANE_Byte temp = data[i + 1];
        data[i + 1] = data[i + 2];
        data[i + 2] = data[i];
        data[i] = temp;
    }

    return SANE_STATUS_GOOD;
}
Enter fullscreen mode Exit fullscreen mode

Explanation

  • The image file path is defined as CUSTOM_IMAGE_PATH.
  • The sane_custom_scanner_get_parameters function retrieves image parameters such as width, height, and format.
  • The sane_custom_scanner_start function starts the scanning process and should be triggered only once.
  • The sane_custom_scanner_read function reads the image data and returns it to the frontend.

Note: The R, G, and B channels need to be swapped to display the image correctly. By default, the image may appear with incorrect colors.

wrong color

Step 2: Build and Install the Backend

All SANE backends are located in /usr/lib/x86_64-linux-gnu/sane/. Use gcc to build the shared library and copy it to the appropriate directory:

gcc -shared -fPIC -o libsane-custom_scanner.so.1 custom_scanner.c -lsane -I/usr/include/sane
sudo cp libsane-mock_sane.so.1 /usr/lib/x86_64-linux-gnu/sane/
Enter fullscreen mode Exit fullscreen mode

Next, update the SANE configuration file /etc/sane.d/dll.conf to include the custom scanner backend:

echo "custom_scanner" | sudo tee -a /etc/sane.d/dll.conf
Enter fullscreen mode Exit fullscreen mode

Step 3: Test the Virtual Scanner

Test the virtual scanner using the scanimage command:

scanimage -d custom_scanner > output.pnm
Enter fullscreen mode Exit fullscreen mode

To troubleshoot issues, enable debug output by running:

SANE_DEBUG_DLL=255 scanimage -d custom_scanner > output.pnm
Enter fullscreen mode Exit fullscreen mode

Finally, test the virtual scanner with the Dynamic Web TWAIN online demo, which supports document scanning and image processing.

Dynamic Web TWAIN Linux Virtual Scanner

Source Code

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

Top comments (0)