Skip to main content

eas_passport_ocr 0.2.8 copy "eas_passport_ocr: ^0.2.8" to clipboard
eas_passport_ocr: ^0.2.8 copied to clipboard

On-device passport MRZ recognition for Flutter Android and iOS.

example/lib/main.dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:eas_passport_ocr/eas_passport_ocr.dart';

void main() => runApp(const PassportDemo());

class PassportDemo extends StatelessWidget {
  const PassportDemo({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
    debugShowCheckedModeBanner: false,
    title: '护照识别',
    theme: ThemeData(
      colorSchemeSeed: const Color(0xff246b54),
      scaffoldBackgroundColor: const Color(0xfff5f7f3),
      useMaterial3: true,
    ),
    home: const PassportPage(),
  );
}

class PassportPage extends StatefulWidget {
  const PassportPage({super.key});
  @override
  State<PassportPage> createState() => _PassportPageState();
}

class _PassportPageState extends State<PassportPage> {
  final _service = EasPassportOcr();
  bool _busy = false;
  bool _showJson = false;
  String _status = '完整拍入护照资料页,确保底部两行文字清晰。';
  String? _imagePath;
  Duration? _elapsed;
  Map<String, dynamic>? _result;

  Future<void> _scan(ImageSource source) async {
    if (_busy) return;
    setState(() => _busy = true);
    try {
      // Keep original pixels: the SDK handles size and orientation itself.
      final file = await ImagePicker().pickImage(source: source);
      if (file == null || !mounted) return;
      setState(() {
        _imagePath = file.path;
        _result = null;
        _elapsed = null;
        _showJson = false;
        _status = '正在识别,请稍候…';
      });
      final watch = Stopwatch()..start();
      final result = await _service.recognizeResult(file.path);
      watch.stop();
      if (mounted) {
        setState(() {
          _result = result;
          _elapsed = watch.elapsed;
          _status = result.containsKey('code')
              ? result['msg'] as String
              : '识别完成,请与原件核对。';
        });
      }
    } on PlatformException catch (error) {
      if (mounted) {
        setState(
          () => _status = error.code.toLowerCase().contains('access')
              ? '无法访问相机或相册,请在系统设置中允许访问后重试。'
              : '无法打开照片,请重新选择或拍摄。',
        );
      }
    } catch (_) {
      if (mounted) {
        setState(() => _status = '本次识别未完成,请重新选择照片后重试。');
      }
    } finally {
      if (mounted) {
        setState(() => _busy = false);
      } else {
        await _service.dispose();
      }
    }
  }

  Future<void> _copy() async {
    final current = _result;
    if (current == null) return;
    try {
      final result = current.containsKey('code')
          ? current
          : parsePassportMrz((current['rawMrz'] as List).join('\n')).toJson();
      await Clipboard.setData(
        ClipboardData(text: const JsonEncoder.withIndent('  ').convert(result)),
      );
      if (mounted) {
        setState(() => _result = result);
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(const SnackBar(content: Text('JSON 已复制')));
      }
    } catch (_) {
      if (mounted) {
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(const SnackBar(content: Text('复制失败,请重试')));
      }
    }
  }

