itc_scanner 0.0.4 copy "itc_scanner: ^0.0.4" to clipboard
itc_scanner: ^0.0.4 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
120
points
112
downloads
screenshot

Documentation

API reference

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

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