How to Integrate Dynamic Web TWAIN into Flutter Windows Desktop Application
Tags: flutter, javascript, programming, desktop
Published: 2023-03-10 06:25:34

Previously, I created a [flutter_web_twain](https://pub.dev/packages/flutter_web_twain) to assist developers in building web-based document scanning applications with Flutter and [Dynamic Web TWAIN](https://www.dynamsoft.com/web-twain/overview/). In this article, I will demonstrate how to integrate Dynamic Web TWAIN into a Flutter Windows desktop application using [Flutter Windows webview plugin](https://pub.dev/packages/webview_windows). 

![Flutter windows desktop document management](https://www.dynamsoft.com/codepool/img/2023/03/flutter-windows-desktop-web-twain.gif)

## Section 1: Setting Up the Flutter Project
1. Create a new Flutter desktop application project
    ```bash
    flutter create web_twain_desktop
    ```
2. Install the Flutter `webview_windows` plugin.
    ```bash
    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. 
    ```bash
    cd lib/assets
    npm install dwt

    node_modules:   
    ├── dwt
    │   ├── dist
    ```
4. Configure the assets in `pubspec.yaml` file.
    ```yaml
    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.
    ```html
    <script src="node_modules/dwt/dist/dynamsoft.webtwain.min.js"></script>
    ```
4. Apply for a [license key](https://www.dynamsoft.com/customer/license/trialLicense?product=dwt) to enable the SDK.
    ```javascript
    Dynamsoft.DWT.ProductKey = "LICENSE-KEY";
    ```
5. Specify the resource path for the SDK.
    ```javascript
    Dynamsoft.DWT.ResourcesPath = "node_modules/dwt/dist/";
    ```

6. Initialize the SDK with an HTML div element:

    ```html
    <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.
    ```javascript
    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:

    ```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`:
    ```dart
    final List<String> _sourceNames = [];
    String _selectedItem = '';
    final _controller = WebviewController();

    @override
    void initState() {
        super.initState();
        initPlatformState();
    }

    Future<void> initPlatformState() async {
        await _controller.initialize();
    }
    ```
2. Find the path of the `index.html` file and load it into the `WebviewController`:
    ```dart
    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:
    ```dart
    _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:

    ```dart
    Stack(
        children: [
        Webview(
            _controller,
        ),
        Positioned(
            top: 10,
            left: 10,
            child: Row())]
    )
    ```
5. Add a `DropdownButton` to select the physical scanner in `Row`:
    ```dart
    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.

    ```dart
    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:
    ```bash
    flutter run -d windows
    ```

    ![Flutter windows desktop web twain scanner](https://www.dynamsoft.com/codepool/img/2023/03/web-twain-desktop-scanner.png)

## Source Code
[https://github.com/yushulx/web-twain-document-scan-management/tree/main/examples/flutter_windows_desktop](https://github.com/yushulx/web-twain-document-scan-management/tree/main/examples/flutter_windows_desktop)