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.
This article is Part 2 in a 6-Part Series.
- Part 1 - Dynamsoft 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
Flutter TWAIN Scanner Package
https://pub.dev/packages/flutter_twain_scanner
Prerequisites
- Install Dynamsoft Service on your host machine that has one or more document scanners connected to it.
- Request a free trial license for Dynamsoft Service.
Setting Up Dynamsoft Service
-
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. -
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.
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
-
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 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
-
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 Dynamsoft 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 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'); } }
-
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