Flutter 高性能剪裁图片

61 阅读2分钟

Flutter 高性能剪裁图片(使用原生插件+rust)

Android ios macos 使用原生插件,其他使用 Rust Dart 层

import 'dart:io';
import 'dart:ui';

import 'package:extended_image/extended_image.dart';
import 'package:flutter/foundation.dart';
import 'package:http_client_helper/http_client_helper.dart';
import 'package:image_editor/image_editor.dart';
import 'package:rust_module/rust_module.dart' as rs;

import '../logger.dart';

enum ImageType { gif, jpg }

class EditImageInfo {
  EditImageInfo(
      this.data,
      this.imageType,
      );
  final Uint8List? data;
  final ImageType imageType;
}

Future<EditImageInfo> cropImageDataWithNativeLibrary(
    ImageEditorController imageEditorController) async {
  logger.d('native library start cropping');

  final EditActionDetails action = imageEditorController.editActionDetails!;

  final Uint8List img = imageEditorController.state!.rawImageData;

  final ImageEditorOption option = ImageEditorOption();

  if (action.hasRotateDegrees) {
    final int rotateDegrees = action.rotateDegrees.toInt();
    option.addOption(RotateOption(rotateDegrees));
  }
  if (action.flipY) {
    option.addOption(const FlipOption(horizontal: true, vertical: false));
  }

  if (action.needCrop) {
    Rect cropRect = imageEditorController.getCropRect()!;
    if (imageEditorController.state!.widget.extendedImageState.imageProvider
    is ExtendedResizeImage) {
      final ImmutableBuffer buffer = await ImmutableBuffer.fromUint8List(img);
      final ImageDescriptor descriptor = await ImageDescriptor.encoded(buffer);

      final double widthRatio =
          descriptor.width / imageEditorController.state!.image!.width;
      final double heightRatio =
          descriptor.height / imageEditorController.state!.image!.height;
      cropRect = Rect.fromLTRB(
        cropRect.left * widthRatio,
        cropRect.top * heightRatio,
        cropRect.right * widthRatio,
        cropRect.bottom * heightRatio,
      );
    }
    option.addOption(ClipOption.fromRect(cropRect));
  }

  final DateTime start = DateTime.now();
  final Uint8List? result = await ImageEditor.editImage(
    image: img,
    imageEditorOption: option,
  );

  logger.d('${DateTime.now().difference(start)} :total time');
  return EditImageInfo(result, ImageType.jpg);
}

/// it may be failed, due to Cross-domain
Future<Uint8List> _loadNetwork(ExtendedNetworkImageProvider key) async {
  try {
    final Response? response = await HttpClientHelper.get(Uri.parse(key.url),
        headers: key.headers,
        timeLimit: key.timeLimit,
        timeRetry: key.timeRetry,
        retries: key.retries,
        cancelToken: key.cancelToken);
    return response!.bodyBytes;
  } on OperationCanceledError catch (_) {
    logger.d('User cancel request ${key.url}.');
    return Future<Uint8List>.error(
        StateError('User cancel request ${key.url}.'));
  } catch (e) {
    return Future<Uint8List>.error(StateError('failed load ${key.url}. \n $e'));
  }
}

Future<EditImageInfo> cropImageDataWithRust(
    ImageEditorController imageEditorController) async {
  logger.d('rust library start cropping');

  ///crop rect base on raw image
  Rect cropRect = imageEditorController.getCropRect()!;
  final ExtendedImageEditorState state = imageEditorController.state!;

  logger.d('getCropRect : $cropRect');

  final Uint8List data = kIsWeb &&
      imageEditorController.state!.widget.extendedImageState.imageWidget
          .image is ExtendedNetworkImageProvider
      ? await _loadNetwork(imageEditorController.state!.widget
      .extendedImageState.imageWidget.image as ExtendedNetworkImageProvider)
      : state.rawImageData;

  if (data == state.rawImageData &&
      state.widget.extendedImageState.imageProvider is ExtendedResizeImage) {
    final ImmutableBuffer buffer =
    await ImmutableBuffer.fromUint8List(state.rawImageData);
    final ImageDescriptor descriptor = await ImageDescriptor.encoded(buffer);
    final double widthRatio = descriptor.width / state.image!.width;
    final double heightRatio = descriptor.height / state.image!.height;
    cropRect = Rect.fromLTRB(
      cropRect.left * widthRatio,
      cropRect.top * heightRatio,
      cropRect.right * widthRatio,
      cropRect.bottom * heightRatio,
    );
  }

  final EditActionDetails editAction = state.editAction!;

  final DateTime time1 = DateTime.now();

  // 使用rust模块进行图片处理
  final Uint8List? result = await rs.editImage(
    imageBytes: data,
    cropX: editAction.needCrop ? cropRect.left.toInt() : 0,
    cropY: editAction.needCrop ? cropRect.top.toInt() : 0,
    cropWidth: editAction.needCrop ? cropRect.width.toInt() : 0,
    cropHeight: editAction.needCrop ? cropRect.height.toInt() : 0,
    rotateDegrees: editAction.hasRotateDegrees ? editAction.rotateDegrees.toInt() : 0,
    flipHorizontal: editAction.flipY,
    needCrop: editAction.needCrop,
  );

  final DateTime time5 = DateTime.now();
  logger.d('${time5.difference(time1)} : total time');
  return EditImageInfo(
    result,
    ImageType.jpg,
  );
}

Future<EditImageInfo> cropImageData(
    {required ImageEditorController controller}) async {
  if (Platform.isAndroid || Platform.isIOS || Platform.isMacOS) {
    return cropImageDataWithNativeLibrary(controller);
  }
  return cropImageDataWithRust(controller);
}

Rust 层

pub fn edit_image(
    image_bytes: Vec<u8>,
    crop_x: u32,
    crop_y: u32,
    crop_width: u32,
    crop_height: u32,
    rotate_degrees: i32,
    flip_horizontal: bool,
    need_crop: bool,
) -> Vec<u8> {
    if image_bytes.is_empty() {
        panic!("Invalid input: empty image bytes");
    }

    // 解码图片
    let mut img = photon_rs::native::open_image_from_bytes(&image_bytes)
        .expect("Failed to decode image");

    // 处理旋转
    if rotate_degrees != 0 {
        let angle = (rotate_degrees % 360) as f32;
        img = photon_rs::transform::rotate(&img, angle);
    }

    // 处理水平翻转
    if flip_horizontal {
        photon_rs::transform::fliph(&mut img);
    }

    // 处理裁剪
    if need_crop && crop_width > 0 && crop_height > 0 {
        // 确保裁剪区域在图片范围内
        let img_width = img.get_width();
        let img_height = img.get_height();
        
        let actual_x = crop_x.min(img_width);
        let actual_y = crop_y.min(img_height);
        let actual_width = crop_width.min(img_width - actual_x);
        let actual_height = crop_height.min(img_height - actual_y);
        
        if actual_width > 0 && actual_height > 0 {
            img = photon_rs::transform::crop(&img, actual_x, actual_y, actual_width, actual_height);
        }
    }

    // 编码为字节数组
    img.get_bytes_jpeg(100).to_vec()
}