How to Integrate Dynamic Web TWAIN into Flutter Windows Desktop Application

Xiao Ling
5 min readMar 9, 2023

Previously, I created a flutter_web_twain to assist developers in building web-based document scanning applications with Flutter and Dynamic Web TWAIN. In this article, I will demonstrate how to integrate Dynamic Web TWAIN into a Flutter Windows desktop application using Flutter Windows webview plugin.

Section 1: Setting Up the Flutter Project

  1. Create a new Flutter desktop application project
 flutter create web_twain_desktop

2. Install the Flutter webview_windows plugin.

cd web_twain_desktop
flutter pub add webview_windows

3. Create an assets folder under the lib folder and download Dynamic Web TWAIN SDK to the assets folder via npm.

cd lib/assets
npm install dwt

node_modules:
├── dwt
│ ├── dist

4. Configure the assets in pubspec.yaml file.

assets:
- lib/assets/
- lib/assets/node_modules/
- lib/assets/node_modules/dwt/
- lib/assets/node_modules/dwt/dist/
- lib/assets/node_modules/dwt/dist/addon/
- lib/assets/node_modules/dwt/dist/dist/
- lib/assets/node_modules/dwt/dist/src/
- lib/assets/node_modules/dwt/dist/types/

Note: Since only files directly located in the directory are included, we must add all subdirectories under the dwt folder to the assets list to ensure that all resource files are packaged. If some resource files are missing, Dynamic Web TWAIN may not function properly.

Section 2: Configuring Dynamic Web TWAIN

  1. Dynamic Web TWAIN consists of Dynamsoft service and JavaScript libraries. The Dynamsoft service is responsible for managing the communication between the JavaScript SDK and the scanner. Thus, the first step is to install node_modules/dwt/dist/dist/DynamsoftServiceSetup.msi on Windows.
  2. Create an index.html file under the assets folder.
  3. Include the dynamsoft.webtwain.min.js file in the index.html file.
<script src="node_modules/dwt/dist/dynamsoft.webtwain.min.js"></script>

4. Apply for a license key to enable the SDK.

Dynamsoft.DWT.ProductKey = "LICENSE-KEY";

5. Specify the resource path for the SDK.

Dynamsoft.DWT.ResourcesPath = "node_modules/dwt/dist/";

6. Initialize the SDK with an HTML div element:

<style>
.container {
position: absolute;
top: 10%;
left: 10;
}
</style>
<div id="document-container" class="container"></div>
<script>
var dwtObject = null;
Dynamsoft.DWT.CreateDWTObjectEx({ "WebTwainId": "container" }, (obj) => {
dwtObject = obj;

dwtObject.Viewer.bind(document.getElementById("document-container"));
dwtObject.Viewer.width = 640;
dwtObject.Viewer.height = 640;
dwtObject.Viewer.show();
onReady();
}, (errorString) => {
console.log(errorString);
});

function onReady() {
if (dwtObject != null) {

dwtObject.IfShowUI = false;

dwtObject.GetDevicesAsync(Dynamsoft.DWT.EnumDWT_DeviceType.TWAINSCANNER | Dynamsoft.DWT.EnumDWT_DeviceType.ESCLSCANNER).then((sources) => {
sourceList = sources;

for (let i = 0; i < sources.length; i++) {
sourceNames.push(sources[i].displayName);
}

if (sources.length > 0) {
if (window.chrome.webview != "undefined") {
var param = {
"event": "sourceNames",
"data": sourceNames
}
window.chrome.webview.postMessage(param);
}
}
});
}
}
</script>

The window.chrome.webview.postMessage() method is used to send messages from JavaScript to Flutter. We will use this method to send the list of available scanners to Flutter.

7. Add event listener to handle method calls from Flutter.

if (window.chrome.webview != "undefined") {
window.chrome.webview.addEventListener('message', function (event) {
let data = JSON.parse(JSON.stringify(event.data));
if (data.event === "acquire") {
acquireImage(data.index);
}
else if (data.event === "load") {
openImage();
}
else if (data.event === "removeAll") {
removeAll();
}
else if (data.event === "removeSelected") {
removeSelected();
}
else if (data.event === "download") {
downloadDocument();
}
});
}

8. Implement the methods in JavaScript:

function removeAll() {
if (!dwtObject || dwtObject.HowManyImagesInBuffer == 0)
return;

dwtObject.RemoveAllImages();
}

function removeSelected() {
if (!dwtObject || dwtObject.HowManyImagesInBuffer == 0)
return;

dwtObject.RemoveImage(dwtObject.CurrentImageIndexInBuffer);
}

function openImage() {
if (!dwtObject)
return;
dwtObject.Addon.PDF.SetConvertMode(Dynamsoft.DWT.EnumDWT_ConvertMode.CM_RENDERALL);
let ret = dwtObject.LoadImageEx("", Dynamsoft.DWT.EnumDWT_ImageType.IT_ALL);
}

