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' if (dart.library.html) "package:vitalapp/architecture/utils/id_card.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/managers/interfaces/cache.dart'; import 'package:fis_common/logger/logger.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 _cameras = []; List get cameras => _cameras; // final idCardRecognition = IDCardRecognition(); 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 kFrameFacesResult = []; /// 当前捕获帧大小 Size kFrameImageSize = Size.zero; /// 缩放更新 Future 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 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 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(>[ // The exposure mode is currently not supported on the web. ...!kIsWeb ? >[ cameraController.getMinExposureOffset().then( (double value) => _minAvailableExposureOffset = value), cameraController .getMaxExposureOffset() .then((double value) => _maxAvailableExposureOffset = value) ] : >[], 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('未找到摄像头'); } 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('未找到摄像头'); } else { for (CameraDescription cameraDescription in _cameras) { if (cameraDescription.lensDirection == CameraLensDirection.front) { await openNewCamera(cameraDescription); lockCaptureOrientation(); update(); state.isCameraReady = true; break; } } } } /// 相机锁定旋转 Future 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 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 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: "身份证识别失败,请保持图像清晰完整"); } } 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.vitalPatient.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( 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(); 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, }); }