Flutter 图片压缩性能对比

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
}
相关推荐
肥肥呀呀呀5 小时前
flutter 高斯模糊闪烁问题
flutter
程序员老刘1 天前
Dart MCP翻车了!3.9.0版本无法运行,这个坑你踩过吗?
flutter·ai编程·客户端
ideal树叶1 天前
flutter 中 的 关键字
flutter
世界不及妳微笑1 天前
Flutter 记录应用授权登录踩坑之旅
flutter
鹏多多2 天前
flutter-使用confetti制作炫酷纸屑爆炸粒子动画
android·前端·flutter
耳東陳12512 天前
【重磅发布】flutter_chen_updater-版本升级更新
flutter
HH思️️无邪2 天前
Flutter 开发技巧 AI 快速构建 json_annotation model 的提示词
flutter·json
笔沫拾光2 天前
hooks_riverpod框架解析
flutter·hooks·hooks_riverpod
problc2 天前
Flutter桌面应用实战:Windows系统代理切换工具开发
windows·flutter