| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- import 'dart:io';
- import 'package:image/image.dart' as img;
- import 'package:sino_med_cloud/core/utils/logger.dart';
- import 'media_exception.dart';
- /// 图片处理封装(压缩 / 裁剪 / 医疗脱敏)
- class ImageProcessor {
- /// 默认压缩宽度(医疗场景推荐)
- static const int defaultMaxWidth = 1080;
-
- /// 默认压缩高度(医疗场景推荐)
- static const int defaultMaxHeight = 1920;
-
- /// 默认压缩质量(85% 平衡质量和体积)
- static const int defaultQuality = 85;
- /// 压缩图片
- ///
- /// 医疗场景:降低上传体积、去EXIF、图片标准化
- ///
- /// - [file] 原始图片文件
- /// - [maxWidth] 最大宽度,默认 1080px
- /// - [maxHeight] 最大高度,默认 1920px
- /// - [quality] 压缩质量(1-100),默认 85
- ///
- /// 返回压缩后的文件(覆盖原文件)
- static Future<File> compress(
- File file, {
- int maxWidth = defaultMaxWidth,
- int? maxHeight,
- int quality = defaultQuality,
- }) async {
- try {
- if (!await file.exists()) {
- throw ImageProcessingException('图片文件不存在: ${file.path}');
- }
- final bytes = await file.readAsBytes();
- if (bytes.isEmpty) {
- throw ImageProcessingException('图片文件为空: ${file.path}');
- }
- final image = img.decodeImage(bytes);
- if (image == null) {
- throw ImageProcessingException('无法解析图片: ${file.path}');
- }
- // 使用默认最大高度(如果未指定)
- final effectiveMaxHeight = maxHeight ?? defaultMaxHeight;
- // 计算是否需要缩放
- final needsResize = image.width > maxWidth || image.height > effectiveMaxHeight;
-
- File? resultFile;
- if (!needsResize) {
- // 如果图片尺寸都在限制内,只进行质量压缩
- final compressed = img.encodeJpg(image, quality: quality);
- resultFile = File(file.path)..writeAsBytesSync(compressed);
- } else {
- // 计算缩放比例,保持宽高比
- final widthRatio = image.width / maxWidth;
- final heightRatio = image.height / effectiveMaxHeight;
- final ratio = widthRatio > heightRatio ? widthRatio : heightRatio;
-
- // 计算新尺寸
- final newWidth = (image.width / ratio).round();
- final newHeight = (image.height / ratio).round();
-
- // 先缩放再压缩
- final resized = img.copyResize(
- image,
- width: newWidth,
- height: newHeight,
- maintainAspect: false, // 我们已经计算好了尺寸
- );
- final compressed = img.encodeJpg(resized, quality: quality);
- resultFile = File(file.path)..writeAsBytesSync(compressed);
- }
- AppLogger.d('图片压缩完成: ${file.path}, 原始大小: ${bytes.length} bytes, 压缩后: ${resultFile.lengthSync()} bytes');
- return resultFile;
- } catch (e) {
- if (e is ImageProcessingException) {
- rethrow;
- }
- AppLogger.e('图片压缩失败: ${file.path}', e);
- throw ImageProcessingException(
- '图片压缩失败: ${e.toString()}',
- originalError: e,
- );
- }
- }
- /// 获取图片信息
- ///
- /// 返回图片的宽度、高度和文件大小
- static Future<Map<String, dynamic>> getImageInfo(File file) async {
- try {
- if (!await file.exists()) {
- throw ImageProcessingException('图片文件不存在: ${file.path}');
- }
- final bytes = await file.readAsBytes();
- final image = img.decodeImage(bytes);
-
- if (image == null) {
- throw ImageProcessingException('无法解析图片: ${file.path}');
- }
- return {
- 'width': image.width,
- 'height': image.height,
- 'size': bytes.length,
- 'format': image.format.toString(),
- };
- } catch (e) {
- if (e is ImageProcessingException) {
- rethrow;
- }
- AppLogger.e('获取图片信息失败: ${file.path}', e);
- throw ImageProcessingException(
- '获取图片信息失败: ${e.toString()}',
- originalError: e,
- );
- }
- }
- /// 医疗脱敏处理(去除 EXIF 信息)
- ///
- /// 移除图片中的 EXIF 元数据,保护患者隐私
- ///
- /// - [file] 原始图片文件
- ///
- /// 返回脱敏后的文件(覆盖原文件)
- static Future<File> removeExif(File file) async {
- try {
- if (!await file.exists()) {
- throw ImageProcessingException('图片文件不存在: ${file.path}');
- }
- final bytes = await file.readAsBytes();
- final image = img.decodeImage(bytes);
-
- if (image == null) {
- throw ImageProcessingException('无法解析图片: ${file.path}');
- }
- // 重新编码图片会移除 EXIF 信息
- final cleaned = img.encodeJpg(image, quality: 100);
- final cleanedFile = File(file.path)..writeAsBytesSync(cleaned);
- AppLogger.d('图片脱敏完成: ${file.path}');
- return cleanedFile;
- } catch (e) {
- if (e is ImageProcessingException) {
- rethrow;
- }
- AppLogger.e('图片脱敏失败: ${file.path}', e);
- throw ImageProcessingException(
- '图片脱敏失败: ${e.toString()}',
- originalError: e,
- );
- }
- }
- }
|