Building a Document Digitization App with Flutter for TWAIN, WIA, and eSCL Scanners

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, 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.

Flutter TWAIN Scanner Package

https://pub.dev/packages/flutter_twain_scanner

Prerequisites

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

  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

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.

Creating a Flutter Plugin to Interface with Dynamsoft Service’s REST API

  1. Create a new Flutter plugin project.

     flutter create -t plugin --platforms=android,ios,linux,macos,windows,web flutter_twain_scanner
    
  2. Install dependent Flutter packages: http and path.

     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.

     // 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;
         }
     }
    
    • 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.

     flutter create app
    
  2. Install the package flutter_twain_scanner:

     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.

     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.

     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.

     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:

     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,
                     ), 
                     );
                 },
                 ))
    
  7. Run the app:

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

    Desktop

    Flutter TWAIN scanner for desktop

    Web

    Flutter TWAIN scanner for web

    Mobile

    Flutter TWAIN scanner for mobile

Source Code

https://github.com/yushulx/flutter_twain_scanner