import 'dart:io'; import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:image/image.dart' as img; import 'package:path/path.dart' as path; import 'package:uuid/uuid.dart'; import '../constants/app_constants.dart'; import '../errors/app_error.dart'; import 'logger.dart'; /// 图片处理工具类 /// 提供图片压缩、格式转换、缩略图生成等功能 class ImageUtils { // 私有构造函数,防止实例化 ImageUtils._(); static const Uuid _uuid = Uuid(); /// 生成缩略图 /// [imagePath] 原图路径 /// [targetPath] 缩略图保存路径 /// [maxSize] 缩略图最大尺寸(长边) /// [quality] 压缩质量(0-100) static Future generateThumbnail({ required String imagePath, required String targetPath, int maxSize = AppConstants.maxThumbnailSize, int quality = AppConstants.thumbnailQuality, }) async { try { Logger.debug('开始生成缩略图: $imagePath'); // 检查原文件是否存在 final originalFile = File(imagePath); if (!await originalFile.exists()) { throw ImageProcessingError( message: '原图文件不存在: $imagePath', code: 'IMAGE_PROCESSING_ERROR', ); } // 获取文件信息 final fileSize = await originalFile.length(); Logger.debug('原图文件大小: ${fileSize / 1024 / 1024}MB'); // 检查文件大小限制 if (fileSize > AppConstants.maxImageSize) { throw ImageProcessingError( message: '图片文件过大,最大支持30MB', code: 'IMAGE_PROCESSING_ERROR', ); } // 根据文件类型选择处理方式 final fileExtension = path.extension(imagePath).toLowerCase(); if (fileExtension == '.gif') { return await _generateGifThumbnail( imagePath: imagePath, targetPath: targetPath, maxSize: maxSize, quality: quality, ); } else { return await _generateStaticImageThumbnail( imagePath: imagePath, targetPath: targetPath, maxSize: maxSize, quality: quality, ); } } catch (error, stackTrace) { Logger.error('生成缩略图失败', error: error, stackTrace: stackTrace); if (error is ImageProcessingError) { rethrow; } throw ImageProcessingError( message: '生成缩略图失败: ${error.toString()}', code: 'IMAGE_PROCESSING_ERROR', stackTrace: stackTrace, ); } } /// 生成静态图片缩略图 static Future _generateStaticImageThumbnail({ required String imagePath, required String targetPath, required int maxSize, required int quality, }) async { try { // 使用 flutter_image_compress 进行压缩 final result = await FlutterImageCompress.compressAndGetFile( imagePath, targetPath, quality: quality, minWidth: maxSize, minHeight: maxSize, format: CompressFormat.webp, keepExif: false, ); if (result == null) { throw ImageProcessingError( message: '图片压缩失败,返回结果为空', code: 'IMAGE_PROCESSING_ERROR', ); } Logger.debug('静态图片缩略图生成成功: ${result.path}'); return result.path; } catch (error, stackTrace) { Logger.error('静态图片压缩失败', error: error, stackTrace: stackTrace); // 如果 flutter_image_compress 失败,使用 image 库作为备选方案 return await _generateThumbnailWithImageLib( imagePath: imagePath, targetPath: targetPath, maxSize: maxSize, quality: quality, ); } } /// 使用 image 库生成缩略图(备选方案) static Future _generateThumbnailWithImageLib({ required String imagePath, required String targetPath, required int maxSize, required int quality, }) async { try { // 读取原图 final originalImage = img.decodeImage(await File(imagePath).readAsBytes()); if (originalImage == null) { throw ImageProcessingError( message: '无法解码图片文件', code: 'IMAGE_PROCESSING_ERROR', ); } // 计算缩放比例 final originalWidth = originalImage.width; final originalHeight = originalImage.height; double scale = 1.0; if (originalWidth > originalHeight) { // 横图,以宽度为基准 if (originalWidth > maxSize) { scale = maxSize / originalWidth; } } else { // 竖图或方图,以高度为基准 if (originalHeight > maxSize) { scale = maxSize / originalHeight; } } final targetWidth = (originalWidth * scale).round(); final targetHeight = (originalHeight * scale).round(); Logger.debug('图片缩放: ${originalWidth}x$originalHeight -> ${targetWidth}x$targetHeight'); // 缩放图片 final resizedImage = img.copyResize( originalImage, width: targetWidth, height: targetHeight, ); // 保存为JPEG格式(简化处理) final jpegData = img.encodeJpg(resizedImage, quality: quality); final thumbnailFile = File(targetPath); await thumbnailFile.writeAsBytes(jpegData); Logger.debug('使用 image 库生成缩略图成功: $targetPath'); return targetPath; } catch (error, stackTrace) { throw ImageProcessingError( message: '图片库处理失败: ${error.toString()}', code: 'IMAGE_PROCESSING_ERROR', stackTrace: stackTrace, ); } } /// 生成GIF缩略图 static Future _generateGifThumbnail({ required String imagePath, required String targetPath, required int maxSize, required int quality, }) async { try { Logger.debug('处理GIF缩略图: $imagePath'); // 读取GIF文件 final gifFile = File(imagePath); final gifBytes = await gifFile.readAsBytes(); // 解码GIF final gifImage = img.decodeGif(gifBytes); if (gifImage == null) { throw ImageProcessingError( message: '无法解码GIF文件', code: 'IMAGE_PROCESSING_ERROR', ); } // 对于大GIF文件,只提取第一帧作为缩略图 if (gifImage.length > 1) { Logger.debug('GIF文件包含 ${gifImage.length} 帧,提取第一帧作为缩略图'); } // 获取第一帧 final firstFrame = gifImage.frames.isNotEmpty ? gifImage.frames.first : null; if (firstFrame == null) { throw ImageProcessingError( message: '无法获取GIF的第一帧', code: 'IMAGE_PROCESSING_ERROR', ); } // 计算缩放比例 final originalWidth = firstFrame.width; final originalHeight = firstFrame.height; double scale = 1.0; if (originalWidth > originalHeight) { if (originalWidth > maxSize) { scale = maxSize / originalWidth; } } else { if (originalHeight > maxSize) { scale = maxSize / originalHeight; } } final targetWidth = (originalWidth * scale).round(); final targetHeight = (originalHeight * scale).round(); // 缩放第一帧 final resizedFrame = img.copyResize( firstFrame, width: targetWidth, height: targetHeight, ); // 保存为JPEG格式 final jpegData = img.encodeJpg(resizedFrame, quality: quality); final thumbnailFile = File(targetPath); await thumbnailFile.writeAsBytes(jpegData); Logger.debug('GIF缩略图生成成功: $targetPath'); return targetPath; } catch (error, stackTrace) { throw ImageProcessingError( message: 'GIF缩略图生成失败: ${error.toString()}', code: 'IMAGE_PROCESSING_ERROR', stackTrace: stackTrace, ); } } /// 生成图片的唯一文件名 /// [extension] 文件扩展名(包含点号) static String generateUniqueFileName(String extension) { final timestamp = DateTime.now().millisecondsSinceEpoch; final uuid = _uuid.v4().substring(0, 8); return '${timestamp}_$uuid$extension'; } /// 根据MIME类型获取文件扩展名 static String getExtensionFromMimeType(String mimeType) { final mimeTypeMap = { 'image/jpeg': '.jpg', 'image/jpg': '.jpg', 'image/png': '.png', 'image/gif': '.gif', 'image/webp': '.webp', 'image/heic': '.heic', 'image/heif': '.heif', }; return mimeTypeMap[mimeType.toLowerCase()] ?? '.jpg'; } /// 获取图片的基本信息 static Future> getImageInfo(String imagePath) async { try { final file = File(imagePath); if (!await file.exists()) { throw ImageProcessingError( message: '图片文件不存在: $imagePath', code: 'IMAGE_PROCESSING_ERROR', ); } final bytes = await file.readAsBytes(); final image = img.decodeImage(bytes); if (image == null) { throw ImageProcessingError( message: '无法解码图片文件', code: 'IMAGE_PROCESSING_ERROR', ); } return { 'width': image.width, 'height': image.height, 'size': bytes.length, 'format': path.extension(imagePath).toLowerCase(), 'mimeType': _getMimeType(imagePath), }; } catch (error, stackTrace) { Logger.error('获取图片信息失败', error: error, stackTrace: stackTrace); if (error is ImageProcessingError) { rethrow; } throw ImageProcessingError( message: '获取图片信息失败: ${error.toString()}', code: 'IMAGE_PROCESSING_ERROR', stackTrace: stackTrace, ); } } /// 根据文件路径获取MIME类型 static String _getMimeType(String filePath) { final extension = path.extension(filePath).toLowerCase(); final mimeTypeMap = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif', '.webp': 'image/webp', '.heic': 'image/heic', '.heif': 'image/heif', }; return mimeTypeMap[extension] ?? 'image/jpeg'; } /// 检查图片文件是否有效 static Future isValidImage(String imagePath) async { try { final file = File(imagePath); if (!await file.exists()) { return false; } final fileSize = await file.length(); if (fileSize == 0 || fileSize > AppConstants.maxImageSize) { return false; } final extension = path.extension(imagePath).toLowerCase(); const validExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.heic', '.heif']; if (!validExtensions.contains(extension)) { return false; } // 尝试解码图片 final bytes = await file.readAsBytes(); final image = img.decodeImage(bytes); return image != null; } catch (error) { Logger.error('图片验证失败', error: error); return false; } } /// 计算图片的宽高比 static double calculateAspectRatio(int width, int height) { if (width <= 0 || height <= 0) { return 1.0; } return width / height; } /// 根据目标宽度计算等比例高度 static int calculateHeightForWidth(int originalWidth, int originalHeight, int targetWidth) { if (originalWidth <= 0) { return originalHeight; } return (originalHeight * targetWidth / originalWidth).round(); } /// 根据目标高度计算等比例宽度 static int calculateWidthForHeight(int originalWidth, int originalHeight, int targetHeight) { if (originalHeight <= 0) { return originalWidth; } return (originalWidth * targetHeight / originalHeight).round(); } } /// 图片处理结果 class ImageProcessingResult { final String originalPath; final String thumbnailPath; final Map metadata; final bool success; final String? errorMessage; ImageProcessingResult({ required this.originalPath, required this.thumbnailPath, required this.metadata, this.success = true, this.errorMessage, }); factory ImageProcessingResult.error(String originalPath, String errorMessage) { return ImageProcessingResult( originalPath: originalPath, thumbnailPath: '', metadata: {}, success: false, errorMessage: errorMessage, ); } } /// 支持的图片格式 class SupportedImageFormats { static const List formats = [ 'jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif', ]; static const List extensions = [ '.jpg', '.jpeg', '.png', '.gif', '.webp', '.heic', '.heif', ]; /// 检查文件扩展名是否支持 static bool isSupported(String filePath) { final extension = path.extension(filePath).toLowerCase(); return extensions.contains(extension); } /// 检查MIME类型是否支持 static bool isMimeTypeSupported(String mimeType) { final supportedMimeTypes = [ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/heic', 'image/heif', ]; return supportedMimeTypes.contains(mimeType.toLowerCase()); } }