Building a Document Digitization App with Flutter for TWAIN, WIA, and eSCL Scanners
Tags: flutter, dart, scanner, tutorial
Published: 2023-09-15 06:58:46

Dynamsoft Service provides compatibility with all major document scanner protocols, including **TWAIN**, **WIA**, **eSCL**, **SANE** and **ICA**, across **Windows**, **Linux** and **macOS** platforms. Initially part of [Dynamic Web TWAIN](https://www.dynamsoft.com/web-twain/overview/), it will soon offer its core scanning functionalities through a REST API in an upcoming release. This article guides you through creating a Flutter plugin to interact with Dynamsoft Service's REST API. Additionally, you'll learn how to build a cross-platform document digitization app compatible with **Windows**, **Linux**, **macOS**, **Android**, **iOS** and **web**.


<video src="https://user-images.githubusercontent.com/2202306/267881597-0e8072c4-c324-48a3-9ade-6543492f849e.mp4" data-canonical-src="https://user-images.githubusercontent.com/2202306/267881597-0e8072c4-c324-48a3-9ade-6543492f849e.mp4" controls="controls" muted="muted" class="d-block rounded-bottom-2 width-fit" style="max-height:640px;max-width:100%;"></video>

## Flutter TWAIN Scanner Package
[https://pub.dev/packages/flutter_twain_scanner](https://pub.dev/packages/flutter_twain_scanner)


## Prerequisites
- Install [Dynamsoft Service REST Preview for Windows](https://www.dynamsoft.com/codepool/downloads/DynamsoftServiceSetup.msi) on a Windows PC that has one or more document scanners connected to it.
- Request a [free trial license](https://www.dynamsoft.com/customer/license/trialLicense?product=dwt&source=codepool) for Dynamsoft Service.

## Setting Up Dynamsoft Service
1. After installing Dynamsoft Service, navigate to http://127.0.0.1:18625/ in your browser. By default, the REST API's host address is set to `http://127.0.0.1:18622`, which restricts access to the local machine only.
    
    ![Dynamsoft Service default IP](https://www.dynamsoft.com/codepool/img/2023/09/dynamsoft-service-ip-configuration.png)

2. Replace `127.0.0.1` with your LAN IP address, such as `http://192.168.8.72:18622` to enable remote access from other devices and platforms within the same Local Area Network (LAN). To update the IP address, you'll need to select the checkboxes and name the service.

    ![Dynamsoft Service IP change](https://www.dynamsoft.com/codepool/img/2023/09/dynamsoft-service-ip-change.png)

## Dynamsoft Service REST API Reference

| Method | Endpoint        | Description                   | Parameters                         | Response                      |
|--------|-----------------|-------------------------------|------------------------------------|-------------------------------|
| GET    | `/DWTAPI/Scanners`    | Get a list of scanners  | None                               | `200 OK` with scanner list       |
| POST   | `/DWTAPI/ScanJobs`    | Creates a scan job      | `license`, `device`, `config`      | `201 Created` with job ID    |
| GET    | `/DWTAPI/ScanJobs/:id/NextDocument`| Retrieves a document image     | `id`: Job ID   | `200 OK` with image stream    |
| DELETE | `/DWTAPI/ScanJobs/:id`| Deletes a scan job       | `id`: Job ID                      | `200 OK`              |

The parameters are consistent with those outlined in the [Dynamic Web TWAIN documentation](https://www.dynamsoft.com/web-twain/docs/info/api/Interfaces.html#DeviceConfiguration).

## Creating a Flutter Plugin to Interface with Dynamsoft Service's REST API
1. Create a new Flutter plugin project.

    ```bash
    flutter create -t plugin --platforms=android,ios,linux,macos,windows,web flutter_twain_scanner
    ```

2. Install dependent Flutter packages: `http` and `path`.
    
    ```bash
    flutter pub add http path
    ```
3. Create a `lib/dynamsoft_service.dart` file, which contains the core functionalities for interacting with Dynamsoft Service's REST API.
    
    ```dart
    // ignore_for_file: empty_catches

    import 'dart:convert';
    import 'dart:io';
    import 'package:flutter/foundation.dart';
    import 'package:http/http.dart' as http;
    import 'package:path/path.dart';

    class ScannerType {
        static const int TWAINSCANNER = 0x10;
        static const int WIASCANNER = 0x20;
        static const int TWAINX64SCANNER = 0x40;
        static const int ICASCANNER = 0x80;
        static const int SANESCANNER = 0x100;
        static const int ESCLSCANNER = 0x200;
        static const int WIFIDIRECTSCANNER = 0x400;
        static const int WIATWAINSCANNER = 0x800;
    }

    class DynamsoftService {  
        Future<List<dynamic>> getDevices(String host, [int? scannerType]) async {
            List<dynamic> devices = [];
            String url = '$host/DWTAPI/Scanners';
            if (scannerType != null) {
            url += '?type=$scannerType';
            }

            try {
            final response = await http.get(Uri.parse(url));
            if (response.statusCode == 200 && response.body.isNotEmpty) {
                devices = json.decode(response.body);
                return devices;
            }
            } catch (error) {}
            return [];
        }

        Future<String> scanDocument(
            String host, Map<String, dynamic> parameters) async {
            final url = '$host/DWTAPI/ScanJobs';
            try {
            final response = await http.post(
                Uri.parse(url),
                body: json.encode(parameters),
                headers: {'Content-Type': 'application/text'},
            );
            final jobId = response.body;

            if (response.statusCode == 201) {
                return jobId;
            }
            } catch (error) {}
            return '';
        }

        Future<void> deleteJob(String host, String jobId) async {
            if (jobId.isEmpty) return;
            final url = '$host/DWTAPI/ScanJobs/$jobId';

            try {
                final response = await http.delete(Uri.parse(url));
                if (response.statusCode == 200) {}
            } catch (error) {}
        }

        Future<List<String>> getImageFiles(
            String host, String jobId, String directory) async {
            final List<String> images = [];
            final url = '$host/DWTAPI/ScanJobs/$jobId/NextDocument';
            while (true) {
                try {
                    final response = await http.get(Uri.parse(url));
    
                    if (response.statusCode == 200) {
                        final timestamp = DateTime.now().millisecondsSinceEpoch;
                        final imagePath = join(directory, 'image_$timestamp.jpg');
                        final file = File(imagePath);
                        await file.writeAsBytes(response.bodyBytes);
                        images.add(imagePath);
                    } else if (response.statusCode == 410) {
                        break;
                    }
                } catch (error) {
                    break;
                }
            }

            return images;
        }

        Future<List<Uint8List>> getImageStreams(String host, String jobId) async {
            final List<Uint8List> streams = [];
            final url = '$host/DWTAPI/ScanJobs/$jobId/NextDocument';

            while (true) {
                try {
                    final response = await http.get(Uri.parse(url));
                    if (response.statusCode == 200) {
                        streams.add(response.bodyBytes);
                    } else if (response.statusCode == 410) {
                    break;
                    }
                } catch (error) {
                    break;
                }
            }

            return streams;
        }
    }
    ```

Methods:
    - `Future<List<dynamic>> getDevices(String host, [int? scannerType])`: Get the list of TWAIN, WIA, and eSCL compatible scanners.
    - `Future<void> deleteJob(String host, String jobId)`: Deletes a scan job based on the provided job ID.
    - `Future<List<String>> getImageFiles(String host, String jobId, String directory)`: Saves images from a scan job to a directory.
    - `Future<List<Uint8List>> getImageStreams(String host, String jobId)`: Retrieves image streams from a scan job.
    - `Future<String> scanDocument(String host, Map<String, dynamic> parameters)`: Creates a new scan job using provided parameters. 

## Creating a Flutter App for Digitizing Documents from Scanners
1. Create a new Flutter app project.

    ```bash
    flutter create app
    ```
2. Install the package `flutter_twain_scanner`:

    ```bash
    flutter pub add flutter_twain_scanner
    ```

3. In `lib/main.dart`, import the package and set the host address to match the IP address of the machine where Dynamsoft Service is running.

    ```dart
    import 'package:flutter_twain_scanner/dynamsoft_service.dart';

    final DynamsoftService dynamsoftService = DynamsoftService();
    String host = 'http://192.168.8.72:18622'; 
    ```

4. Create a button to list all available scanners, allowing for type-based filtering. For instance, using `ScannerType.TWAINSCANNER | ScannerType.TWAINX64SCANNER` will display scanners that support both 32-bit and 64-bit TWAIN protocols.

    ```dart
    MaterialButton(
        textColor: Colors.white,
        color: Colors.blue,
        onPressed: () async {
            try {
                final scanners = await dynamsoftService.getDevices(host,
                    ScannerType.TWAINSCANNER | ScannerType.TWAINX64SCANNER);
                for (var i = 0; i < scanners.length; i++) {
                devices.add(scanners[i]);
                scannerNames.add(scanners[i]['name']);
                }
            } catch (error) {
                print('An error occurred: $error');
            }
    
            if (devices.isNotEmpty) {
                setState(() {
                _selectedScanner = devices[0]['name'];
                });
            }
        },
        child: const Text('List Scanners')),
    ```

5. Create a button to initiate the document scanning process. Remember to replace `LICENSE-KEY` with your own Dynamsoft Service license key.
    
    ```dart
    MaterialButton(
        textColor: Colors.white,
        color: Colors.blue,
        onPressed: () async {
        if (_selectedScanner != null) {
            int index = scannerNames.indexOf(_selectedScanner!);
            await _scanDocument(index);
        }
        },
        child: const Text('Scan Document')),

    Future<void> _scanDocument(int index) async {
        final Map<String, dynamic> parameters = {
        'license':
            'LICENSE-KEY',
        'device': devices[index]['device'],
        };

        parameters['config'] = {
        'IfShowUI': false,
        'PixelType': 2,
        'Resolution': 200,
        'IfFeederEnabled': false,
        'IfDuplexEnabled': false,
        };

        try {
        final String jobId =
            await dynamsoftService.scanDocument(host, parameters);

        if (jobId != '') {
            List<Uint8List> paths =
                await dynamsoftService.getImageStreams(host, jobId);

            await dynamsoftService.deleteJob(host, jobId);

            if (paths.isNotEmpty) {
            setState(() {
                imagePaths.insertAll(0, paths);
            });
            }
        }
        } catch (error) {
        print('An error occurred: $error');
        }
    }    
    ```
6. Display the acquired document images in a `ListView`:

    ```dart
    Expanded(
        child: imagePaths.isEmpty
            ? Image.asset('images/default.png')
            : ListView.builder(
                itemCount: imagePaths.length,
                itemBuilder: (context, index) {
                    return Padding(
                    padding: const EdgeInsets.all(10.0),
                    child: Image.memory(
                        imagePaths[index],
                        fit: BoxFit.contain,
                    ), 
                    );
                },
                ))
    ```

4. Run the app:

    ```bash
    flutter run # for Android and iOS
    # flutter run -d chrome # for web
    # flutter run -d windows # for Windows
    ```

    **Desktop**

    ![Flutter TWAIN scanner for desktop](https://www.dynamsoft.com/codepool/img/2023/09/flutter-twain-dynamsoft-service.png)
    
    **Web**

    ![Flutter TWAIN scanner for web](https://www.dynamsoft.com/codepool/img/2023/09/flutter-twain-scanner-web.png)

    **Mobile**

    ![Flutter TWAIN scanner for mobile](https://www.dynamsoft.com/codepool/img/2023/09/flutter-twain-scanner-android.jpg)

## Source Code
[https://github.com/yushulx/flutter_twain_scanner](https://github.com/yushulx/flutter_twain_scanner)