Building a Desktop Document Scanning and Barcode Recognition Application with Qt and Python

This article demonstrates how to build a desktop application that scans physical documents and reads barcodes from them — all within a single Python window. The application combines PySide6 for the native UI, Dynamic Web TWAIN (DWT) for scanner integration, and Dynamsoft Barcode Reader for barcode decoding. DWT is a cross-platform JavaScript library with native scanner support on Windows, macOS, and Linux, making it a solid choice for embedding document acquisition into any web-based or hybrid desktop application.

Prerequisites

  • Dynamic Web TWAIN: A cross-platform JavaScript library for document scanning. It supports a wide range of image acquisition sources, including TWAIN/WIA/SANE scanners, webcams, and local image files.

      npm install dwt
    
  • Dynamsoft Barcode Reader: A high-performance barcode scanning SDK for web, desktop, and mobile platforms. It recognizes 1D, 2D, and postal barcode formats.

      pip install dynamsoft-barcode-reader-bundle
    
  • PySide6: The official Python bindings for Qt6. A single pip package bundles QtWidgets, QtWebEngineWidgets, QtWebEngineCore, and QtWebChannel.

      pip install PySide6
    
  • A License Key for Dynamsoft Products

Steps to Build a Cross-Platform Document Scanning and Barcode Recognition Application

The application is a hybrid that combines an HTML5/JavaScript front end (powered by DWT) with a Python back end (powered by PySide6 and Dynamsoft Barcode Reader).

1. Create a Qt Application with PySide6 Widgets

The UI requires three types of widget:

  • QWebEngineView: Hosts the HTML/JavaScript page and renders document images acquired by DWT.
  • QPushButton: Three buttons — Scan Document, Load Document, and Read Barcode.
  • QTextEdit: Displays the barcode recognition results.

First, create an empty Qt window:

from PySide6.QtWidgets import *
from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtWebChannel import QWebChannel

app = QApplication([])
win = QWidget()
win.setWindowTitle('Dynamic Web TWAIN and Dynamsoft Barcode Reader')
win.show()
app.exec()

Note that in PySide6, the application event loop is started with app.exec().

Next, create a layout and add the widgets to it:

class WebView(QWebEngineView):
    def __init__(self):
        QWebEngineView.__init__(self)

layout = QVBoxLayout()
win.setLayout(layout)

view = WebView()
bt_scan = QPushButton('Scan Document')
bt_load = QPushButton('Load Document')
bt_read = QPushButton('Read Barcode')
text_area = QTextEdit()

layout.addWidget(view)
layout.addWidget(bt_scan)
layout.addWidget(bt_load)
layout.addWidget(bt_read)
layout.addWidget(text_area)

Two keyboard shortcuts improve usability: press R to reload the web view and Q to quit the application:

def keyPressEvent(event):
    if event.key() == Qt.Key.Key_Q:
        win.close()
    elif event.key() == Qt.Key.Key_R:
        view.reload()

win.keyPressEvent = keyPressEvent

2. Serve the Web Page via a Local HTTP Server

Dynamic Web TWAIN v19 validates the page origin at initialization time and rejects file:// URLs with an origin mismatch error. To satisfy this requirement, the application starts a lightweight http.server in a background daemon thread and serves all files from http://127.0.0.1:8000:

import http.server
import socketserver
import threading

class HttpDaemon(threading.Thread):
    def __init__(self, port):
        threading.Thread.__init__(self)
        self.port = port
        self.daemon = True

    def run(self):
        Handler = http.server.SimpleHTTPRequestHandler
        os.chdir(os.path.dirname(os.path.abspath(__file__)))
        with socketserver.TCPServer(("", self.port), Handler) as httpd:
            httpd.serve_forever()

http_daemon = HttpDaemon(8000)
http_daemon.start()

Load the page from that origin inside WebView. Two PySide6-specific details apply here: QWebEngineSettings has moved to QtWebEngineCore, and all settings enumerators must use their fully qualified WebAttribute.* form:

from PySide6.QtWebEngineCore import QWebEngineSettings

class WebView(QWebEngineView):
    def __init__(self):
        QWebEngineView.__init__(self)

        self.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True)
        self.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True)
        self.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalStorageEnabled, True)

        self.load(QUrl("http://127.0.0.1:8000/index.html"))

3. Implement Document Scanning and Loading in JavaScript

The index.html file handles two document input methods: scanning from a physical device and loading a local file.

Scan from a physical scanner:

var sourceList = [];

