itc_scanner 0.0.3
itc_scanner: ^0.0.3 copied to clipboard
Flutter plugin for extracting data from Ghana vehicle licensing documents using ML Kit text recognition. Simple integration for cross-platform document scanning.
example/lib/main.dart
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:itc_scanner/itc_scanner.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
bool _isLoading = false;
File? _selectedImage;
Map<String, dynamic>? _extractionResult;
bool _useRegistration = false;
final _itcScannerPlugin = ItcScanner();
@override
void initState() {
super.initState();
initPlatformState();
}
Future<void> initPlatformState() async {
String platformVersion;
try {
platformVersion =
await _itcScannerPlugin.getPlatformVersion() ??
'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
}
Future<void> _pickImageFromSource(ImageSource source) async {
try {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(
source: source,
maxWidth: 1024,
maxHeight: 1024,
imageQuality: 90,
);
if (image != null) {
setState(() {
_selectedImage = File(image.path);
_extractionResult = null;
});
}
} catch (e) {
_showErrorSnackBar(
'Error ${source == ImageSource.camera ? 'capturing' : 'selecting'} image: $e',
);
}
}
Future<void> _extractDocument() async {
if (_selectedImage == null) {
_showErrorSnackBar('Please capture or select an image first');
return;
}
setState(() {
_isLoading = true;
});
try {
final Uint8List imageBytes = await _selectedImage!.readAsBytes();
final result = await _itcScannerPlugin.extractDocument(
imageBytes,
authKey: "ITC_apps@itconsortiumgh.com",
extractionKey: _useRegistration ? 'itc_reg_form' : null,
);
setState(() {
_extractionResult = result;
});
} catch (e) {
_showErrorSnackBar('Error extracting document: $e');
} finally {
setState(() {
_isLoading = false;
});
}
}
void _clearImage() {
setState(() {
_selectedImage = null;
_extractionResult = null;
});
}
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.red[600]),
);
}
Widget _buildFieldCard(
String label,
String value,
double confidence,
String fieldType,
) {
final confidencePercent = (confidence * 100).toInt();
Color confidenceColor = Colors.green;
if (confidence < 0.5) {
confidenceColor = Colors.red;
} else if (confidence < 0.8) {
confidenceColor = Colors.orange;
}
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
label,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: confidenceColor.withValues(alpha: .1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: confidenceColor.withValues(alpha: .3),
),
),
child: Text(
'$confidencePercent%',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: confidenceColor,
),
),
),
],
),
const SizedBox(height: 8),
Text(
value,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.blue,
),
),
const SizedBox(height: 4),
Text(
'Type: $fieldType',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
],
),
),
);
}
Widget _buildSummaryCard() {
if (_extractionResult == null) return const SizedBox.shrink();
final fields = _extractionResult!['fields'] as List<dynamic>? ?? [];
final processingTime = _extractionResult!['processingTime'] ?? 0;
final documentType = _extractionResult!['documentType'] ?? 'Unknown';
final highConfidenceFields = fields
.where((f) => (f['confidence'] ?? 0) > 0.8)
.length;
final avgConfidence = fields.isNotEmpty
? fields
.map((f) => f['confidence'] as double? ?? 0.0)
.reduce((a, b) => a + b) /
fields.length
: 0.0;
return Card(
color: Colors.green[50],
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.analytics, color: Colors.green[700], size: 24),
const SizedBox(width: 8),
const Text(
'Extraction Summary',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
const SizedBox(height: 16),
_buildSummaryRow('Document Type', documentType),
_buildSummaryRow('Processing Time', '${processingTime}ms'),
_buildSummaryRow('Total Fields', '${fields.length}'),
_buildSummaryRow(
'High Confidence Fields',
'$highConfidenceFields/${fields.length}',
),
_buildSummaryRow(
'Average Confidence',
'${(avgConfidence * 100).toInt()}%',
),
_buildSummaryRow(
'Extraction Method',
_useRegistration
? 'Registration Particulars'
: 'Driver Vehicle Licenses',
),
],
),
),
);
}
Widget _buildSummaryRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
);
}
Widget _buildDocumentTypeSelector() {
return Card(
color: Colors.orange[50],
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.document_scanner,
color: Colors.orange[700],
size: 20,
),
const SizedBox(width: 8),
const Text(
'Document Type',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.orange,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: RadioListTile<bool>(
title: const Text(
'Driver Vehicle Licenses (Road Worthy)',
style: TextStyle(fontSize: 12),
),
value: false,
groupValue: _useRegistration,
onChanged: (value) {
setState(() {
_useRegistration = value!;
_extractionResult = null;
});
},
dense: true,
),
),
Expanded(
child: RadioListTile<bool>(
title: const Text(
'Registration Particulars',
style: TextStyle(fontSize: 12),
),
value: true,
groupValue: _useRegistration,
onChanged: (value) {
setState(() {
_useRegistration = value!;
_extractionResult = null;
});
},
dense: true,
),
),
],
),
],
),
),
);
}
Widget _buildDirectAccessDemo() {
if (_extractionResult == null || _extractionResult!['data'] == null) {
return const SizedBox.shrink();
}
final data = _extractionResult!['data'] as Map<String, dynamic>;
final sampleKeys = _useRegistration
? ['reg_number', 'make', 'colour', 'owner']
: ['reg_number', 'make', 'colour', 'document_id'];
return Card(
color: Colors.purple[50],
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.bolt, color: Colors.purple[700], size: 24),
const SizedBox(width: 8),
const Text(
'Direct Data Access Demo',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.purple,
),
),
],
),
const SizedBox(height: 16),
const Text(
'Third-party developers can access data directly:',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
const SizedBox(height: 12),
...sampleKeys.where((key) => data.containsKey(key)).map((key) {
final value = data[key].toString();
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.purple[100],
borderRadius: BorderRadius.circular(4),
),
child: Text(
'result["data"]["$key"]',
style: const TextStyle(
fontSize: 11,
fontFamily: 'monospace',
color: Colors.purple,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 12),
const Text(
'β',
style: TextStyle(fontSize: 16, color: Colors.purple),
),
const SizedBox(width: 12),
Expanded(
child: Text(
'"$value"',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.green,
),
),
),
],
),
);
}),
if (data.isNotEmpty) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.purple[100],
borderRadius: BorderRadius.circular(6),
),
child: Text(
'Total available keys: ${data.keys.length}',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.purple,
),
),
),
],
],
),
),
);
}
Widget _buildRegistrationKeysCard() {
return Card(
color: Colors.blue[50],
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.code, color: Colors.blue[700], size: 20),
const SizedBox(width: 8),
const Text(
'Third-Party Integration Keys',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
const SizedBox(height: 12),
if (_useRegistration) ...[
const Text(
'Registration Particulars - Direct Access Keys:',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),
Wrap(
spacing: 6,
runSpacing: 4,
children: [
_buildKeyChip('reg_number'),
_buildKeyChip('document_id'),
_buildKeyChip('owner'),
_buildKeyChip('postal_address'),
_buildKeyChip('res_address'),
_buildKeyChip('make'),
_buildKeyChip('colour'),
_buildKeyChip('model'),
_buildKeyChip('type'),
_buildKeyChip('chassis_number'),
_buildKeyChip('country_of_origin'),
_buildKeyChip('year_of_manufacturing'),
_buildKeyChip('number_of_axles'),
_buildKeyChip('number_of_wheels'),
_buildKeyChip('number_of_persons'),
_buildKeyChip('engine_make'),
_buildKeyChip('engine_number_of_clys'),
_buildKeyChip('engine_cc'),
_buildKeyChip('hp'),
_buildKeyChip('fuel'),
_buildKeyChip('use_private_or_commercial'),
_buildKeyChip('date_of_entry'),
],
),
] else ...[
const Text(
'Driver Vehicle Licenses (Road Worthy) - Direct Access Keys:',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),
Wrap(
spacing: 6,
runSpacing: 4,
children: [
_buildKeyChip('reg_number'),
_buildKeyChip('document_id'),
_buildKeyChip('make'),
_buildKeyChip('model'),
_buildKeyChip('colour'),
_buildKeyChip('use'),
_buildKeyChip('expiry_date'),
],
),
],
const SizedBox(height: 8),
const Text(
'Easy Access: result["data"]["reg_number"] or result["data"]["make"]',
style: TextStyle(
fontSize: 10,
fontFamily: 'monospace',
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
const Text(
'UI Display: result["fields"] array with label, value, confidence',
style: TextStyle(
fontSize: 9,
fontFamily: 'monospace',
color: Colors.grey,
),
),
],
),
),
);
}
Widget _buildKeyChip(String key) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
decoration: BoxDecoration(
color: Colors.blue[100],
borderRadius: BorderRadius.circular(6),
),
child: Text(
key,
style: const TextStyle(
fontSize: 9,
fontFamily: 'monospace',
color: Colors.blue,
),
),
);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('ITC Scanner Test'),
backgroundColor: Colors.blue[700],
foregroundColor: Colors.white,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Running on: $_platformVersion',
style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
),
),
const SizedBox(height: 20),
_buildDocumentTypeSelector(),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _useRegistration ? Colors.purple[50] : Colors.blue[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _useRegistration
? Colors.purple[200]!
: Colors.blue[200]!,
),
),
child: Text(
_useRegistration
? 'π Select a Ghana vehicle registration form to test extraction.'
: 'π Select a Ghana driver and vehicle licensing authority to test extraction.',
style: TextStyle(
fontSize: 14,
color: _useRegistration ? Colors.purple : Colors.blue,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 20),
if (_selectedImage != null) ...[
Container(
height: 200,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(_selectedImage!, fit: BoxFit.contain),
),
),
const SizedBox(height: 8),
TextButton.icon(
onPressed: _clearImage,
icon: const Icon(Icons.clear, size: 18),
label: const Text('Clear Image'),
style: TextButton.styleFrom(foregroundColor: Colors.red[600]),
),
const SizedBox(height: 16),
],
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => _pickImageFromSource(ImageSource.camera),
icon: const Icon(Icons.camera_alt),
label: const Text('Camera'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[600],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () =>
_pickImageFromSource(ImageSource.gallery),
icon: const Icon(Icons.photo_library),
label: const Text('Gallery'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange[600],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _isLoading ? null : _extractDocument,
icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
)
: const Icon(Icons.document_scanner),
label: Text(
_isLoading ? 'Processing...' : 'Extract Document',
),
style: ElevatedButton.styleFrom(
backgroundColor: _useRegistration
? Colors.purple[600]
: Colors.green[600],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
),
const SizedBox(height: 20),
if (_extractionResult != null &&
_extractionResult!['success'] == true) ...[
const Text(
'Extracted Document Fields:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
...(_extractionResult!['fields'] as List<dynamic>).map((field) {
return _buildFieldCard(
field['label']?.toString() ?? 'Unknown',
field['value']?.toString() ?? 'No value',
(field['confidence'] as double?) ?? 0.0,
field['fieldType']?.toString() ?? 'Unknown',
);
}),
const SizedBox(height: 20),
_buildSummaryCard(),
const SizedBox(height: 20),
_buildDirectAccessDemo(),
const SizedBox(height: 20),
_buildRegistrationKeysCard(),
] else if (_extractionResult != null) ...[
Card(
color: Colors.red[50],
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Icon(Icons.error, color: Colors.red[600], size: 48),
const SizedBox(height: 8),
const Text(
'Extraction Failed',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.red,
),
),
const SizedBox(height: 4),
const Text(
'Could not extract document data. Please try with a clearer image.',
textAlign: TextAlign.center,
),
],
),
),
),
] else ...[
Card(
child: Container(
padding: const EdgeInsets.all(32),
child: Column(
children: [
Icon(
Icons.document_scanner,
size: 48,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'No document processed yet',
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
'Select document type and capture an image to see extraction results',
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
textAlign: TextAlign.center,
),
],
),
),
),
],
const SizedBox(height: 50),
],
),
),
),
);
}
}