controller.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'package:camera/camera.dart';
  4. import 'package:fis_jsonrpc/rpc.dart';
  5. import 'package:flutter/foundation.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter/services.dart';
  8. import 'package:flutter_smartscan_plugin/id_card_recognition.dart';
  9. import 'package:get/get.dart';
  10. import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
  11. import 'package:intl/intl.dart';
  12. import 'package:vitalapp/architecture/storage/storage.dart';
  13. import 'package:vitalapp/architecture/utils/common_util.dart';
  14. import 'package:vitalapp/architecture/utils/prompt_box.dart';
  15. import 'package:vitalapp/architecture/values/features.dart';
  16. import 'package:vitalapp/global.dart';
  17. import 'package:vitalapp/managers/interfaces/cache.dart';
  18. import 'package:fis_common/logger/logger.dart';
  19. import 'package:vitalapp/managers/interfaces/patient.dart';
  20. import 'package:vitalapp/rpc.dart';
  21. import 'package:vitalapp/store/store.dart';
  22. import 'index.dart';
  23. import 'dart:ui' as ui;
  24. class IdCardScanController extends GetxController with WidgetsBindingObserver {
  25. IdCardScanController();
  26. final state = IdCardScanState();
  27. List<CameraDescription> _cameras = <CameraDescription>[];
  28. List<CameraDescription> get cameras => _cameras;
  29. // final idCardRecognition = IDCardRecognition();
  30. CameraController? kCameraController;
  31. double _minAvailableExposureOffset = 0.0;
  32. double _maxAvailableExposureOffset = 0.0;
  33. double _minAvailableZoom = 1.0;
  34. double _maxAvailableZoom = 1.0;
  35. final double _currentScale = 1.0;
  36. double _baseScale = 1.0;
  37. // 屏幕上手指数量
  38. int pointers = 0;
  39. /// 开始缩放
  40. void handleScaleStart(ScaleStartDetails details) {
  41. _baseScale = _currentScale;
  42. }
  43. /// 当前捕获帧的人脸列表
  44. List<Face> kFrameFacesResult = [];
  45. /// 当前捕获帧大小
  46. Size kFrameImageSize = Size.zero;
  47. /// 缩放更新
  48. Future<void> handleScaleUpdate(ScaleUpdateDetails details) async {
  49. // When there are not exactly two fingers on screen don't scale
  50. if (kCameraController == null || pointers != 2) {
  51. return;
  52. }
  53. // 屏蔽缩放
  54. // _currentScale = (_baseScale * details.scale)
  55. // .clamp(_minAvailableZoom, _maxAvailableZoom);
  56. // await kCameraController!.setZoomLevel(_currentScale);
  57. }
  58. /// 修改对焦点 [暂不执行]
  59. void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) {
  60. if (kCameraController == null) {
  61. return;
  62. }
  63. final CameraController cameraController = kCameraController!;
  64. final Offset offset = Offset(
  65. details.localPosition.dx / constraints.maxWidth,
  66. details.localPosition.dy / constraints.maxHeight,
  67. );
  68. cameraController.setExposurePoint(offset);
  69. cameraController.setFocusPoint(offset);
  70. }
  71. /// 初始化相机
  72. Future<void> initAvailableCameras() async {
  73. try {
  74. _cameras = await availableCameras();
  75. if (_cameras.isNotEmpty) {
  76. // state.isCameraReady = true;
  77. }
  78. // print("cameras: ${_cameras.length}");
  79. } on CameraException catch (e) {
  80. logger.e("cameras: ${e.code} ${e.description}");
  81. }
  82. }
  83. /// 启动指定相机
  84. Future<void> openNewCamera(CameraDescription cameraDescription) async {
  85. final CameraController? oldController = kCameraController;
  86. if (oldController != null) {
  87. // `kCameraController` needs to be set to null before getting disposed,
  88. // to avoid a race condition when we use the kCameraController that is being
  89. // disposed. This happens when camera permission dialog shows up,
  90. // which triggers `didChangeAppLifecycleState`, which disposes and
  91. // re-creates the kCameraController.
  92. kCameraController = null;
  93. await oldController.dispose();
  94. }
  95. final CameraController cameraController = CameraController(
  96. cameraDescription,
  97. ResolutionPreset.max,
  98. enableAudio: false,
  99. imageFormatGroup: ImageFormatGroup.jpeg,
  100. );
  101. kCameraController = cameraController;
  102. // If the kCameraController is updated then update the UI.
  103. cameraController.addListener(() {
  104. if (cameraController.value.hasError) {
  105. PromptBox.toast(
  106. "Camera error ${cameraController.value.errorDescription}");
  107. }
  108. });
  109. try {
  110. await cameraController.initialize();
  111. await Future.wait(<Future<Object?>>[
  112. // The exposure mode is currently not supported on the web.
  113. ...!kIsWeb
  114. ? <Future<Object?>>[
  115. cameraController.getMinExposureOffset().then(
  116. (double value) => _minAvailableExposureOffset = value),
  117. cameraController
  118. .getMaxExposureOffset()
  119. .then((double value) => _maxAvailableExposureOffset = value)
  120. ]
  121. : <Future<Object?>>[],
  122. cameraController
  123. .getMaxZoomLevel()
  124. .then((double value) => _maxAvailableZoom = value),
  125. cameraController
  126. .getMinZoomLevel()
  127. .then((double value) => _minAvailableZoom = value),
  128. ]);
  129. } on CameraException catch (e) {
  130. switch (e.code) {
  131. case 'CameraAccessDenied':
  132. PromptBox.toast('You have denied camera access.');
  133. break;
  134. case 'CameraAccessDeniedWithoutPrompt':
  135. // iOS only
  136. PromptBox.toast('Please go to Settings app to enable camera access.');
  137. break;
  138. case 'CameraAccessRestricted':
  139. // iOS only
  140. PromptBox.toast('Camera access is restricted.');
  141. break;
  142. case 'AudioAccessDenied':
  143. PromptBox.toast('You have denied audio access.');
  144. break;
  145. case 'AudioAccessDeniedWithoutPrompt':
  146. // iOS only
  147. PromptBox.toast('Please go to Settings app to enable audio access.');
  148. break;
  149. case 'AudioAccessRestricted':
  150. // iOS only
  151. PromptBox.toast('Audio access is restricted.');
  152. break;
  153. default:
  154. PromptBox.toast('Error: ${e.code}\n${e.description}');
  155. break;
  156. }
  157. }
  158. }
  159. /// 遍历当前相机列表并启动后置相机
  160. void openBackCamera() async {
  161. if (_cameras.isEmpty) {
  162. PromptBox.toast('Error: No cameras found.');
  163. } else {
  164. for (CameraDescription cameraDescription in _cameras) {
  165. if (cameraDescription.lensDirection == CameraLensDirection.back) {
  166. await openNewCamera(cameraDescription);
  167. lockCaptureOrientation();
  168. update();
  169. state.isCameraReady = true;
  170. break;
  171. }
  172. }
  173. }
  174. }
  175. /// 遍历当前相机列表并启动前置相机
  176. void openFrontCamera() async {
  177. if (_cameras.isEmpty) {
  178. PromptBox.toast('Error: No cameras found.');
  179. } else {
  180. for (CameraDescription cameraDescription in _cameras) {
  181. if (cameraDescription.lensDirection == CameraLensDirection.front) {
  182. await openNewCamera(cameraDescription);
  183. lockCaptureOrientation();
  184. update();
  185. state.isCameraReady = true;
  186. break;
  187. }
  188. }
  189. }
  190. }
  191. /// 相机锁定旋转
  192. Future<void> lockCaptureOrientation() async {
  193. final CameraController? cameraController = kCameraController;
  194. if (cameraController == null || !cameraController.value.isInitialized) {
  195. PromptBox.toast('Error: select a camera first.');
  196. return;
  197. }
  198. if (!cameraController.value.isCaptureOrientationLocked) {
  199. try {
  200. await cameraController
  201. .lockCaptureOrientation(DeviceOrientation.landscapeLeft);
  202. } on CameraException catch (e) {
  203. PromptBox.toast('Error: ${e.code}\n${e.description}');
  204. }
  205. } else {
  206. PromptBox.toast('Rotation lock is already enabled.');
  207. }
  208. }
  209. /// 执行一次拍摄
  210. Future<XFile?> takePicture() async {
  211. final CameraController? cameraController = kCameraController;
  212. if (cameraController == null || !cameraController.value.isInitialized) {
  213. PromptBox.toast('Error: select a camera first.');
  214. return null;
  215. }
  216. if (cameraController.value.isTakingPicture) {
  217. // A capture is already pending, do nothing.
  218. return null;
  219. }
  220. try {
  221. final XFile file = await cameraController.takePicture();
  222. return file;
  223. } on CameraException catch (e) {
  224. PromptBox.toast('Error: ${e.code}\n${e.description}');
  225. return null;
  226. }
  227. }
  228. /// 发生拍摄身份证事件
  229. void onCaptureIdCardButtonPressed() {
  230. state.isIdCardScanning = true;
  231. state.processingImageLocalPath = '';
  232. takePicture().then((XFile? file) async {
  233. if (file != null) {
  234. state.processingImageLocalPath = file.path;
  235. try {
  236. PatientBaseDTO result;
  237. ///具有权限或者离线情况使用本地离线身份证识别包
  238. if (Store.user.hasFeature(FeatureKeys.IdCardOfflineRecognition)) {
  239. File fileIDCard = File(file.path);
  240. List<int> bytes = await fileIDCard.readAsBytes();
  241. ui.Codec codec =
  242. await ui.instantiateImageCodec(Uint8List.fromList(bytes));
  243. ui.FrameInfo frameInfo = await codec.getNextFrame();
  244. ui.Image image = frameInfo.image;
  245. // 计算裁剪区域 身份证尺寸 85.6毫米×54毫米
  246. if (image.width > 856 && image.height > 540) {
  247. final pictureRecorder = ui.PictureRecorder();
  248. final canvas = Canvas(pictureRecorder);
  249. const srcRect = Rect.fromLTWH(532, 270, 856, 540);
  250. const dstRect = Rect.fromLTWH(0, 0, 856, 540);
  251. canvas.drawImageRect(image, srcRect, dstRect, Paint());
  252. final picture = pictureRecorder.endRecording();
  253. image = await picture.toImage(856, 540);
  254. }
  255. ByteData? byteData = await image.toByteData();
  256. state.processingImageUint8List = byteData!.buffer.asUint8List();
  257. logger.i("getPatientBaseByImageAsync evaluateOneImage start");
  258. final idCardRecogResultInfo =
  259. await CommonUtil.idCardRecognition.evaluateOneImage(image);
  260. if (idCardRecogResultInfo != null) {
  261. logger.i(
  262. "getPatientBaseByImageAsync idCardRecogResultInfo.numerStatus: ${idCardRecogResultInfo.numerStatus}");
  263. } else {
  264. logger.e(
  265. "getPatientBaseByImageAsync CommonUtil.idCardRecognition.evaluateOneImage fial!");
  266. }
  267. if (idCardRecogResultInfo != null &&
  268. idCardRecogResultInfo.numerStatus == 1) {
  269. String formattedDateString = idCardRecogResultInfo.birthdate!
  270. .replaceAll('年', '-')
  271. .replaceAll('月', '-')
  272. .replaceAll('日', '');
  273. DateFormat format = DateFormat('yyyy-MM-dd');
  274. DateTime birthday = format.parse(formattedDateString);
  275. result = PatientBaseDTO(
  276. isSuccess: true,
  277. cardNo: idCardRecogResultInfo.idNumber,
  278. patientName: idCardRecogResultInfo.name,
  279. patientAddress: idCardRecogResultInfo.address,
  280. patientGender: idCardRecogResultInfo.gender == "女"
  281. ? GenderEnum.Female
  282. : GenderEnum.Male,
  283. nationality: idCardRecogResultInfo.nation,
  284. birthday: birthday,
  285. );
  286. } else {
  287. result = PatientBaseDTO(
  288. isSuccess: false, errorMessage: "身份证识别失败,请保持图像清晰完整");
  289. }
  290. // CommonUtil.idCardRecognition.release();
  291. } else {
  292. final String fileType = file.path.split('.').last;
  293. if (!['png', 'jpg'].contains(fileType)) {
  294. PromptBox.toast('上传的图像类型错误');
  295. return;
  296. }
  297. final url = await rpc.storage.upload(
  298. file,
  299. fileType: fileType,
  300. );
  301. print('⭐⭐⭐⭐⭐⭐⭐⭐ url: $url');
  302. if (url == null || url.isEmpty) {
  303. PromptBox.toast('图像上传超时,请检测网络');
  304. throw Exception('图像上传超时');
  305. }
  306. result = await rpc.patient.getPatientBaseByImageAsync(
  307. GetPatientBaseByImageRequest(
  308. token: Store.user.token,
  309. image: url,
  310. ),
  311. );
  312. }
  313. state.processingImageLocalPath = '';
  314. /// 用于关闭 ImageDetectingDialog
  315. if (result.isSuccess) {
  316. PromptBox.toast('身份证识别成功');
  317. final idCardScanResult = IdCardScanResult(
  318. success: true,
  319. patientBaseDTO: result,
  320. );
  321. Get.back<IdCardScanResult>(
  322. result: idCardScanResult,
  323. );
  324. } else {
  325. PromptBox.toast('身份证识别失败,请保持图像清晰完整');
  326. }
  327. } catch (e) {
  328. logger.e("getPatientBaseByImageAsync failed: $e", e);
  329. }
  330. }
  331. state.processingImageLocalPath = '';
  332. state.isIdCardScanning = false;
  333. });
  334. }
  335. /// 在 widget 内存中分配后立即调用。
  336. @override
  337. void onInit() async {
  338. // await initCamera();
  339. super.onInit();
  340. WidgetsBinding.instance.addObserver(this);
  341. }
  342. /// 在 onInit() 之后调用 1 帧。这是进入的理想场所
  343. @override
  344. void onReady() async {
  345. super.onReady();
  346. await initAvailableCameras();
  347. openBackCamera();
  348. }
  349. /// 在 [onDelete] 方法之前调用。
  350. @override
  351. void onClose() {
  352. super.onClose();
  353. final cacheManager = Get.find<ICacheManager>();
  354. cacheManager.clearApplicationImageCache();
  355. WidgetsBinding.instance.removeObserver(this);
  356. final CameraController? cameraController = kCameraController;
  357. if (cameraController != null) {
  358. kCameraController = null;
  359. cameraController.dispose();
  360. }
  361. }
  362. /// dispose 释放内存
  363. @override
  364. void dispose() {
  365. super.dispose();
  366. }
  367. @override
  368. void didChangeAppLifecycleState(AppLifecycleState state) async {
  369. super.didChangeAppLifecycleState(state);
  370. final CameraController? cameraController = kCameraController;
  371. // App state changed before we got the chance to initialize.
  372. if (cameraController == null || !cameraController.value.isInitialized) {
  373. return;
  374. }
  375. if (state == AppLifecycleState.inactive) {
  376. cameraController.dispose();
  377. this.state.isCameraReady = false;
  378. } else if (state == AppLifecycleState.resumed) {
  379. await openNewCamera(cameraController.description);
  380. this.state.isCameraReady = true;
  381. }
  382. }
  383. }
  384. class IdCardScanResult {
  385. bool success;
  386. /// 身份证信息
  387. PatientBaseDTO patientBaseDTO;
  388. IdCardScanResult({
  389. required this.success,
  390. required this.patientBaseDTO,
  391. });
  392. }