async function acquireImage() {
    if (!dwtObject) { alert("DWT not ready."); return; }
    if (sourceList.length > 0) {
        try {
            await dwtObject.SelectDeviceAsync(sourceList[selectSources.selectedIndex]);
            await dwtObject.OpenSourceAsync();
            await dwtObject.AcquireImageAsync({ IfDisableSourceAfterAcquire: true });
        } catch (e) { console.error(e); }
    } else {
        alert("No Source Available!");
    }
}

AcquireImageAsync must be fully awaited. Without it, the scanner source closes immediately and no image is captured.

Load a local image file:

Trigger a hidden <input type="file"> element to open the OS file picker, then pass the selected file to LoadImageFromBinary:

<input type="file" id="localFileInput" accept="image/*,.pdf" style="display:none;"
       onchange="onFileSelected(event);" />
function loadImage() {
    if (!dwtObject) return;
    if (dwtObject.Addon && dwtObject.Addon.PDF) {
        dwtObject.Addon.PDF.SetConvertMode(Dynamsoft.DWT.EnumDWT_ConvertMode.CM_RENDERALL);
    }
    var fileInput = document.getElementById("localFileInput");
    fileInput.value = "";
    fileInput.click();
}

function onFileSelected(e) {
    var file = e.target.files[0];
    if (!file || !dwtObject) return;
    dwtObject.LoadImageFromBinary(
        file,
        function () { console.log("Image loaded:", file.name); },
        function (errorCode, errorString) { console.error("LoadImageFromBinary error:", errorString); }
    );
}

On the Python side, each button’s click handler calls the corresponding JavaScript function via runJavaScript:

def acquire_image():
    view.page().runJavaScript('acquireImage();')

def load_image():
    view.page().runJavaScript('loadImage();')

bt_scan.clicked.connect(acquire_image)
bt_load.clicked.connect(load_image)

4. Transfer Images from JavaScript to Python via QWebChannel

Once a document is in the DWT image buffer, getCurrentImage converts it to a base64-encoded JPEG string and passes it to the Python Backend object through QWebChannel:

var backend;
new QWebChannel(qt.webChannelTransport, function (channel) {
    backend = channel.objects.backend;
});

function getCurrentImage() {
    if (dwtObject) {
        dwtObject.ConvertToBase64(
            [dwtObject.CurrentImageIndexInBuffer],
            Dynamsoft.DWT.EnumDWT_ImageType.IT_JPG,
            function (result, indices, type) {
                backend.onDataReady(result.getData(0, result.getLength()));
            },
            function (errorCode, errorString) {
                console.log(errorString);
            }
        );
    }
}

The Python Backend class receives and processes this data:

class Backend(QObject):
    @Slot(str)
    def onDataReady(self, base64img):
        imgdata = base64.b64decode(base64img)
        # ... decode barcodes ...

class WebView(QWebEngineView):
    def __init__(self):
        QWebEngineView.__init__(self)
        # ...
        self.backend = Backend(self)
        self.channel = QWebChannel(self.page())
        self.channel.registerObject('backend', self.backend)
        self.page().setWebChannel(self.channel)

def read_barcode():
    view.page().runJavaScript('getCurrentImage();')

bt_read.clicked.connect(read_barcode)

5. Decode Barcodes from Scanned Documents

With the raw image bytes available in Python, Dynamsoft Barcode Reader performs the barcode decoding:

from dynamsoft_barcode_reader_bundle import *
import base64

# Initialize license (call once at startup)
errorCode, errorMsg = LicenseManager.init_license('LICENSE-KEY')
if errorCode != EnumErrorCode.EC_OK and errorCode != EnumErrorCode.EC_LICENSE_WARNING:
    print('License initialization failed:', errorCode, errorMsg)

cvr_instance = CaptureVisionRouter()

class Backend(QObject):
    @Slot(str)
    def onDataReady(self, base64img):
        imgdata = base64.b64decode(base64img)

        try:
            result = cvr_instance.capture(bytes(imgdata), EnumPresetTemplate.PT_READ_BARCODES)
            if result.get_error_code() not in [EnumErrorCode.EC_OK, EnumErrorCode.EC_LICENSE_WARNING]:
                print('Capture error:', result.get_error_string())
                return
            barcode_result = result.get_decoded_barcodes_result()
            if barcode_result is None:
                text_area.setText('No barcode found.')
                return
            items = barcode_result.get_items()
            out = ''
            for item in items:
                out += 'Barcode Format : ' + item.get_format_string() + '\n'
                out += 'Barcode Text : ' + item.get_text() + '\n'
                out += '-------------------------------------------------\n'
            text_area.setText(out)
        except Exception as e:
            print(e)

Qt application: document scanning and barcode reading

Source Code

https://github.com/yushulx/web-twain-document-scan-management/tree/main/examples/qt