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.
This article is Part 2 in a 8-Part Series.
- Part 1 - Dynamic Web TWAIN Service REST API - Scan Documents in Node.js
- Part 2 - Building a Document Digitization App with Flutter for TWAIN, WIA, and eSCL Scanners
- Part 3 - Access Document Scanners in Java
- Part 4 - How to Scan Documents from TWAIN, WIA, SANE Compatible Scanners in Python
- Part 5 - Building .NET Apps for Scanning Documents from TWAIN, WIA, SANE, and eSCL Scanners
- Part 6 - Scanning Documents to Web Pages with JavaScript and Dynamic Web TWAIN RESTful API
- Part 7 - How to Build a Remote Document Scanner in SwiftUI to Digitize Documents Over Network
- Part 8 - How to Scan Documents from Document Scanners Over a Local Network Using JavaScript and HTML5
Flutter TWAIN Scanner Package
https://pub.dev/packages/flutter_twain_scanner
Prerequisites
- Install Dynamic Web TWAIN Service on your host machine that has one or more document scanners connected to it.
- Request a free trial license for Dynamic Web TWAIN Service.
Setting Up Dynamic Web TWAIN Service
-
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. -
Replace
127.0.0.1
with your LAN IP address, such ashttp://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 REST API Reference
Creating a Flutter Plugin to Interface with Dynamic Web TWAIN Service’s REST API
-
Create a new Flutter plugin project.
flutter create -t plugin --platforms=android,ios,linux,macos,windows,web flutter_twain_scanner
-
Install dependent Flutter packages:
http
andpath
.flutter pub add http path
-
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
-
Create a new Flutter app project.
flutter create app
-
Install the package
flutter_twain_scanner
:flutter pub add flutter_twain_scanner
-
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';
-
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')),
-
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'); } }
-
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, ), ); }, ))
-
Run the app:
flutter run # for Android and iOS # flutter run -d chrome # for web # flutter run -d windows # for Windows
Desktop
Web
Mobile