  @override
  void dispose() {
    if (!_busy) unawaited(_service.dispose());
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(
      title: const Text('护照识别'),
      backgroundColor: const Color(0xfff5f7f3),
    ),
    body: SafeArea(
      child: ListView(
        padding: const EdgeInsets.fromLTRB(20, 12, 20, 28),
        children: [
          Text('拍一张,读取护照信息', style: Theme.of(context).textTheme.headlineSmall),
          const SizedBox(height: 8),
          const Text(
            'Flutter SDK · 设备内识别',
            style: TextStyle(color: Colors.black54),
          ),
          const SizedBox(height: 24),
          if (_imagePath != null)
            ClipRRect(
              borderRadius: BorderRadius.circular(16),
              child: Image.file(
                File(_imagePath!),
                height: 220,
                fit: BoxFit.contain,
                cacheWidth: 1000,
                errorBuilder: (_, _, _) => const SizedBox(
                  height: 80,
                  child: Center(child: Text('照片预览不可用')),
                ),
              ),
            )
          else
            Container(
              height: 160,
              decoration: BoxDecoration(
                color: const Color(0xffe5ede6),
                borderRadius: BorderRadius.circular(16),
              ),
              child: const Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(
                    Icons.document_scanner_outlined,
                    size: 48,
                    color: Color(0xff246b54),
                  ),
                  SizedBox(height: 12),
                  Text('选择完整的护照资料页'),
                ],
              ),
            ),
          const SizedBox(height: 20),
          Row(
            children: [
              Expanded(
                child: FilledButton.icon(
                  onPressed: _busy ? null : () => _scan(ImageSource.camera),
                  icon: const Icon(Icons.camera_alt_outlined),
                  label: const Text('拍摄护照'),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: OutlinedButton.icon(
                  onPressed: _busy ? null : () => _scan(ImageSource.gallery),
                  icon: const Icon(Icons.photo_library_outlined),
                  label: const Text('从相册选择'),
                ),
              ),
            ],
          ),
          const SizedBox(height: 16),
          if (_busy) ...[
            const LinearProgressIndicator(),
            const SizedBox(height: 12),
          ],
          Text(_status),
          if (_elapsed != null) ...[
            const SizedBox(height: 6),
            Text(
              '识别耗时 ${(_elapsed!.inMilliseconds / 1000).toStringAsFixed(2)} 秒',
              style: const TextStyle(color: Colors.black54),
            ),
          ],
          if (_result != null) ...[
            const SizedBox(height: 20),
            if (_result!.containsKey('code'))
              Text(_result!['suggestion'] as String)
            else ...[
              SegmentedButton<bool>(
                segments: const [
                  ButtonSegment(value: false, label: Text('字段')),
                  ButtonSegment(value: true, label: Text('JSON')),
                ],
                selected: {_showJson},
                onSelectionChanged: (value) =>
                    setState(() => _showJson = value.first),
              ),
              const SizedBox(height: 12),
              if (!_showJson) PassportFields(result: _result!),
            ],
            if (_showJson || _result!.containsKey('code'))
              SelectableText(
                const JsonEncoder.withIndent('  ').convert(_result),
              ),
            const SizedBox(height: 16),
            OutlinedButton.icon(
              onPressed: _busy ? null : _copy,
              icon: const Icon(Icons.copy),
              label: const Text('复制 JSON 结果'),
            ),
            TextButton(
              onPressed: _busy
                  ? null
                  : () => setState(() {
                      _result = null;
                      _imagePath = null;
                      _elapsed = null;
                      _status = '完整拍入护照资料页,确保底部两行文字清晰。';
                    }),
              child: const Text('清空结果'),
            ),
          ],
        ],
      ),
    ),
  );
}

class PassportFields extends StatelessWidget {
  const PassportFields({super.key, required this.result});
  final Map<String, dynamic> result;
  static const labels = {
    'surname': '姓',
    'givenNames': '名',
    'passportNumber': '护照号',
    'nationality': '国籍',
    'birthDate': '出生日期',
    'sex': '性别',
    'expiryDate': '有效期至',
    'issuingCountry': '签发国家',
    'documentType': '证件类型',
  };

  @override
  Widget build(BuildContext context) => Card(
    margin: EdgeInsets.zero,
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          for (final entry in labels.entries)
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 8),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    entry.value,
                    style: const TextStyle(color: Colors.black54, fontSize: 12),
                  ),
                  SelectableText(
                    '${result[entry.key] ?? '—'}',
                    style: const TextStyle(fontSize: 18),
                  ),
                ],
              ),
            ),
          Text(
            result['isExpired'] == true
                ? '已过期'
                : result['isExpired'] == false
                ? '有效期内'
                : '请核对有效期',
          ),
        ],
      ),
    ),
  );
}
0
likes
110
points
327
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

On-device passport MRZ recognition for Flutter Android and iOS.

Topics

#ocr #passport #mrz

License

unknown (license)

Dependencies

flutter, google_mlkit_text_recognition, image

More

Packages that depend on eas_passport_ocr

Packages that implement eas_passport_ocr