How to Integrate Dynamic Web TWAIN into Flutter Windows Desktop Application
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
- Create a new Flutter desktop application project
flutter create web_twain_desktop
- Install the Flutter
webview_windows
plugin.cd web_twain_desktop flutter pub add webview_windows
- Create an
assets
folder under thelib
folder and download Dynamic Web TWAIN SDK to theassets
folder via npm.cd lib/assets npm install dwt node_modules: ├── dwt │ ├── dist
- 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
- 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. - Create an
index.html
file under the assets folder. - Include the
dynamsoft.webtwain.min.js
file in theindex.html
file.<script src="node_modules/dwt/dist/dynamsoft.webtwain.min.js"></script>
- Apply for a license key to enable the SDK.
Dynamsoft.DWT.ProductKey = "LICENSE-KEY";
- Specify the resource path for the SDK.
Dynamsoft.DWT.ResourcesPath = "node_modules/dwt/dist/";
-
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. - 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(); } }); }
-
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.
- Initialize the
WebviewController
:final List<String> _sourceNames = []; String _selectedItem = ''; final _controller = WebviewController(); @override void initState() { super.initState(); initPlatformState(); } Future<void> initPlatformState() async { await _controller.initialize(); }
- Find the path of the
index.html
file and load it into theWebviewController
:final assetsDirectory = join(dirname(Platform.resolvedExecutable), 'data', 'flutter_assets', "lib/assets/index.html"); await _controller.loadUrl(Uri.file(assetsDirectory).toString());
- 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. -
Use
Stack
layout to show the web view:Stack( children: [ Webview( _controller, ), Positioned( top: 10, left: 10, child: Row())] )
- Add a
DropdownButton
to select the physical scanner inRow
: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; }); }, ),
-
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"))
- Run the Flutter Windows desktop app to scan documents from a physical scanner:
flutter run -d windows