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, andQtWebChannel.pip install PySide6
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)

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