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

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

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 []

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 ""

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

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 ''

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

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

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'])

To generate the wheel file, run the following command:

python setup.py bdist_wheel

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

We can scaffold a new flet project as follows:

flet create myapp
cd myapp

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
    )
  • 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

python-flet-twain-document-scanner

Source Code

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