photo_preview_page.dart 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import 'dart:convert';
  2. import 'dart:io';
  3. import 'package:auto_route/annotations.dart';
  4. import 'package:bot_toast/bot_toast.dart';
  5. import 'package:eitc_erm_dental_flutter/db_util.dart';
  6. import 'package:eitc_erm_dental_flutter/entity/db/local_patient_info.dart';
  7. import 'package:eitc_erm_dental_flutter/exts.dart';
  8. import 'package:eitc_erm_dental_flutter/global.dart';
  9. import 'package:eitc_erm_dental_flutter/pages/view/widget/preview_operation_view.dart';
  10. import 'package:eitc_erm_dental_flutter/pages/view/widget/preview_title.dart';
  11. import 'package:eitc_erm_dental_flutter/pages/view/widget/video_patient_info_bar.dart';
  12. import 'package:eitc_erm_dental_flutter/sp_util.dart';
  13. import 'package:flutter/material.dart';
  14. import 'package:image/image.dart' as img;
  15. import 'package:path_provider/path_provider.dart';
  16. import 'package:photo_view/photo_view.dart';
  17. import '../../funcs.dart';
  18. ///照片预览页面
  19. @RoutePage(name: "photoPreviewRoute")
  20. class PhotoPreviewPage extends StatefulWidget {
  21. final LocalPatientInfo info;
  22. final String path;
  23. final String area;
  24. final DateTime time;
  25. final String mobile;
  26. final String wifi;
  27. final String deviceModel;
  28. const PhotoPreviewPage(
  29. {super.key,
  30. required this.info,
  31. required this.path,
  32. required this.area,
  33. required this.time,
  34. required this.mobile,
  35. required this.deviceModel,
  36. required this.wifi});
  37. @override
  38. State<PhotoPreviewPage> createState() => _PhotoPreviewPageState();
  39. }
  40. class _PhotoPreviewPageState extends State<PhotoPreviewPage> {
  41. bool _isSaving = false;
  42. bool _isSaved = false;
  43. @override
  44. void initState() {
  45. super.initState();
  46. }
  47. @override
  48. void dispose() {
  49. super.dispose();
  50. _deleteCache();
  51. }
  52. @override
  53. Widget build(BuildContext context) {
  54. Size screenSize = MediaQuery.of(context).size;
  55. return PopScope(
  56. canPop: !_isSaving,
  57. child: Scaffold(
  58. body: SafeArea(
  59. child: Container(
  60. color: Colors.black,
  61. child: Stack(
  62. children: [
  63. PhotoView.customChild(
  64. childSize: MediaQuery.of(context).orientation ==
  65. Orientation.landscape
  66. ? Size(screenSize.width,
  67. screenSize.height + screenSize.height / 2)
  68. : null,
  69. child: Container(
  70. alignment: Alignment.center,
  71. child: Column(
  72. mainAxisSize: MainAxisSize.min,
  73. children: [
  74. Image.file(File(widget.path)),
  75. VideoPatientInfoBar(
  76. info: widget.info,
  77. orientation: MediaQuery.of(context).orientation,
  78. area: widget.area,
  79. deviceModel: widget.deviceModel,
  80. time: widget.time,
  81. ),
  82. ],
  83. ),
  84. )),
  85. Align(
  86. alignment: Alignment.topLeft,
  87. child: PreviewTitle(
  88. title: getS().previewPhoto,
  89. ),
  90. ),
  91. PreviewOperationView(
  92. onSave: _onSave,
  93. onCancel: _onCancel,
  94. ),
  95. ],
  96. ),
  97. ),
  98. ),
  99. ));
  100. }
  101. Future<String?> _editPhoto() async {
  102. int height = 55;
  103. int fontHeight = 48;
  104. //读取原图片
  105. logd("读取原图片,path=${widget.path}");
  106. img.Command sourceImgCmd = img.Command();
  107. sourceImgCmd.decodeJpgFile(widget.path);
  108. await sourceImgCmd.executeThread();
  109. img.Image? sourceImg = await sourceImgCmd.getImage();
  110. if (sourceImg == null) {
  111. logd("读取原图片返回null");
  112. return null;
  113. }
  114. logd("原图片读取成功,image=${sourceImg.toString()}");
  115. logd("读取名字图片");
  116. LocalPatientInfo? patientInfo =
  117. await DbUtil.instance.getLocalPatientById(selectedPatientId);
  118. //读取名字图片
  119. img.Image? nameImg;
  120. img.Command? nameImgCmd;
  121. if (patientInfo != null && !patientInfo.namePic.isNullOrEmpty) {
  122. nameImgCmd = img.Command();
  123. nameImgCmd.decodePng(base64Decode(patientInfo.namePic!));
  124. await nameImgCmd.executeThread();
  125. nameImg = await nameImgCmd.getImage();
  126. logd("名字读片读取成功,image=${nameImg.toString()}");
  127. } else {
  128. logd("名字图片不存在,咨询人信息为null或名字图片字段为空");
  129. }
  130. //性别
  131. int gender = getGenderFromIdCard(widget.info.idCardDecrypt);
  132. String sex = getS().unknown;
  133. if (gender == 0) {
  134. sex = getS().male;
  135. } else if (gender == 1) {
  136. sex = getS().female;
  137. }
  138. //年龄
  139. int age = getAgeFromIdCard(
  140. widget.info.idCardDecrypt.isEmpty ? "--" : widget.info.idCardDecrypt);
  141. //区域
  142. String toothArea = toothAreaTranslate(widget.area);
  143. //时间
  144. String yyyy = widget.time.yyyyMMddHHmmss;
  145. logd("信息,性别=$sex,年龄=$age,area=$toothArea,时间=$yyyy");
  146. logd("创建黑条并写入信息");
  147. int gap = 30;
  148. //创建黑条
  149. img.Command blackCmd = img.Command()
  150. ..createImage(
  151. width: sourceImg.width,
  152. height: height,
  153. )
  154. ..fill(color: img.ColorRgb8(0, 0, 0))
  155. ..drawString(
  156. "/$sex/${getS().xxAge(age < 0 ? 0 : age)} $toothArea $yyyy",
  157. font: photoFont ?? img.arial48,
  158. x: (nameImg?.width ?? 0) + gap,
  159. y: (height - fontHeight) ~/ 2,
  160. color: img.ColorRgb8(255, 255, 255))
  161. ..drawString(widget.deviceModel.isEmpty ? "--" : widget.deviceModel,
  162. font: photoFont ?? img.arial48,
  163. rightJustify: true,
  164. //右对齐坐标从右往左算
  165. x: sourceImg.width - gap,
  166. y: (height - fontHeight) ~/ 2,
  167. color: img.ColorRgb8(255, 255, 255));
  168. if (nameImg != null && nameImgCmd != null) {
  169. logd("存在名字图片,合并到黑条中");
  170. blackCmd.compositeImage(nameImgCmd,
  171. dstX: gap, dstY: (height - nameImg.height) ~/ 2);
  172. }
  173. logd("黑条创建成功");
  174. logd("创建最终图片,合并原图片和黑条");
  175. //创建一个更大的图片装下原图片和黑条
  176. img.Command allCmd = img.Command();
  177. allCmd.createImage(
  178. width: sourceImg.width, height: sourceImg.height + height);
  179. allCmd.compositeImage(sourceImgCmd);
  180. allCmd.compositeImage(blackCmd, dstY: sourceImg.height);
  181. logd("最终图片创建成功");
  182. logd("开始保存");
  183. //保存
  184. Directory? dir;
  185. if (Platform.isIOS) {
  186. dir = await getApplicationDocumentsDirectory();
  187. } else {
  188. dir = await getDownloadsDirectory();
  189. }
  190. if (dir == null) {
  191. return null;
  192. }
  193. dir = Directory("${dir.path}/${Platform.isIOS ? "jpgimage" : "tempPhoto"}");
  194. if (!await dir.exists()) {
  195. await dir.create(recursive: true);
  196. }
  197. String prefix = makeFilePrefix(
  198. name: widget.info.name ?? "",
  199. idCard: widget.info.idCardDecrypt,
  200. mobile: widget.mobile,
  201. area: widget.area,
  202. wifi: widget.wifi,
  203. time: widget.time.millisecondsSinceEpoch,
  204. userId: await SpUtil.getUserId());
  205. String newPath =
  206. "${dir.path}/$prefix${widget.time.millisecondsSinceEpoch}.jpg";
  207. logd("保存路径=$newPath");
  208. allCmd.encodeJpgFile(newPath);
  209. await allCmd.executeThread();
  210. logd("执行完毕");
  211. return newPath;
  212. }
  213. void _onSave() async {
  214. logd("保存");
  215. setState(() {
  216. _isSaving = true;
  217. });
  218. var cancelFunc = BotToast.showLoading(clickClose: false, crossPage: false);
  219. try {
  220. //编辑并保存照片
  221. String? path = await _editPhoto();
  222. if (path == null) {
  223. setState(() {
  224. _isSaving = false;
  225. });
  226. cancelFunc();
  227. showToast(text: getS().saveFailed);
  228. return;
  229. }
  230. //通知原生保存图片
  231. await videoChannel.invokeMethod("savePhoto", [widget.path, path]);
  232. _isSaved = true;
  233. cancelFunc();
  234. setState(() {
  235. _isSaving = false;
  236. });
  237. showToast(text: getS().saveSuccess);
  238. if (mounted) {
  239. Navigator.pop(context);
  240. }
  241. } catch (e) {
  242. setState(() {
  243. _isSaving = false;
  244. });
  245. cancelFunc();
  246. showToast(text: getS().saveFailed);
  247. loge("保存图片异常", error: e);
  248. }
  249. }
  250. void _onCancel() {
  251. logd("取消");
  252. Navigator.pop(context);
  253. }
  254. void _deleteCache() {
  255. if (widget.path.isEmpty && _isSaved) {
  256. return;
  257. }
  258. try {
  259. fileChannel.invokeMethod("deleteFile", widget.path);
  260. } catch (e) {
  261. loge("照片预览删除文件异常", error: e);
  262. }
  263. }
  264. }