image_processor.dart 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import 'dart:io';
  2. import 'package:image/image.dart' as img;
  3. import 'package:sino_med_cloud/core/utils/logger.dart';
  4. import 'media_exception.dart';
  5. /// 图片处理封装(压缩 / 裁剪 / 医疗脱敏)
  6. class ImageProcessor {
  7. /// 默认压缩宽度(医疗场景推荐)
  8. static const int defaultMaxWidth = 1080;
  9. /// 默认压缩质量(85% 平衡质量和体积)
  10. static const int defaultQuality = 85;
  11. /// 压缩图片
  12. ///
  13. /// 医疗场景:降低上传体积、去EXIF、图片标准化
  14. ///
  15. /// - [file] 原始图片文件
  16. /// - [maxWidth] 最大宽度,默认 1080px
  17. /// - [quality] 压缩质量(1-100),默认 85
  18. ///
  19. /// 返回压缩后的文件(覆盖原文件)
  20. static Future<File> compress(
  21. File file, {
  22. int maxWidth = defaultMaxWidth,
  23. int quality = defaultQuality,
  24. }) async {
  25. try {
  26. if (!await file.exists()) {
  27. throw ImageProcessingException('图片文件不存在: ${file.path}');
  28. }
  29. final bytes = await file.readAsBytes();
  30. if (bytes.isEmpty) {
  31. throw ImageProcessingException('图片文件为空: ${file.path}');
  32. }
  33. final image = img.decodeImage(bytes);
  34. if (image == null) {
  35. throw ImageProcessingException('无法解析图片: ${file.path}');
  36. }
  37. // 如果图片宽度小于等于最大宽度,只进行质量压缩
  38. File? resultFile;
  39. if (image.width <= maxWidth) {
  40. final compressed = img.encodeJpg(image, quality: quality);
  41. resultFile = File(file.path)..writeAsBytesSync(compressed);
  42. } else {
  43. // 先缩放再压缩
  44. final resized = img.copyResize(
  45. image,
  46. width: maxWidth,
  47. maintainAspect: true,
  48. );
  49. final compressed = img.encodeJpg(resized, quality: quality);
  50. resultFile = File(file.path)..writeAsBytesSync(compressed);
  51. }
  52. AppLogger.d('图片压缩完成: ${file.path}, 原始大小: ${bytes.length} bytes, 压缩后: ${resultFile.lengthSync()} bytes');
  53. return resultFile;
  54. } catch (e) {
  55. if (e is ImageProcessingException) {
  56. rethrow;
  57. }
  58. AppLogger.e('图片压缩失败: ${file.path}', e);
  59. throw ImageProcessingException(
  60. '图片压缩失败: ${e.toString()}',
  61. originalError: e,
  62. );
  63. }
  64. }
  65. /// 获取图片信息
  66. ///
  67. /// 返回图片的宽度、高度和文件大小
  68. static Future<Map<String, dynamic>> getImageInfo(File file) async {
  69. try {
  70. if (!await file.exists()) {
  71. throw ImageProcessingException('图片文件不存在: ${file.path}');
  72. }
  73. final bytes = await file.readAsBytes();
  74. final image = img.decodeImage(bytes);
  75. if (image == null) {
  76. throw ImageProcessingException('无法解析图片: ${file.path}');
  77. }
  78. return {
  79. 'width': image.width,
  80. 'height': image.height,
  81. 'size': bytes.length,
  82. 'format': image.format.toString(),
  83. };
  84. } catch (e) {
  85. if (e is ImageProcessingException) {
  86. rethrow;
  87. }
  88. AppLogger.e('获取图片信息失败: ${file.path}', e);
  89. throw ImageProcessingException(
  90. '获取图片信息失败: ${e.toString()}',
  91. originalError: e,
  92. );
  93. }
  94. }
  95. /// 医疗脱敏处理(去除 EXIF 信息)
  96. ///
  97. /// 移除图片中的 EXIF 元数据,保护患者隐私
  98. ///
  99. /// - [file] 原始图片文件
  100. ///
  101. /// 返回脱敏后的文件(覆盖原文件)
  102. static Future<File> removeExif(File file) async {
  103. try {
  104. if (!await file.exists()) {
  105. throw ImageProcessingException('图片文件不存在: ${file.path}');
  106. }
  107. final bytes = await file.readAsBytes();
  108. final image = img.decodeImage(bytes);
  109. if (image == null) {
  110. throw ImageProcessingException('无法解析图片: ${file.path}');
  111. }
  112. // 重新编码图片会移除 EXIF 信息
  113. final cleaned = img.encodeJpg(image, quality: 100);
  114. final cleanedFile = File(file.path)..writeAsBytesSync(cleaned);
  115. AppLogger.d('图片脱敏完成: ${file.path}');
  116. return cleanedFile;
  117. } catch (e) {
  118. if (e is ImageProcessingException) {
  119. rethrow;
  120. }
  121. AppLogger.e('图片脱敏失败: ${file.path}', e);
  122. throw ImageProcessingException(
  123. '图片脱敏失败: ${e.toString()}',
  124. originalError: e,
  125. );
  126. }
  127. }
  128. }