Flutter 图片压缩性能对比(都是release模式下测试)
- image(dart): 图片压缩完成 - 原始大小: 4.4MB, 压缩后大小: 1.5MB, 压缩率: 64.9%, 耗时: 12336ms
- rust(flutter_rust_bridge): 图片压缩完成 - 原始大小: 4.4MB, 压缩后大小: 1.8MB, 压缩率: 58.6%, 耗时: 6454ms
- native(plugin): 图片压缩完成 - 原始大小: 4.4MB, 压缩后大小: 300.2KB, 压缩率: 93.3%, 耗时: 373ms
dart
import 'dart:typed_data';
import 'dart:isolate';
import 'dart:io' as io;
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:image/image.dart' as img;
import 'package:rust_module/rust_module.dart' as rs;
Future<Uint8List> compressImage(Uint8List imageBytes,
{int minWidth = 1920, int minHeight = 1080}) async {
final stopwatch = Stopwatch()..start();
final originalSize = imageBytes.length;
var target = 2 * 1024 * 1024; // 2 MB 的大小
if (imageBytes.length <= target) {
target = (imageBytes.lengthInBytes * 0.75).toInt();
}
final compressedBytes = await compressToTargetSize(
imageBytes,
target,
minWidth: minWidth,
minHeight: minHeight,
);
stopwatch.stop();
final compressedSize = compressedBytes.length;
final compressionRatio = (1 - compressedSize / originalSize) * 100;
final elapsedMs = stopwatch.elapsedMilliseconds;
// 打印压缩统计信息
print(
'图片压缩完成 - 原始大小: ${_formatBytes(originalSize)}, '
'压缩后大小: ${_formatBytes(compressedSize)}, '
'压缩率: ${compressionRatio.toStringAsFixed(1)}%, '
'耗时: ${elapsedMs}ms',
);
return compressedBytes;
}
/// 格式化字节大小为可读格式
String _formatBytes(int bytes) {
if (bytes < 1024) return '${bytes}B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)}KB';
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)}MB';
}
/// 主入口:根据平台选择压缩方式
Future<Uint8List> compressToTargetSize(Uint8List imageBytes, int targetSize,
{int minWidth = 1920, int minHeight = 1080}) async {
if (io.Platform.isAndroid || io.Platform.isIOS || io.Platform.isMacOS) {
// 使用 flutter_image_compress
logger.d('使用原生库插件 flutter_image_compress 进行图片压缩');
return await _compressWithNative(
imageBytes,
targetSize,
minWidth: minWidth,
minHeight: minHeight,
);
} else {
logger.d('使用 rust 进行图片压缩');
return await rs.compressImage(
imageBytes: imageBytes,
targetSize: targetSize,
minWidth: minWidth,
minHeight: minHeight,
);
}
}
/// 使用原生库进行压缩
Future<Uint8List> _compressWithNative(Uint8List imageBytes, int targetSize,
{int quality = 90, required int minWidth, required int minHeight}) async {
Uint8List compressedBytes = imageBytes;
// 使用 flutter_image_compress 逐步压缩图片
while (compressedBytes.lengthInBytes > targetSize && quality > 10) {
compressedBytes = await FlutterImageCompress.compressWithList(
minWidth: minWidth,
minHeight: minHeight,
compressedBytes,
quality: quality,
);
quality -= 10; // 每次降低 10% 质量
}
return compressedBytes;
}
// ------------------------------------------------------------------------------------
/// 使用 Isolate 和 Dart 库进行压缩
Future<Uint8List> compressToTargetSizeWithIsolate(
Uint8List imageBytes, int targetSize,
{required int minWidth, required int minHeight}) async {
final receivePort = ReceivePort();
// 启动 Isolate
await Isolate.spawn(
_isolateCompressionEntryPoint,
CompressionParams(
TransferableTypedData.fromList([imageBytes]), // 转换为 TransferableTypedData
targetSize,
minWidth,
minHeight,
receivePort.sendPort,
),
);
// 等待结果
final TransferableTypedData result =
await receivePort.first as TransferableTypedData;
return result.materialize().asUint8List(); // 返回压缩后的字节数据
}
/// 数据类,用于传递参数
class CompressionParams {
final TransferableTypedData imageData; // 高效传递 Uint8List
final int targetSize; // 目标大小
final int minWidth;
final int minHeight;
final SendPort sendPort; // 用于返回结果
CompressionParams(this.imageData, this.targetSize, this.minWidth,
this.minHeight, this.sendPort);
}
/// Isolate 的入口函数
void _isolateCompressionEntryPoint(CompressionParams params) async {
final Uint8List imageBytes =
params.imageData.materialize().asUint8List(); // 解压数据
final Uint8List compressedBytes = await _compressWithDartLibrary(
imageBytes, params.targetSize,
minWidth: params.minWidth, minHeight: params.minHeight);
// 将结果发送回主线程
params.sendPort.send(TransferableTypedData.fromList([compressedBytes]));
}
/// 使用 Dart 库进行实际压缩
Future<Uint8List> _compressWithDartLibrary(Uint8List imageBytes, int targetSize,
{int quality = 90, required int minWidth, required int minHeight}) async {
// 解码图片
img.Image? image = img.decodeImage(imageBytes);
if (image == null) {
throw Exception('Failed to decode image');
}
int currentWidth = image.width;
int currentHeight = image.height;
// 优先调整分辨率
while ((currentWidth > minWidth || currentHeight > minHeight) &&
imageBytes.lengthInBytes > targetSize) {
currentWidth = (currentWidth * 0.8).toInt(); // 每次降低宽度 20%
currentHeight = (currentHeight * 0.8).toInt(); // 每次降低高度 20%
// 确保分辨率不会低于最小值
if (currentWidth < minWidth) currentWidth = minWidth;
if (currentHeight < minHeight) currentHeight = minHeight;
// 调整分辨率
image = img.copyResize(image!, width: currentWidth, height: currentHeight);
// 再次计算压缩后的大小
imageBytes = Uint8List.fromList(img.encodeJpg(image, quality: quality));
}
// 调整完分辨率后,逐步降低质量
Uint8List compressedBytes =
Uint8List.fromList(img.encodeJpg(image!, quality: quality));
while (compressedBytes.lengthInBytes > targetSize && quality > 10) {
quality -= 10; // 每次降低 10% 质量
compressedBytes =
Uint8List.fromList(img.encodeJpg(image, quality: quality));
}
return compressedBytes;
}
Rust 实现
rust
use image::{DynamicImage, ImageReader};
use std::io::Cursor;
pub fn compress_image(
image_bytes: Vec<u8>,
target_size: u32,
min_width: u32,
min_height: u32,
) -> Vec<u8> {
if image_bytes.is_empty() || target_size == 0 {
panic!("Invalid input: empty image bytes or zero target size");
}
// 如果原图已经小于目标大小,尝试轻微压缩到目标大小的75%
let actual_target = if image_bytes.len() <= target_size as usize {
(image_bytes.len() as f32 * 0.75) as u32
} else {
target_size
};
// 解码图片
let img = decode_image(&image_bytes);
// 压缩到目标大小
compress_to_target_size(img, actual_target, min_width, min_height)
}
fn decode_image(image_bytes: &[u8]) -> DynamicImage {
let reader = ImageReader::new(Cursor::new(image_bytes))
.with_guessed_format()
.expect("Failed to create image reader");
reader.decode().expect("Failed to decode image")
}
fn compress_to_target_size(
mut img: DynamicImage,
target_size: u32,
min_width: u32,
min_height: u32,
) -> Vec<u8> {
let mut current_width = img.width();
let mut current_height = img.height();
let mut quality = 90u8;
// 首先尝试调整分辨率
while current_width > min_width || current_height > min_height {
// 计算当前编码大小
let encoded = encode_jpeg(&img, quality);
if encoded.len() <= target_size as usize {
return encoded;
}
// 降低分辨率(保持长宽比)
current_width = (current_width as f32 * 0.9) as u32;
current_height = (current_height as f32 * 0.9) as u32;
// 确保不低于最小尺寸
if current_width < min_width {
current_width = min_width;
}
if current_height < min_height {
current_height = min_height;
}
// 调整图片尺寸
img = img.resize(current_width, current_height, image::imageops::FilterType::Lanczos3);
// 如果已经达到最小尺寸,退出循环
if current_width == min_width && current_height == min_height {
break;
}
}
// 然后调整质量
loop {
let encoded = encode_jpeg(&img, quality);
if encoded.len() <= target_size as usize || quality <= 10 {
return encoded;
}
quality = quality.saturating_sub(5);
}
}
fn encode_jpeg(img: &DynamicImage, quality: u8) -> Vec<u8> {
let mut buffer = Vec::new();
let mut cursor = Cursor::new(&mut buffer);
// 使用 JPEG 编码器并设置质量
let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut cursor, quality);
img.write_with_encoder(encoder).expect("Failed to encode JPEG");
buffer
}