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

Dynamic Web TWAIN 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 Dynamic Web TWAIN 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 Dynamic Web TWAIN Service

  1. After installing Dynamic Web TWAIN 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.

    Dynamic Web TWAIN 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.

    Dynamic Web TWAIN Service IP change

Dynamic Web TWAIN Service REST API Reference

Creating a Flutter Plugin to Interface with Dynamic Web TWAIN 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 Dynamic Web TWAIN 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 {
             String url = '$host/api/device/scanners';
             if (scannerType != null) {
               url += '?type=$scannerType';
             }
            
             try {
               final response = await http.get(Uri.parse(url));
               if (response.statusCode == 200) {
                 return json.decode(response.body);
               }
             } catch (error) {}
             return [];
           }
    
         Future<Map<String, dynamic>> createJob(
               String host, Map<String, dynamic> parameters) async {
             final url = '$host/api/device/scanners/jobs';
             try {
               final response = await http.post(
                 Uri.parse(url),
                 headers: {
                   'Content-Type': 'application/json',
                   'Content-Length': json.encode(parameters).length.toString()
                 },
                 body: json.encode(parameters),
               );
               if (response.statusCode == 201) {
                 return json.decode(response.body);
               }
             } catch (error) {}
             return {};
           }
    
         Future<Map<String, dynamic>> deleteJob(String host, String jobId) async {
             final url = '$host/api/device/scanners/jobs/$jobId';
            
             try {
               final response = await http.delete(Uri.parse(url));
               return json.decode(response.body);
             } catch (error) {
               return {};
             }
           }
    
         Future<List<String>> getImageFiles(
               String host, String jobId, String directory) async {
             final List<String> images = [];
             final url = '$host/api/device/scanners/jobs/$jobId/next-page';
             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/api/device/scanners/jobs/$jobId/next-page';
             while (true) {
               try {
                 final response = await http.get(Uri.parse(url));
                 if (response.statusCode == 200) {
                   streams.add(response.bodyBytes);
                 } else {
                   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> createJob(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 Dynamic Web TWAIN 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 Dynamic Web TWAIN 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.createJob(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