controller.dart 16 KB

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