function acquireImage(index) {
if (!dwtObject)
return;

if (sourceList.length > 0) {
dwtObject.SelectDeviceAsync(sourceList[index]).then(() => {
return dwtObject.OpenSourceAsync()
}).then(() => {
return dwtObject.AcquireImageAsync({
})
}).then(() => {
if (dwtObject) {
dwtObject.CloseSource();
}
}).catch(
(e) => {
console.error(e)
}
)
}
}

function downloadDocument() {
if (dwtObject) {
dwtObject.SaveAllAsPDF("DynamicWebTWAIN.pdf");
}
}

Section 3: Building the Flutter UI for Document Scanning

Let’s go to lib/main.dart to implement the Flutter logic.

  1. Initialize the WebviewController:
function removeAll() {
if (!dwtObject || dwtObject.HowManyImagesInBuffer == 0)
return;

dwtObject.RemoveAllImages();
}

function removeSelected() {
if (!dwtObject || dwtObject.HowManyImagesInBuffer == 0)
return;

dwtObject.RemoveImage(dwtObject.CurrentImageIndexInBuffer);
}

function openImage() {
if (!dwtObject)
return;
dwtObject.Addon.PDF.SetConvertMode(Dynamsoft.DWT.EnumDWT_ConvertMode.CM_RENDERALL);
let ret = dwtObject.LoadImageEx("", Dynamsoft.DWT.EnumDWT_ImageType.IT_ALL);
}

function acquireImage(index) {
if (!dwtObject)
return;

if (sourceList.length > 0) {
dwtObject.SelectDeviceAsync(sourceList[index]).then(() => {
return dwtObject.OpenSourceAsync()
}).then(() => {
return dwtObject.AcquireImageAsync({
})
}).then(() => {
if (dwtObject) {
dwtObject.CloseSource();
}
}).catch(
(e) => {
console.error(e)
}
)
}
}

function downloadDocument() {
if (dwtObject) {
dwtObject.SaveAllAsPDF("DynamicWebTWAIN.pdf");
}
}

2. Find the path of the index.html file and load it into the WebviewController:

final assetsDirectory = join(dirname(Platform.resolvedExecutable), 'data', 'flutter_assets', "lib/assets/index.html");

await _controller.loadUrl(Uri.file(assetsDirectory).toString());

3. Add event listener to handle messages from JavaScript:

_controller.webMessage.listen((event) {
if (event['event'] == null) return;

if (event['event'] == 'sourceNames') {
_sourceNames.clear();
for (var item in event['data']) {
_sourceNames.add(item.toString());
}

if (_sourceNames.isNotEmpty) {
setState(() {
_selectedItem = _sourceNames[0];
});
}
}
});

Here we get the physical scanner names from JavaScript and store them in the _sourceNames list. We also set the first item in the list as the default selected item.

4. Use Stack layout to show the web view:

Stack(
children: [
Webview(
_controller,
),
Positioned(
top: 10,
left: 10,
child: Row())]
)

5. Add a DropdownButton to select the physical scanner in Row:

DropdownButton<String>(
value: _selectedItem,
items: _sourceNames
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue == null || newValue == '') return;
setState(() {
_selectedItem = newValue;
});
},
),

6. Add five ElevatedButton s for scanning a document, loading images, removing a selected image, removing all images, and saving all acquired images to a PDF file.

ElevatedButton(
onPressed: () async {
await _controller.postWebMessage(json.encode({
"event": "acquire",
"index": _sourceNames.indexOf(_selectedItem)
}));
},
child: const Text("Scan Documents")),
const SizedBox(
width: 10,
height: 10,
),
ElevatedButton(
onPressed: () async {
await _controller
.postWebMessage(json.encode({"event": "load"}));
},
child: const Text("Load Documents")),
const SizedBox(
width: 10,
height: 10,
),
ElevatedButton(
onPressed: () async {
await _controller.postWebMessage(
json.encode({"event": "removeSelected"}));
},
child: const Text("Remove Selected")),
const SizedBox(
height: 10,
width: 10,
),
ElevatedButton(
onPressed: () async {
await _controller.postWebMessage(
json.encode({"event": "removeAll"}));
},
child: const Text("Remove All")),
const SizedBox(
width: 10,
height: 10,
),
ElevatedButton(
onPressed: () async {
await _controller
.postWebMessage(json.encode({"event": "download"}));
},
child: const Text("Download Documents"))

7. Run the Flutter Windows desktop app to scan documents from a physical scanner:

flutter run -d windows

Source Code

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

Originally published at https://www.dynamsoft.com on March 9, 2023.

--

--

Xiao Ling

Manager of Dynamsoft Open Source Projects | Tech Lover