How to Scan Documents from TWAIN, WIA, SANE Compatible Scanners in Python

Xiao Ling - Sep 28 '23 - - Dev Community

Dynamsoft Service streamlines document scanner programming for developers by providing a unified REST API that works with TWAIN, WIA, eSCL, SANE and ICA drivers. We have previously released a Node.js package and a Flutter plugin to interact with Dynamsoft Service's REST API. In this article, we will extend this convenience to Python development.

Python Package

https://pypi.org/project/twain-wia-sane-scanner/

pip install twain-wia-sane-scanner
Enter fullscreen mode Exit fullscreen mode

Prerequisites

  1. Install Dynamsoft Service.

  2. Request a free trial license.

Building a Python Package for Acquiring Document Images

A Python package primarily consists of two files: __init__.py and setup.py.

init.py

We create a dynamsoftservice folder and add a __init__.py file to it.

The __init__.py file contains two classes: ScannerType and ScannerController.

  • ScannerType is an enumeration of scanner types.

    class ScannerType:
        TWAINSCANNER = 0x10
        WIASCANNER = 0x20
        TWAINX64SCANNER = 0x40
        ICASCANNER = 0x80
        SANESCANNER = 0x100
        ESCLSCANNER = 0x200
        WIFIDIRECTSCANNER = 0x400
        WIATWAINSCANNER = 0x800
    
  • ScannerController is a class that provides methods for interacting with Dynamsoft Service.

    class ScannerController:
    
        def getDevices(self, host: str, scannerType: int = None) -> List[Any]:
            return []
    
        def scanDocument(self, host: str, parameters: Dict[str, Any]) -> str:
            return ""
    
        def deleteJob(self, host: str, jobId: str) -> None:
            pass
    
        def getImageFile(self, host, job_id, directory):
            return ''
    
        def getImageFiles(self, host: str, jobId: str, directory: str) -> List[str]:
            return []
    
        def getImageStreams(self, host: str, jobId: str) -> List[bytes]:
            return []
    

The getDevices() method returns a list of scanners.

def getDevices(self, host: str, scannerType: int = None) -> List[Any]:
    devices = []
    url = f"{host}/DWTAPI/Scanners"
    if scannerType is not None:
        url += f"?type={scannerType}"

    try:
        response = requests.get(url)
        if response.status_code == 200 and response.text:
            devices = json.loads(response.text)
            return devices
    except Exception as error:
        pass
    return []
Enter fullscreen mode Exit fullscreen mode

The scanDocument() method starts a scan job.

def scanDocument(self, host: str, parameters: Dict[str, Any]) -> str:
    url = f"{host}/DWTAPI/ScanJobs"
    try:
        response = requests.post(url, json=parameters, headers={
                                    'Content-Type': 'application/text'})
        jobId = response.text
        if response.status_code == 201:
            return jobId
    except Exception as error:
        pass
    return ""
Enter fullscreen mode Exit fullscreen mode

The deleteJob() method deletes a scan job.

def deleteJob(self, host: str, jobId: str) -> None:
    if not jobId:
        return
    url = f"{host}/DWTAPI/ScanJobs/{jobId}"
    try:
        response = requests.delete(url)
        if response.status_code == 200:
            pass
    except Exception as error:
        pass
Enter fullscreen mode Exit fullscreen mode

The getImageFile() method fetches a document image from Dynamsoft service to a specified directory.

def getImageFile(self, host, job_id, directory):
    url = f"{host}/DWTAPI/ScanJobs/{job_id}/NextDocument"
    try:
        response = requests.get(url, stream=True)
        if response.status_code == 200:
            timestamp = str(int(time.time() * 1000))
            filename = f"image_{timestamp}.jpg"
            image_path = os.path.join(directory, filename)
            with open(image_path, 'wb') as f:
                f.write(response.content)
            return filename
    except Exception as e:
        print("No more images.")
        return ''
    return ''
Enter fullscreen mode Exit fullscreen mode

The getImageFiles() method consecutively fetch all document images until the job is finished.

def getImageFiles(self, host: str, jobId: str, directory: str) -> List[str]:
    images = []
    while True:
        filename = self.getImageFile(host, jobId, directory)
        if filename == '':
            break
        else:
            images.append(filename)
    return images
Enter fullscreen mode Exit fullscreen mode

The getImageStreams() method returns a list of document image streams.

def getImageStreams(self, host: str, jobId: str) -> List[bytes]:
    streams = []
    url = f"{host}/DWTAPI/ScanJobs/{jobId}/NextDocument"
    while True:
        try:
            response = requests.get(url)
            if response.status_code == 200:
                streams.append(response.content)
            elif response.status_code == 410:
                break
        except Exception as error:
            break
    return streams
Enter fullscreen mode Exit fullscreen mode

