123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435 |
- import 'dart:async';
- import 'dart:io';
- import 'package:camera/camera.dart';
- import 'package:fis_jsonrpc/rpc.dart';
- import 'package:flutter/foundation.dart';
- import 'package:flutter/material.dart';
- import 'package:flutter/services.dart';
- import 'package:flutter_smartscan_plugin/id_card_recognition.dart';
- import 'package:get/get.dart';
- import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
- import 'package:intl/intl.dart';
- import 'package:vitalapp/architecture/storage/storage.dart';
- import 'package:vitalapp/architecture/utils/common_util.dart';
- import 'package:vitalapp/architecture/utils/prompt_box.dart';
- import 'package:vitalapp/architecture/values/features.dart';
- import 'package:vitalapp/global.dart';
- import 'package:vitalapp/managers/interfaces/cache.dart';
- import 'package:fis_common/logger/logger.dart';
- import 'package:vitalapp/managers/interfaces/patient.dart';
- import 'package:vitalapp/rpc.dart';
- import 'package:vitalapp/store/store.dart';
- import 'index.dart';
- import 'dart:ui' as ui;
- class IdCardScanController extends GetxController with WidgetsBindingObserver {
- IdCardScanController();
- final state = IdCardScanState();
- List<CameraDescription> _cameras = <CameraDescription>[];
- List<CameraDescription> get cameras => _cameras;
- // final idCardRecognition = IDCardRecognition();
- CameraController? kCameraController;
- double _minAvailableExposureOffset = 0.0;
- double _maxAvailableExposureOffset = 0.0;
- double _minAvailableZoom = 1.0;
- double _maxAvailableZoom = 1.0;
- final double _currentScale = 1.0;
- double _baseScale = 1.0;
- // 屏幕上手指数量
- int pointers = 0;
- /// 开始缩放
- void handleScaleStart(ScaleStartDetails details) {
- _baseScale = _currentScale;
- }
- /// 当前捕获帧的人脸列表
- List<Face> kFrameFacesResult = [];
- /// 当前捕获帧大小
- Size kFrameImageSize = Size.zero;
- /// 缩放更新
- Future<void> handleScaleUpdate(ScaleUpdateDetails details) async {
- // When there are not exactly two fingers on screen don't scale
- if (kCameraController == null || pointers != 2) {
- return;
- }
- // 屏蔽缩放
- // _currentScale = (_baseScale * details.scale)
- // .clamp(_minAvailableZoom, _maxAvailableZoom);
- // await kCameraController!.setZoomLevel(_currentScale);
- }
- /// 修改对焦点 [暂不执行]
- void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) {
- if (kCameraController == null) {
- return;
- }
- final CameraController cameraController = kCameraController!;
- final Offset offset = Offset(
- details.localPosition.dx / constraints.maxWidth,
- details.localPosition.dy / constraints.maxHeight,
- );
- cameraController.setExposurePoint(offset);
- cameraController.setFocusPoint(offset);
- }
- /// 初始化相机
- Future<void> initAvailableCameras() async {
- try {
- _cameras = await availableCameras();
- if (_cameras.isNotEmpty) {
- // state.isCameraReady = true;
- }
- // print("cameras: ${_cameras.length}");
- } on CameraException catch (e) {
- logger.e("cameras: ${e.code} ${e.description}");
- }
- }
- /// 启动指定相机
- Future<void> openNewCamera(CameraDescription cameraDescription) async {
- final CameraController? oldController = kCameraController;
- if (oldController != null) {
- // `kCameraController` needs to be set to null before getting disposed,
- // to avoid a race condition when we use the kCameraController that is being
- // disposed. This happens when camera permission dialog shows up,
- // which triggers `didChangeAppLifecycleState`, which disposes and
- // re-creates the kCameraController.
- kCameraController = null;
- await oldController.dispose();
- }
- final CameraController cameraController = CameraController(
- cameraDescription,
- ResolutionPreset.max,
- enableAudio: false,
- imageFormatGroup: ImageFormatGroup.jpeg,
- );
- kCameraController = cameraController;
- // If the kCameraController is updated then update the UI.
- cameraController.addListener(() {
- if (cameraController.value.hasError) {
- PromptBox.toast(
- "Camera error ${cameraController.value.errorDescription}");
- }
- });
- try {
- await cameraController.initialize();
- await Future.wait(<Future<Object?>>[
- // The exposure mode is currently not supported on the web.
- ...!kIsWeb
- ? <Future<Object?>>[
- cameraController.getMinExposureOffset().then(
- (double value) => _minAvailableExposureOffset = value),
- cameraController
- .getMaxExposureOffset()
- .then((double value) => _maxAvailableExposureOffset = value)
- ]
- : <Future<Object?>>[],
- cameraController
- .getMaxZoomLevel()
- .then((double value) => _maxAvailableZoom = value),
- cameraController
- .getMinZoomLevel()
- .then((double value) => _minAvailableZoom = value),
- ]);
- } on CameraException catch (e) {
- switch (e.code) {
- case 'CameraAccessDenied':
- PromptBox.toast('You have denied camera access.');
- break;
- case 'CameraAccessDeniedWithoutPrompt':
- // iOS only
- PromptBox.toast('Please go to Settings app to enable camera access.');
- break;
- case 'CameraAccessRestricted':
- // iOS only
- PromptBox.toast('Camera access is restricted.');
- break;
- case 'AudioAccessDenied':
- PromptBox.toast('You have denied audio access.');
- break;
- case 'AudioAccessDeniedWithoutPrompt':
- // iOS only
- PromptBox.toast('Please go to Settings app to enable audio access.');
- break;
- case 'AudioAccessRestricted':
- // iOS only
- PromptBox.toast('Audio access is restricted.');
- break;
- default:
- PromptBox.toast('Error: ${e.code}\n${e.description}');
- break;
- }
- }
- }
- /// 遍历当前相机列表并启动后置相机
- void openBackCamera() async {
- if (_cameras.isEmpty) {
- PromptBox.toast('Error: No cameras found.');
- } else {
- for (CameraDescription cameraDescription in _cameras) {
- if (cameraDescription.lensDirection == CameraLensDirection.back) {
- await openNewCamera(cameraDescription);
- lockCaptureOrientation();
- update();
- state.isCameraReady = true;
- break;
- }
- }
- }
- }
- /// 遍历当前相机列表并启动前置相机
- void openFrontCamera() async {
- if (_cameras.isEmpty) {
- PromptBox.toast('Error: No cameras found.');
- } else {
- for (CameraDescription cameraDescription in _cameras) {
- if (cameraDescription.lensDirection == CameraLensDirection.front) {
- await openNewCamera(cameraDescription);
- lockCaptureOrientation();
- update();
- state.isCameraReady = true;
- break;
- }
- }
- }
- }
- /// 相机锁定旋转
- Future<void> lockCaptureOrientation() async {
- final CameraController? cameraController = kCameraController;
- if (cameraController == null || !cameraController.value.isInitialized) {
- PromptBox.toast('Error: select a camera first.');
- return;
- }
- if (!cameraController.value.isCaptureOrientationLocked) {
- try {
- await cameraController
- .lockCaptureOrientation(DeviceOrientation.landscapeLeft);
- } on CameraException catch (e) {
- PromptBox.toast('Error: ${e.code}\n${e.description}');
- }
- } else {
- PromptBox.toast('Rotation lock is already enabled.');
- }
- }
- /// 执行一次拍摄
- Future<XFile?> takePicture() async {
- final CameraController? cameraController = kCameraController;
- if (cameraController == null || !cameraController.value.isInitialized) {
- PromptBox.toast('Error: select a camera first.');
- return null;
- }
- if (cameraController.value.isTakingPicture) {
- // A capture is already pending, do nothing.
- return null;
- }
- try {
- final XFile file = await cameraController.takePicture();
- return file;
- } on CameraException catch (e) {
- PromptBox.toast('Error: ${e.code}\n${e.description}');
- return null;
- }
- }
- /// 发生拍摄身份证事件
- void onCaptureIdCardButtonPressed() {
- state.isIdCardScanning = true;
- state.processingImageLocalPath = '';
- takePicture().then((XFile? file) async {
- if (file != null) {
- state.processingImageLocalPath = file.path;
- try {
- PatientBaseDTO result;
- ///具有权限或者离线情况使用本地离线身份证识别包
- if (Store.user.hasFeature(FeatureKeys.IdCardOfflineRecognition)) {
- File fileIDCard = File(file.path);
- List<int> bytes = await fileIDCard.readAsBytes();
- ui.Codec codec =
- await ui.instantiateImageCodec(Uint8List.fromList(bytes));
- ui.FrameInfo frameInfo = await codec.getNextFrame();
- ui.Image image = frameInfo.image;
- // 计算裁剪区域 身份证尺寸 85.6毫米×54毫米
- if (image.width > 856 && image.height > 540) {
- final pictureRecorder = ui.PictureRecorder();
- final canvas = Canvas(pictureRecorder);
- const srcRect = Rect.fromLTWH(532, 270, 856, 540);
- const dstRect = Rect.fromLTWH(0, 0, 856, 540);
- canvas.drawImageRect(image, srcRect, dstRect, Paint());
- final picture = pictureRecorder.endRecording();
- image = await picture.toImage(856, 540);
- }
- ByteData? byteData = await image.toByteData();
- state.processingImageUint8List = byteData!.buffer.asUint8List();
- logger.i("getPatientBaseByImageAsync evaluateOneImage start");
- final idCardRecogResultInfo =
- await CommonUtil.idCardRecognition.evaluateOneImage(image);
- if (idCardRecogResultInfo != null) {
- logger.i(
- "getPatientBaseByImageAsync idCardRecogResultInfo.numerStatus: ${idCardRecogResultInfo.numerStatus}");
- } else {
- logger.e(
- "getPatientBaseByImageAsync CommonUtil.idCardRecognition.evaluateOneImage fial!");
- }
- if (idCardRecogResultInfo != null &&
- idCardRecogResultInfo.numerStatus == 1) {
- String formattedDateString = idCardRecogResultInfo.birthdate!
- .replaceAll('年', '-')
- .replaceAll('月', '-')
- .replaceAll('日', '');
- DateFormat format = DateFormat('yyyy-MM-dd');
- DateTime birthday = format.parse(formattedDateString);
- result = PatientBaseDTO(
- isSuccess: true,
- cardNo: idCardRecogResultInfo.idNumber,
- patientName: idCardRecogResultInfo.name,
- patientAddress: idCardRecogResultInfo.address,
- patientGender: idCardRecogResultInfo.gender == "女"
- ? GenderEnum.Female
- : GenderEnum.Male,
- nationality: idCardRecogResultInfo.nation,
- birthday: birthday,
- );
- } else {
- result = PatientBaseDTO(
- isSuccess: false, errorMessage: "身份证识别失败,请保持图像清晰完整");
- }
- // CommonUtil.idCardRecognition.release();
- } else {
- final String fileType = file.path.split('.').last;
- if (!['png', 'jpg'].contains(fileType)) {
- PromptBox.toast('上传的图像类型错误');
- return;
- }
- final url = await rpc.storage.upload(
- file,
- fileType: fileType,
- );
- print('⭐⭐⭐⭐⭐⭐⭐⭐ url: $url');
- if (url == null || url.isEmpty) {
- PromptBox.toast('图像上传超时,请检测网络');
- throw Exception('图像上传超时');
- }
- result = await rpc.patient.getPatientBaseByImageAsync(
- GetPatientBaseByImageRequest(
- token: Store.user.token,
- image: url,
- ),
- );
- }
- state.processingImageLocalPath = '';
- /// 用于关闭 ImageDetectingDialog
- if (result.isSuccess) {
- PromptBox.toast('身份证识别成功');
- final idCardScanResult = IdCardScanResult(
- success: true,
- patientBaseDTO: result,
- );
- Get.back<IdCardScanResult>(
- result: idCardScanResult,
- );
- } else {
- PromptBox.toast('身份证识别失败,请保持图像清晰完整');
- }
- } catch (e) {
- logger.e("getPatientBaseByImageAsync failed: $e", e);
- }
- }
- state.processingImageLocalPath = '';
- state.isIdCardScanning = false;
- });
- }
- /// 在 widget 内存中分配后立即调用。
- @override
- void onInit() async {
- // await initCamera();
- super.onInit();
- WidgetsBinding.instance.addObserver(this);
- }
- /// 在 onInit() 之后调用 1 帧。这是进入的理想场所
- @override
- void onReady() async {
- super.onReady();
- await initAvailableCameras();
- openBackCamera();
- }
- /// 在 [onDelete] 方法之前调用。
- @override
- void onClose() {
- super.onClose();
- final cacheManager = Get.find<ICacheManager>();
- cacheManager.clearApplicationImageCache();
- WidgetsBinding.instance.removeObserver(this);
- final CameraController? cameraController = kCameraController;
- if (cameraController != null) {
- kCameraController = null;
- cameraController.dispose();
- }
- }
- /// dispose 释放内存
- @override
- void dispose() {
- super.dispose();
- }
- @override
- void didChangeAppLifecycleState(AppLifecycleState state) async {
- super.didChangeAppLifecycleState(state);
- final CameraController? cameraController = kCameraController;
- // App state changed before we got the chance to initialize.
- if (cameraController == null || !cameraController.value.isInitialized) {
- return;
- }
- if (state == AppLifecycleState.inactive) {
- cameraController.dispose();
- this.state.isCameraReady = false;
- } else if (state == AppLifecycleState.resumed) {
- await openNewCamera(cameraController.description);
- this.state.isCameraReady = true;
- }
- }
- }
- class IdCardScanResult {
- bool success;
- /// 身份证信息
- PatientBaseDTO patientBaseDTO;
- IdCardScanResult({
- required this.success,
- required this.patientBaseDTO,
- });
- }
|