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
-
Install Dynamsoft Service.
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 anImage
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