setup.py

The setup.py file is used to build the Python package. It defines the package name, version, description, author, install_requires and so on.

from setuptools.command import build_ext
from setuptools import setup
import os
import io
from setuptools.command.install import install
import shutil

long_description = io.open("README.md", encoding="utf-8").read()

setup(name='twain-wia-sane-scanner',
      version='1.0.0',
      description='A Python package for digitizing documents from TWAIN, WIA, SANE, ICA and eSCL compatible scanners.',
      long_description=long_description,
      long_description_content_type="text/markdown",
      author='yushulx',
      url='https://github.com/yushulx/twain-wia-sane-scanner',
      license='MIT',
      packages=['dynamsoftservice'],
      classifiers=[
           "Development Status :: 5 - Production/Stable",
           "Environment :: Console",
           "Intended Audience :: Developers",
          "Intended Audience :: Education",
          "Intended Audience :: Information Technology",
          "Intended Audience :: Science/Research",
          "License :: OSI Approved :: MIT License",
          "Operating System :: Microsoft :: Windows",
          "Operating System :: MacOS",
          "Operating System :: POSIX :: Linux",
          "Programming Language :: Python",
          "Programming Language :: Python :: 3",
          "Programming Language :: Python :: 3 :: Only",
          "Programming Language :: Python :: 3.6",
          "Programming Language :: Python :: 3.7",
          "Programming Language :: Python :: 3.8",
          "Programming Language :: Python :: 3.9",
          "Programming Language :: Python :: 3.10",
          "Topic :: Scientific/Engineering",
          "Topic :: Software Development",
      ],
      install_requires=['requests'])
Enter fullscreen mode Exit fullscreen mode

To generate the wheel file, run the following command:

python setup.py bdist_wheel
Enter fullscreen mode Exit fullscreen mode

Creating a Desktop GUI App with Flet

Flet, powered by Flutter, is a framework that enables developers to easily build web, mobile, and desktop apps in Python. No frontend experience is required.

python install flet
Enter fullscreen mode Exit fullscreen mode

We can scaffold a new flet project as follows:

flet create myapp
cd myapp
Enter fullscreen mode Exit fullscreen mode

In the main.py file, add some UI controls:

def main(page: ft.Page):
    page.title = "Document Scanner"

    buttonDevice = ft.ElevatedButton(
        text="Get Devices", on_click=get_device)

    dd = ft.Dropdown(on_change=dropdown_changed,)
    buttonScan = ft.ElevatedButton(text="Scan", on_click=scan_document)
    row = ft.Row(spacing=10, controls=[
                 buttonDevice, dd, buttonScan], alignment=ft.MainAxisAlignment.CENTER,)

    lv = ft.ListView(expand=1, spacing=10, padding=20, auto_scroll=True)

    page.add(
        row, lv
    )
Enter fullscreen mode Exit fullscreen mode
  • The button buttonDevice is used to get all available scanners.

    def get_device(e):
        devices.clear()
        dd.options = []
        scanners = scannerController.getDevices(
            host, ScannerType.TWAINSCANNER | ScannerType.TWAINX64SCANNER)
        for i, scanner in enumerate(scanners):
            devices.append(scanner)
            dd.options.append(ft.dropdown.Option(scanner['name']))
            dd.value = scanner['name']
    
        page.update()
    
  • The dropdown dd is used to select a scanner.

    def dropdown_changed(e):
        page.update()
    
  • The button buttonScan is used to trigger the document scan. You need to replace the license key with your own. The source of an Image control can be either a file path or a base64-encoded string. In this case, we use the latter.

    def scan_document(e):
        if len(devices) == 0:
            return
    
        device = devices[0]["device"]
    
        for scanner in devices:
            if scanner['name'] == dd.value:
                device = scanner['device']
                break
    
        parameters = {
            "license": license_key,
            "device": device,
        }
    
        parameters["config"] = {
            "IfShowUI": False,
            "PixelType": 2,
            "Resolution": 200,
            "IfFeederEnabled": False,
            "IfDuplexEnabled": False,
        }
    
        job_id = scannerController.scanDocument(host, parameters)
    
        if job_id != "":
            images = scannerController.getImageStreams(host, job_id)
            for i, image in enumerate(images):
                base64_encoded = base64.b64encode(image)
                display = ft.Image(src_base64=base64_encoded.decode('utf-8'), width=600,
                                   height=600,
                                   fit=ft.ImageFit.CONTAIN,)
                lv.controls.append(display)
    
            scannerController.deleteJob(host, job_id)
    
        page.update()
    
  • The listview lv is used to append and display document images.

Run the app:

flet run
Enter fullscreen mode Exit fullscreen mode

python-flet-twain-document-scanner

Source Code

https://github.com/yushulx/twain-wia-sane-scanner

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player