itc_scanner 0.0.1
itc_scanner: ^0.0.1 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;
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);
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: [
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()}%',
),
],
),
),
);
}
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 _buildApiKeysCard() {
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(
'Keys for Third-Party Integration',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
const SizedBox(height: 12),
const Text(
'Field Labels (keys):',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),
Wrap(
spacing: 8,
runSpacing: 4,
children: [
_buildKeyChip('Registration Number'),
_buildKeyChip('Document ID'),
_buildKeyChip('Make'),
_buildKeyChip('Model'),
_buildKeyChip('Color'),
_buildKeyChip('Use'),
_buildKeyChip('Expiry Date'),
],
),
const SizedBox(height: 8),
const Text(
'Field Properties: label, value, confidence, fieldType',
style: TextStyle(
fontSize: 11,
fontFamily: 'monospace',
color: Colors.blue,
),
),
],
),
),
);
}
Widget _buildKeyChip(String key) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.blue[100],
borderRadius: BorderRadius.circular(8),
),
child: Text(
key,
style: const TextStyle(
fontSize: 10,
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: [
// Platform version
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),
// Instructions
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue[200]!),
),
child: const Text(
'📸 Capture or select a Ghana vehicle registration document (road worthy certificate) to test the extraction.',
style: TextStyle(fontSize: 14, color: Colors.blue),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 20),
// Image preview
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),
],
// Action buttons
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),
// Extract button
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: Colors.green[600],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
),
const SizedBox(height: 20),
// Results section
if (_extractionResult != null &&
_extractionResult!['success'] == true) ...[
const Text(
'Extracted Document Fields:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// Show each field individually
...(_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',
);
}).toList(),
const SizedBox(height: 20),
// Summary card
_buildSummaryCard(),
const SizedBox(height: 20),
// API keys reference
_buildApiKeysCard(),
] 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(
'Capture or select an image to see extraction results',
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
textAlign: TextAlign.center,
),
],
),
),
),
],
const SizedBox(height: 50),
],
),
),
),
);
}
}