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.

What you’ll build: A PySide6 desktop app that embeds a local Dynamic Web TWAIN scanning page inside QWebEngineView, then sends captured document images to Python for barcode decoding with Dynamsoft Barcode Reader.

Key Takeaways

  • PySide6 can host a full cross-platform scanning UI by embedding a DWT-powered HTML page inside QWebEngineView.
  • Dynamic Web TWAIN v19 must be served from http://127.0.0.1 rather than file://, so the app starts a lightweight local HTTP server in a background thread.
  • Python triggers browser-side scan and load actions through runJavaScript, which keeps the Qt UI and DWT frontend loosely coupled.
  • QWebChannel is the bridge that lets JavaScript transfer captured document data back to Python for barcode recognition.

Common Developer Questions

How do I build a desktop document scanner with Qt and Python?

Create a PySide6 desktop shell with QWebEngineView, load a local web page that uses Dynamic Web TWAIN for acquisition, and add Python buttons for scan, load, and barcode-read actions. That hybrid structure gives you native desktop UI plus browser-based scanner access in one window.

Why does Dynamic Web TWAIN need a local HTTP server inside a PySide6 app?

Dynamic Web TWAIN v19 validates the page origin and rejects file:// pages during initialization, so opening an HTML file directly from disk is not enough. Serving the assets from a tiny local http.server instance satisfies the origin check while still keeping everything local to the desktop app.

How do I send scanned document data from JavaScript to Python in a Qt app?

Use QWebChannel to expose a Python object to the embedded page and let JavaScript call back into that object after DWT has loaded or exported the image data. The Python side can then feed the received bytes into Dynamsoft Barcode Reader and render the decoded results in Qt widgets.

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

Get the complete sample project source code on GitHub