snap_wish/lib/core/utils/image_utils.dart
2025-09-12 18:17:35 +08:00

474 lines
13 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String> 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<String> _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<String> _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<String> _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<Map<String, dynamic>> 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<bool> 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<String, dynamic> 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<String> formats = [
'jpg',
'jpeg',
'png',
'gif',
'webp',
'heic',
'heif',
];
static const List<String> 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());
}
}