controller.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  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. if (!state.isCameraReady) {
  210. openFrontCamera();
  211. }
  212. }
  213. }
  214. /// 遍历当前相机列表并启动前置相机
  215. void openFrontCamera() async {
  216. if (_cameras.isEmpty) {
  217. PromptBox.toast('未找到摄像头');
  218. } else {
  219. for (CameraDescription cameraDescription in _cameras) {
  220. if (cameraDescription.lensDirection == CameraLensDirection.front) {
  221. await openNewCamera(cameraDescription);
  222. lockCaptureOrientation();
  223. update();
  224. state.isCameraReady = true;
  225. break;
  226. }
  227. }
  228. // 如果没找到前置摄像头,启动后置摄像头
  229. if (!state.isCameraReady) {
  230. openBackCamera();
  231. }
  232. }
  233. }
  234. /// 相机锁定旋转
  235. Future<void> lockCaptureOrientation() async {
  236. final CameraController? cameraController = kCameraController;
  237. if (cameraController == null || !cameraController.value.isInitialized) {
  238. PromptBox.toast('Error: select a camera first.');
  239. return;
  240. }
  241. if (!cameraController.value.isCaptureOrientationLocked) {
  242. try {
  243. DeviceOrientation deviceOrientation =
  244. cameraController.value.deviceOrientation;
  245. await cameraController.lockCaptureOrientation(
  246. _isLocalStation ? DeviceOrientation.portraitUp : deviceOrientation);
  247. } on CameraException catch (e) {
  248. PromptBox.toast('Error: ${e.code}\n${e.description}');
  249. }
  250. } else {
  251. PromptBox.toast('Rotation lock is already enabled.');
  252. }
  253. }
  254. /// 执行一次拍摄
  255. Future<XFile?> takePicture() async {
  256. final CameraController? cameraController = kCameraController;
  257. if (cameraController == null || !cameraController.value.isInitialized) {
  258. PromptBox.toast('Error: select a camera first.');
  259. return null;
  260. }
  261. if (cameraController.value.isTakingPicture) {
  262. // A capture is already pending, do nothing.
  263. return null;
  264. }
  265. try {
  266. final XFile file = await cameraController.takePicture();
  267. return file;
  268. } on CameraException catch (e) {
  269. PromptBox.toast('Error: ${e.code}\n${e.description}');
  270. return null;
  271. }
  272. }
  273. /// 发生拍摄身份证事件
  274. void onCaptureIdCardButtonPressed() {
  275. state.isIdCardScanning = true;
  276. state.processingImageLocalPath = '';
  277. takePicture().then((XFile? file) async {
  278. if (file != null) {
  279. state.processingImageLocalPath = file.path;
  280. try {
  281. PatientBaseDTO result;
  282. ///具有权限或者离线情况使用本地离线身份证识别包
  283. if (Store.user.hasFeature(FeatureKeys.IdCardOfflineRecognition)) {
  284. File fileIDCard = File(file.path);
  285. List<int> bytes = await fileIDCard.readAsBytes();
  286. ui.Codec codec =
  287. await ui.instantiateImageCodec(Uint8List.fromList(bytes));
  288. ui.FrameInfo frameInfo = await codec.getNextFrame();
  289. ui.Image image = frameInfo.image;
  290. // 计算裁剪区域 身份证尺寸 85.6毫米×54毫米
  291. if (image.width > 856 && image.height > 540) {
  292. double widthRatio = image.width / 1920.0;
  293. double heightRatio = image.height / 1080.0;
  294. double width = 856 * widthRatio;
  295. double height = 540 * heightRatio;
  296. final pictureRecorder = ui.PictureRecorder();
  297. final canvas = Canvas(pictureRecorder);
  298. var srcRect = Rect.fromLTWH(
  299. 532 * widthRatio, 270 * heightRatio, width, height);
  300. var dstRect = Rect.fromLTWH(0, 0, width, height);
  301. canvas.drawImageRect(image, srcRect, dstRect, Paint());
  302. final picture = pictureRecorder.endRecording();
  303. image = await picture.toImage(width.toInt(), height.toInt());
  304. }
  305. ByteData? byteData = await image.toByteData();
  306. state.processingImageUint8List = byteData!.buffer.asUint8List();
  307. logger.i("getPatientBaseByImageAsync evaluateOneImage start");
  308. final idCardRecogResultInfo =
  309. await CommonUtil.idCardRecognition.evaluateOneImage(image);
  310. if (idCardRecogResultInfo != null) {
  311. logger.i(
  312. "getPatientBaseByImageAsync idCardRecogResultInfo.numerStatus: ${idCardRecogResultInfo.numerStatus}");
  313. } else {
  314. logger.e(
  315. "getPatientBaseByImageAsync CommonUtil.idCardRecognition.evaluateOneImage fial!");
  316. }
  317. if (idCardRecogResultInfo != null &&
  318. idCardRecogResultInfo.numerStatus == 1) {
  319. String formattedDateString = idCardRecogResultInfo.birthdate!
  320. .replaceAll('年', '-')
  321. .replaceAll('月', '-')
  322. .replaceAll('日', '');
  323. DateFormat format = DateFormat('yyyy-MM-dd');
  324. DateTime birthday = format.parse(formattedDateString);
  325. result = PatientBaseDTO(
  326. isSuccess: true,
  327. cardNo: idCardRecogResultInfo.idNumber,
  328. patientName: idCardRecogResultInfo.name,
  329. patientAddress: idCardRecogResultInfo.address,
  330. patientGender: idCardRecogResultInfo.gender == "女"
  331. ? GenderEnum.Female
  332. : GenderEnum.Male,
  333. nationality: idCardRecogResultInfo.nation,
  334. birthday: birthday,
  335. );
  336. } else {
  337. result = PatientBaseDTO(
  338. isSuccess: false, errorMessage: "身份证识别失败,请保持图像清晰完整");
  339. }
  340. } else {
  341. final String fileType = file.path.split('.').last;
  342. if (!['png', 'jpg'].contains(fileType)) {
  343. PromptBox.toast('上传的图像类型错误');
  344. return;
  345. }
  346. final url = await rpc.storage.upload(
  347. file,
  348. fileType: fileType,
  349. );
  350. print('⭐⭐⭐⭐⭐⭐⭐⭐ url: $url');
  351. if (url == null || url.isEmpty) {
  352. PromptBox.toast('图像上传超时,请检测网络');
  353. throw Exception('图像上传超时');
  354. }
  355. result = await rpc.vitalPatient.getPatientBaseByImageAsync(
  356. GetPatientBaseByImageRequest(
  357. token: Store.user.token,
  358. image: url,
  359. ),
  360. );
  361. }
  362. state.processingImageLocalPath = '';
  363. /// 用于关闭 ImageDetectingDialog
  364. if (result.isSuccess) {
  365. PromptBox.toast('身份证识别成功');
  366. final idCardScanResult = IdCardScanResult(
  367. success: true,
  368. patientBaseDTO: result,
  369. );
  370. Get.back<IdCardScanResult>(
  371. result: idCardScanResult,
  372. );
  373. } else {
  374. PromptBox.toast('身份证识别失败,请保持图像清晰完整');
  375. }
  376. } catch (e) {
  377. logger.e("getPatientBaseByImageAsync failed: $e", e);
  378. }
  379. }
  380. state.processingImageLocalPath = '';
  381. state.isIdCardScanning = false;
  382. });
  383. }
  384. /// 在 widget 内存中分配后立即调用。
  385. @override
  386. void onInit() async {
  387. // await initCamera();
  388. super.onInit();
  389. WidgetsBinding.instance.addObserver(this);
  390. }
  391. /// 在 onInit() 之后调用 1 帧。这是进入的理想场所
  392. @override
  393. void onReady() async {
  394. super.onReady();
  395. await initAvailableCameras();
  396. openBackCamera();
  397. }
  398. /// 在 [onDelete] 方法之前调用。
  399. @override
  400. void onClose() {
  401. super.onClose();
  402. final cacheManager = Get.find<ICacheManager>();
  403. cacheManager.clearApplicationImageCache();
  404. WidgetsBinding.instance.removeObserver(this);
  405. final CameraController? cameraController = kCameraController;
  406. if (cameraController != null) {
  407. kCameraController = null;
  408. cameraController.dispose();
  409. }
  410. }
  411. /// dispose 释放内存
  412. @override
  413. void dispose() {
  414. super.dispose();
  415. }
  416. @override
  417. void didChangeAppLifecycleState(AppLifecycleState state) async {
  418. super.didChangeAppLifecycleState(state);
  419. final CameraController? cameraController = kCameraController;
  420. // App state changed before we got the chance to initialize.
  421. if (cameraController == null || !cameraController.value.isInitialized) {
  422. return;
  423. }
  424. if (state == AppLifecycleState.inactive) {
  425. cameraController.dispose();
  426. this.state.isCameraReady = false;
  427. } else if (state == AppLifecycleState.resumed) {
  428. await openNewCamera(cameraController.description);
  429. this.state.isCameraReady = true;
  430. }
  431. }
  432. }
  433. class IdCardScanResult {
  434. bool success;
  435. /// 身份证信息
  436. PatientBaseDTO patientBaseDTO;
  437. IdCardScanResult({
  438. required this.success,
  439. required this.patientBaseDTO,
  440. });
  441. }