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()
}