| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- 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;
-
- /// 默认压缩质量(85% 平衡质量和体积)
- static const int defaultQuality = 85;
- /// 压缩图片
- ///
- /// 医疗场景:降低上传体积、去EXIF、图片标准化
- ///
- /// - [file] 原始图片文件
- /// - [maxWidth] 最大宽度,默认 1080px
- /// - [quality] 压缩质量(1-100),默认 85
- ///
- /// 返回压缩后的文件(覆盖原文件)
- static Future<File> compress(
- File file, {
- int maxWidth = defaultMaxWidth,
- 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}');
- }
- // 如果图片宽度小于等于最大宽度,只进行质量压缩
- File? resultFile;
- if (image.width <= maxWidth) {
- final compressed = img.encodeJpg(image, quality: quality);
- resultFile = File(file.path)..writeAsBytesSync(compressed);
- } else {
- // 先缩放再压缩
- final resized = img.copyResize(
- image,
- width: maxWidth,
- maintainAspect: true,
- );
- 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,
- );
- }
- }
- }
|