How to Scan Documents from TWAIN, WIA, SANE Compatible Scanners in Python
Tags: python, flet, scanner, programming
Published: 2023-09-28 08:36:22

[Dynamsoft Service](https://www.dynamsoft.com/blog/announcement/dynamsoft-service-restful-api/) 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](https://www.npmjs.com/package/docscan4nodejs) and a [Flutter plugin](https://pub.dev/packages/flutter_twain_scanner) 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/](https://pypi.org/project/twain-wia-sane-scanner/)

```bash
pip install twain-wia-sane-scanner
```

## Prerequisites
1. Install Dynamsoft Service.
    - Windows: [Dynamsoft-Service-Setup.msi](https://demo.dynamsoft.com/DWT/DWTResources/dist/DynamsoftServiceSetup.msi)
    - macOS: [Dynamsoft-Service-Setup.pkg](https://demo.dynamsoft.com/DWT/DWTResources/dist/DynamsoftServiceSetup.pkg)
    - Linux: 
        - [Dynamsoft-Service-Setup.deb](https://demo.dynamsoft.com/DWT/DWTResources/dist/DynamsoftServiceSetup.deb)
        - [Dynamsoft-Service-Setup-arm64.deb](https://demo.dynamsoft.com/DWT/DWTResources/dist/DynamsoftServiceSetup-arm64.deb)
        - [Dynamsoft-Service-Setup-mips64el.deb](https://demo.dynamsoft.com/DWT/DWTResources/dist/DynamsoftServiceSetup-mips64el.deb)
        - [Dynamsoft-Service-Setup.rpm](https://demo.dynamsoft.com/DWT/DWTResources/dist/DynamsoftServiceSetup.rpm)
        
2. Request a [free trial license](https://www.dynamsoft.com/customer/license/trialLicense?product=dwt).

## 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.
    
    ```python 
    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.

    ```python 
    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. 

```python 
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. 

```python 
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. 

```python 
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. 

```python 
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.

```python 
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.

```python 
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.

```python 
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:

```bash
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.

```bash
python install flet
```

We can scaffold a new flet project as follows:

```bash
flet create myapp
cd myapp
```

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

```python
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.
    
    ```python
    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.
    
    ```python
    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.
    
    ```python
    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:

```bash
flet run
```

![python-flet-twain-document-scanner](https://www.dynamsoft.com/codepool/img/2023/09/python-flet-twain-document-scanner.gif)

## Source Code
[https://github.com/yushulx/twain-wia-sane-scanner](https://github.com/yushulx/twain-wia-sane-scanner)