itc_scanner 0.0.3 copy "itc_scanner: ^0.0.3" to clipboard
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),
            ],
          ),
        ),
      ),
    );
  }
}
3
likes
130
points
30
downloads
screenshot

Publisher

unverified uploader

Weekly Downloads

Flutter plugin for extracting data from Ghana vehicle licensing documents using ML Kit text recognition. Simple integration for cross-platform document scanning.

Homepage

Topics

#ocr #document-scanning #ghana #vehicle-documents #ml-kit

Documentation

API reference

Funding

Consider supporting this project:

www.itconsortiumgh.com

License

unknown (license)

Dependencies

flutter, google_mlkit_commons, plugin_platform_interface

More

Packages that depend on itc_scanner

Packages that implement itc_scanner