Skip to main content

flutter_ocr_native 0.3.6 copy "flutter_ocr_native: ^0.3.6" to clipboard
flutter_ocr_native: ^0.3.6 copied to clipboard

Flutter plugin for on-device OCR using ML Kit (Android), Vision (iOS/macOS), WinRT (Windows), Tesseract (Linux). Document parsing, validation, and masking.

example/lib/main.dart

import 'dart:io';

import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_ocr_native/flutter_ocr_native.dart';
import 'package:image_picker/image_picker.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final deviceSecurity = await OcrIntegrity.checkDeviceSecurity();
  runApp(OcrExampleApp(deviceCompromised: !deviceSecurity.secure));
}

bool get isMobile => !kIsWeb && (Platform.isAndroid || Platform.isIOS);
bool get isDesktop =>
    !kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux);

class OcrExampleApp extends StatelessWidget {
  final bool deviceCompromised;
  const OcrExampleApp({super.key, this.deviceCompromised = false});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'OCR Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      home: deviceCompromised
          ? const _CompromisedDeviceScreen()
          : const OcrHomePage(),
    );
  }
}

/// Full-screen block shown when device is rooted/jailbroken.
/// The app cannot be used until the device is secured.
class _CompromisedDeviceScreen extends StatelessWidget {
  const _CompromisedDeviceScreen();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.red.shade900,
      body: SafeArea(
        child: Center(
          child: Padding(
            padding: const EdgeInsets.all(32),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                const Icon(Icons.gpp_bad, size: 80, color: Colors.white),
                const SizedBox(height: 24),
                const Text(
                  'Device Security Risk',
                  style: TextStyle(
                    color: Colors.white,
                    fontSize: 24,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 16),
                const Text(
                  'This device appears to be rooted or jailbroken.\n\n'
                  'For the security of your documents and personal data, '
                  'this application cannot be used on compromised devices.\n\n'
                  'Please use a secure, unmodified device.',
                  textAlign: TextAlign.center,
                  style: TextStyle(color: Colors.white70, fontSize: 15, height: 1.5),
                ),
                const SizedBox(height: 32),
                OutlinedButton.icon(
                  onPressed: () => SystemNavigator.pop(),
                  icon: const Icon(Icons.exit_to_app, color: Colors.white),
                  label: const Text('Exit App',
                      style: TextStyle(color: Colors.white)),
                  style: OutlinedButton.styleFrom(
                    side: const BorderSide(color: Colors.white54),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class OcrHomePage extends StatefulWidget {
  const OcrHomePage({super.key});

  @override
  State<OcrHomePage> createState() => _OcrHomePageState();
}

class _OcrHomePageState extends State<OcrHomePage> {
  final _reader = OcrReader(validateDocument: true, maskAadhaar: true);
  final _picker = ImagePicker();

  File? _imageFile;
  Uint8List? _processedBytes;
  OcrResult? _result;
  DocumentDetails? _details;
  OcrAuditRecord? _auditRecord;
  List<String> _consistencyErrors = [];
  bool _loading = false;
  bool _sessionLocked = false;
  String? _error;
  DetectedDocType _docType = DetectedDocType.unknown;

  OcrWatermark get _watermark => const OcrWatermark(lines: {
        'Lead ID': 'LD-20250101-001',
        'Lat': '12.9716',
        'Long': '77.5946',
        'Agent': 'Ram Kumar',
        'Date': '2025-01-15 10:30',
      });

  Future<void> _pickFromCamera() async {
    final proceed = await OcrCaptureInstructions.showAsBottomSheet(context);
    if (proceed != true) return;
    final picked = await _picker.pickImage(source: ImageSource.camera);
    if (picked != null) _processFile(File(picked.path));
  }

  Future<void> _pickFromGallery() async {
    if (isMobile) {
      final proceed = await OcrCaptureInstructions.showAsBottomSheet(context);
      if (proceed != true) return;
      final picked = await _picker.pickImage(source: ImageSource.gallery);
      if (picked != null) _processFile(File(picked.path));
    } else {
      _pickFromFileBrowser();
    }
  }

  Future<void> _pickFromFileBrowser() async {
    final result = await FilePicker.platform.pickFiles(
      type: FileType.custom,
      allowedExtensions: ['jpg', 'jpeg', 'png', 'webp', 'bmp', 'gif', 'heic', 'tiff', 'pdf'],
      allowMultiple: false,
    );
    if (result != null && result.files.single.path != null) {
      _processFile(File(result.files.single.path!));
    }
  }

  bool _isPdf(File file) => file.path.toLowerCase().endsWith('.pdf');

  Future<void> _processFile(File file) async {
    final rawBytes = await file.readAsBytes();
    if (!mounted) return;

    Uint8List imageBytes;

    // Handle PDF: render first page to image
    if (_isPdf(file)) {
      final rendered = await OcrDocumentSaver.renderPdfPage(rawBytes);
      if (rendered == null) {
        setState(() => _error = 'Failed to render PDF. Platform may not support it.');
        return;
      }
      imageBytes = rendered;
    } else {
      imageBytes = rawBytes;
    }

    if (!mounted) return;

    // Auto-correct orientation
    final originalBytes = await OcrDocumentSaver.correctOrientation(imageBytes);
    if (!mounted) return;

    final croppedBytes = await OcrImageCropper.show(
      context,
      imageBytes: originalBytes,
      title: 'Crop Document',
    );

    if (croppedBytes == null) return;

    // ── Gap 3: Image quality gate ─────────────────────────────────────────
    final qualityError = OcrIntegrity.checkImageQuality(croppedBytes);
    if (qualityError != null) {
      setState(() => _error = qualityError);
      return;
    }

    setState(() {
      _imageFile = file;
      _processedBytes = croppedBytes;
      _result = null;
      _details = null;
      _auditRecord = null;
      _consistencyErrors = [];
      _error = null;
      _loading = true;
      _docType = DetectedDocType.unknown;
    });

    try {
      final result = await _reader.readFromBytes(croppedBytes);

      // ── Gap 2: OCR confidence threshold ──────────────────────────────────
      final confidences = result.blocks
          .expand((b) => b.lines)
          .map((l) => l.confidence ?? 0.0)
          .toList();
      final confidenceError = OcrIntegrity.checkConfidence(confidences);
      if (confidenceError != null) {
        setState(() {
          _error = confidenceError;
          _loading = false;
        });
        return;
      }

      // Use unified DocumentDetails — handles all doc types + face extraction
      final details = await DocumentDetails.fromResult(
        result,
        imageBytes: croppedBytes,
      );

      // ── Gap 1 & 6: Create audit record with unique scanId ─────────────────
      final auditRecord = OcrAuditRecord.create(
        details,
        croppedBytes,
        // agentId: 'Raja',
        sessionId: DateTime.now().microsecondsSinceEpoch.toString(),
      );

      // ── Gap 4: Consistency + expiry checks ────────────────────────────────
      final consistencyErrors = OcrIntegrity.consistencyErrors(details);

      // ── Gap 5: Persist audit record ───────────────────────────────────────
      await OcrIntegrity.persistAuditRecord(auditRecord);

      setState(() {
        _result = result;
        _details = details;
        _docType = details.docType;
        _auditRecord = auditRecord;
        _consistencyErrors = consistencyErrors;
      });

      if (mounted) _showValidationToast(details);
    } on EmptyImageException {
      setState(() => _error = 'No text detected in the image');
    } on HandwrittenTextException {
      setState(() => _error =
          'Handwritten text detected. Only printed documents are accepted');
    } catch (e) {
      setState(() => _error = e.toString());
    } finally {
      setState(() => _loading = false);
    }
  }

  void _showValidationToast(DocumentDetails details) {
    final String message;
    final Color bgColor;

    if (details.docType == DetectedDocType.unknown) {
      message = '⚠️ Document type not recognized';
      bgColor = Colors.orange;
    } else if (details.isValid) {
      message = '✅ Valid ${_docTypeLabel(details.docType)}';
      if (details.documentNumber != null) {
        bgColor = Colors.green;
      } else {
        bgColor = Colors.green;
      }
    } else if (details.documentNumber != null) {
      message = '❌ ${details.validationError}';
      bgColor = Colors.red;
    } else {
      message = '⚠️ No ${_docTypeLabel(details.docType)} number found';
      bgColor = Colors.orange;
    }

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
          content: Text(message),
          backgroundColor: bgColor,
          duration: const Duration(seconds: 3)),
    );
  }

  Future<void> _saveImage() async {
    if (_result == null || _processedBytes == null || _auditRecord == null) return;

    // ── Re-verify before save ───────────────────────────────────────────────
    final verification = OcrIntegrity.verify(_details!, _processedBytes!, _auditRecord!);
    if (!verification.passed) {
      await _handleTamper(verification);
      return;
    }

    final file = await OcrDocumentSaver.downloadBytes(
      imageBytes: _processedBytes!,
      watermark: _watermark,
    );
    if (mounted) {
      ScaffoldMessenger.of(context)
          .showSnackBar(SnackBar(content: Text('Saved to ${file.path}')));
    }
  }

  /// Full tamper response:
  /// 1. Log tamper event to disk
  /// 2. Wipe all sensitive state from memory
  /// 3. Lock the session (no further actions allowed)
  /// 4. Show a blocking, non-dismissible alert dialog
  Future<void> _handleTamper(OcrVerificationResult verification) async {
    // 1. Log
    final event = TamperEvent.fromVerification(verification, _auditRecord!);
    await OcrIntegrity.persistTamperEvent(event);
    // TODO(production): POST event.toJson() to your security endpoint here

    // 2. Wipe sensitive state
    setState(() {
      _processedBytes = null;
      _result = null;
      _details = null;
      _auditRecord = null;
      _consistencyErrors = [];
      _imageFile = null;
      _sessionLocked = true;
    });

    // 3 & 4. Show blocking dialog — user must acknowledge, cannot dismiss
    if (!mounted) return;
    await showDialog<void>(
      context: context,
      barrierDismissible: false,
      builder: (_) => AlertDialog(
        icon: const Icon(Icons.gpp_bad, color: Colors.red, size: 48),
        title: const Text('Security Alert'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              verification.reason,
              style: const TextStyle(fontWeight: FontWeight.w600),
            ),
            const SizedBox(height: 12),
            const Text(
              'All captured data has been cleared.\n'
              'This incident has been logged.\n\n'
              'Please restart the scan with the original document.',
            ),
            const SizedBox(height: 12),
            Text(
              'Scan ID: ${event.scanId}',
              style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
            ),
          ],
        ),
        actions: [
          FilledButton(
            onPressed: () => Navigator.of(context).pop(),
            style: FilledButton.styleFrom(backgroundColor: Colors.red),
            child: const Text('I Understand'),
          ),
        ],
      ),
    );
  }

  void _viewImage() {
    if (_result == null || _processedBytes == null) return;
    OcrDocumentViewer.show(
      context,
      result: _result!,
      originalBytes: _processedBytes!,
      title: _docTypeLabel(_docType),
      watermark: _watermark,
      onSave: (bytes) async {
        final file = await OcrDocumentSaver.downloadBytes(imageBytes: bytes);
        if (mounted) {
          ScaffoldMessenger.of(context)
              .showSnackBar(SnackBar(content: Text('Saved to ${file.path}')));
        }
      },
    );
  }

  @override
  void dispose() {
    _reader.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final hasResult = _details != null;
    return Scaffold(
      appBar: AppBar(
        title: const Text('OCR Reader'),
        actions: [
          if (hasResult && !_loading && !_sessionLocked)
            IconButton(
                onPressed: _viewImage, icon: const Icon(Icons.fullscreen)),
          if (hasResult && !_loading && !_sessionLocked)
            IconButton(onPressed: _saveImage, icon: const Icon(Icons.download)),
        ],
      ),
      body: _sessionLocked
          ? _buildLockedScreen()
          : ListView(
        padding: const EdgeInsets.all(16),
        children: [
          // Action buttons
          _buildActionButtons(),
          const SizedBox(height: 16),

          // Image preview
          if (hasResult && _result!.hasAadhaar)
            GestureDetector(
              onTap: _viewImage,
              child: ClipRRect(
                borderRadius: BorderRadius.circular(12),
                child: Image.memory(_result!.maskedImageBytes!,
                    height: 250, width: double.infinity, fit: BoxFit.contain),
              ),
            )
          else if (_processedBytes != null)
            GestureDetector(
              onTap: hasResult ? _viewImage : null,
              child: ClipRRect(
                borderRadius: BorderRadius.circular(12),
                child: Image.memory(_processedBytes!,
                    height: 250, width: double.infinity, fit: BoxFit.contain),
              ),
            ),

          if (hasResult && !_loading) ...[
            const SizedBox(height: 12),
            if (_docType != DetectedDocType.unknown)
              Chip(
                avatar: Icon(_docTypeIcon(_docType), size: 18),
                label: Text('Detected: ${_docTypeLabel(_docType)}'),
                backgroundColor: Colors.indigo.shade50,
              ),
            const SizedBox(height: 8),
            Row(children: [
              Expanded(
                  child: OutlinedButton.icon(
                      onPressed: _viewImage,
                      icon: const Icon(Icons.visibility),
                      label: const Text('View'))),
              const SizedBox(width: 12),
              Expanded(
                  child: OutlinedButton.icon(
                      onPressed: _saveImage,
                      icon: const Icon(Icons.save_alt),
                      label: const Text('Download'))),
            ]),
          ],

          const SizedBox(height: 16),
          if (_loading) const Center(child: CircularProgressIndicator()),
          if (_error != null)
            Card(
              color: Theme.of(context).colorScheme.errorContainer,
              child: Padding(
                  padding: const EdgeInsets.all(12),
                  child: Text(_error!,
                      style: TextStyle(
                          color:
                              Theme.of(context).colorScheme.onErrorContainer))),
            ),

          // Unified results display
          if (hasResult) ...[
            // Face photo
            if (_details!.hasPhoto) _buildPhotoCard(),
            if (_details!.hasPhoto) const SizedBox(height: 12),
            // Details card
            _buildDetailsCard(),
            const SizedBox(height: 12),
            // Validation card
            _buildValidationCard(),
            const SizedBox(height: 12),
            // Integrity card
            _buildIntegrityCard(),
            const SizedBox(height: 12),
            // Raw text
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          Expanded(child: Text('Raw OCR Text',
                              style: Theme.of(context).textTheme.titleMedium)),
                          IconButton(
                            icon: const Icon(Icons.copy, size: 18),
                            tooltip: 'Copy raw text',
                            onPressed: () {
                              Clipboard.setData(ClipboardData(text: _result!.text));
                              ScaffoldMessenger.of(context).showSnackBar(
                                const SnackBar(content: Text('Raw OCR text copied!'), duration: Duration(seconds: 2)),
                              );
                            },
                          ),
                        ],
                      ),
                      const Divider(),
                      SelectableText(
                          _result!.text.isEmpty
                              ? 'No text found'
                              : _result!.text,
                          style: Theme.of(context).textTheme.bodyMedium),
                    ]),
              ),
            ),
          ],
        ],
      ),
    );
  }

  Widget _buildLockedScreen() {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(32),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Icon(Icons.lock, size: 64, color: Colors.red),
            const SizedBox(height: 16),
            Text('Session Locked',
                style: Theme.of(context)
                    .textTheme
                    .titleLarge
                    ?.copyWith(color: Colors.red)),
            const SizedBox(height: 8),
            const Text(
              'A tamper attempt was detected and logged.\n'
              'Start a new scan to continue.',
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 24),
            FilledButton.icon(
              onPressed: () => setState(() {
                _sessionLocked = false;
                _error = null;
                _docType = DetectedDocType.unknown;
              }),
              icon: const Icon(Icons.refresh),
              label: const Text('Start New Scan'),
              style: FilledButton.styleFrom(backgroundColor: Colors.red),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildPhotoCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            ClipRRect(
              borderRadius: BorderRadius.circular(8),
              child: Image.memory(_details!.photoBytes!,
                  width: 80, height: 100, fit: BoxFit.cover),
            ),
            const SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('Photo Extracted',
                      style: Theme.of(context).textTheme.titleSmall),
                  const SizedBox(height: 4),
                  Text(
                    'Face detected from ${_docTypeLabel(_docType)} document',
                    style: Theme.of(context).textTheme.bodySmall,
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildDetailsCard() {
    final fields = _details!.toDisplayMap(maskAadhaar: true);
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
          Text('${_docTypeLabel(_docType)} Details',
              style: Theme.of(context).textTheme.titleMedium),
          const Divider(),
          if (fields.isEmpty)
            const Text('No details detected')
          else
            ...fields.entries.map((e) => _detailRow(e.key, e.value)),
        ]),
      ),
    );
  }

  Widget _buildValidationCard() {
    final details = _details!;
    if (details.isValid) {
      final label = details.documentNumber != null
          ? '${_docTypeLabel(_docType)} Valid: ${details.documentNumber}'
          : '${_docTypeLabel(_docType)} Valid';
      return _validationChip(label, true);
    }
    if (details.documentNumber != null && details.validationError != null) {
      return _validationChip(details.validationError!, false);
    }
    return _validationChip(
        details.validationError ?? 'No document number found', false);
  }

  Widget _validationChip(String text, bool valid) {
    return Card(
      color: valid ? Colors.green.shade50 : Colors.red.shade50,
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Row(children: [
          Icon(valid ? Icons.check_circle : Icons.cancel,
              color: valid ? Colors.green : Colors.red, size: 20),
          const SizedBox(width: 8),
          Expanded(child: Text(text)),
        ]),
      ),
    );
  }

  Widget _detailRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
        SizedBox(
            width: 120,
            child: Text(label,
                style: TextStyle(
                    color: Colors.grey.shade700,
                    fontWeight: FontWeight.w600,
                    fontSize: 13))),
        const Text(': '),
        Expanded(
            child: SelectableText(value,
                style: const TextStyle(fontWeight: FontWeight.w500))),
      ]),
    );
  }

  Widget _buildIntegrityCard() {
    final record = _auditRecord;
    if (record == null) return const SizedBox.shrink();

    final hasConsistencyIssues = _consistencyErrors.isNotEmpty;

    return Card(
      color: hasConsistencyIssues ? Colors.orange.shade50 : Colors.blue.shade50,
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(children: [
              Icon(
                hasConsistencyIssues ? Icons.warning_amber : Icons.verified_user,
                color: hasConsistencyIssues ? Colors.orange : Colors.blue,
                size: 20,
              ),
              const SizedBox(width: 8),
              Text(
                hasConsistencyIssues ? 'Integrity: Warnings' : 'Integrity: Verified',
                style: Theme.of(context).textTheme.titleSmall,
              ),
            ]),
            const Divider(),
            _detailRow('Scan ID', record.scanId),
            _detailRow('Data Hash', '${record.dataHash.substring(0, 16)}…'),
            _detailRow('Image Hash', '${record.imageHash.substring(0, 16)}…'),
            _detailRow('Captured At', record.capturedAt.toLocal().toString()),
            if (record.agentId != null) _detailRow('Agent', record.agentId!),
            if (hasConsistencyIssues) ...[
              const SizedBox(height: 8),
              Text('Consistency Issues:',
                  style: TextStyle(
                      color: Colors.orange.shade800,
                      fontWeight: FontWeight.w600,
                      fontSize: 13)),
              ..._consistencyErrors.map(
                (e) => Padding(
                  padding: const EdgeInsets.only(top: 2),
                  child: Text('• $e',
                      style: TextStyle(
                          color: Colors.orange.shade900, fontSize: 13)),
                ),
              ),
            ],
            // ── Tamper simulation buttons (debug builds only) ──────────────
            if (kDebugMode) ...[
              const SizedBox(height: 12),
              const Divider(),
              Text(
                'Tamper Detection Tests',
                style: TextStyle(
                    fontSize: 11,
                    color: Colors.grey.shade600,
                    fontWeight: FontWeight.w600),
              ),
              const SizedBox(height: 6),
              Row(children: [
                Expanded(
                  child: OutlinedButton.icon(
                    onPressed: () {
                      // Mutate extracted fields → breaks dataHash
                      setState(() {
                        _details = _details!.copyWithTampered(
                          name: 'TAMPERED NAME',
                          documentNumber: '0000 0000 0000',
                        );
                      });
                      ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(
                          content: Text('⚠️ Data mutated — now tap Download'),
                          backgroundColor: Colors.deepOrange,
                          duration: Duration(seconds: 2),
                        ),
                      );
                    },
                    icon: const Icon(Icons.edit, size: 16),
                    label: const Text('Tamper Data',
                        style: TextStyle(fontSize: 12)),
                    style: OutlinedButton.styleFrom(
                        foregroundColor: Colors.deepOrange),
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: OutlinedButton.icon(
                    onPressed: () {
                      // Replace image bytes with noise → breaks imageHash
                      final fake = Uint8List(100);
                      for (int i = 0; i < fake.length; i++) { fake[i] = i % 256; }
                      setState(() => _processedBytes = fake);
                      ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(
                          content: Text('⚠️ Image replaced — now tap Download'),
                          backgroundColor: Colors.deepOrange,
                          duration: Duration(seconds: 2),
                        ),
                      );
                    },
                    icon: const Icon(Icons.image_not_supported, size: 16),
                    label: const Text('Tamper Image',
                        style: TextStyle(fontSize: 12)),
                    style: OutlinedButton.styleFrom(
                        foregroundColor: Colors.deepOrange),
                  ),
                ),
              ]),
            ],
          ],
        ),
      ),
    );
  }

  Widget _buildActionButtons() {
    if (isMobile) {
      return Row(children: [
        Expanded(
            child: FilledButton.icon(
                onPressed: _loading ? null : _pickFromCamera,
                icon: const Icon(Icons.camera_alt),
                label: const Text('Camera'))),
        const SizedBox(width: 12),
        Expanded(
            child: FilledButton.tonalIcon(
                onPressed: _loading ? null : _pickFromGallery,
                icon: const Icon(Icons.photo_library),
                label: const Text('Gallery'))),
      ]);
    }
    return Row(children: [
      Expanded(
          child: FilledButton.icon(
              onPressed: _loading ? null : _pickFromFileBrowser,
              icon: const Icon(Icons.folder_open),
              label: const Text('Open Image File'))),
      if (_imageFile != null) ...[
        const SizedBox(width: 12),
        Text(_imageFile!.path.split(Platform.pathSeparator).last,
            style: Theme.of(context).textTheme.bodySmall)
      ],
    ]);
  }

  String _docTypeLabel(DetectedDocType type) =>
      DocumentTypeDetector.label(type);

  IconData _docTypeIcon(DetectedDocType type) =>
      DocumentTypeDetector.icon(type);
}
2
likes
0
points
1.99k
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter plugin for on-device OCR using ML Kit (Android), Vision (iOS/macOS), WinRT (Windows), Tesseract (Linux). Document parsing, validation, and masking.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

crypto, flutter, path_provider

More

Packages that depend on flutter_ocr_native

Packages that implement flutter_ocr_native