我正在尝试
resize
和 compress
以及 Isolate
中的图像,因为如果没有这些,用户界面就会变得很卡顿。我试过这样:
import 'dart:isolate';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:image/image.dart' as img;
// Helper function to perform image processing in an isolate
Future<Uint8List> _processImageInIsolate(
Uint8List imageData, int quality, int width) async {
try {
// Decode the image
img.Image? image = img.decodeImage(imageData);
if (image == null) {
throw Exception("Failed to decode image");
}
// Crop to square
final squareImage = Uint8ListHelper.cropToSquare(image);
// Resize the image to the desired size
final resizedImage = Uint8ListHelper.resizeToSquare(squareImage, width);
// Compress the image
final compressed = await FlutterImageCompress.compressWithList(
Uint8List.fromList(img.encodePng(resizedImage)),
quality: quality,
);
return Uint8List.fromList(compressed);
} catch (e) {
debugPrint(e.toString());
return imageData; // Return original if processing fails
}
}
extension Uint8ListHelper on Uint8List {
static img.Image cropToSquare(img.Image image) {
int xOffset = 0;
int yOffset = 0;
int squareSize;
if (image.width > image.height) {
// If the image is wider than it is tall, crop the sides
squareSize = image.height;
xOffset = (image.width - image.height) ~/ 2;
} else {
// If the image is taller than it is wide, crop the top and bottom
squareSize = image.width;
yOffset = (image.height - image.width) ~/ 2;
}
// Crop the image to a square
return img.copyCrop(
image,
x: xOffset,
y: yOffset,
width: squareSize,
height: squareSize,
);
}
static img.Image resizeToSquare(img.Image image, int size) {
// Resize the image to the target square size
return img.copyResize(
image,
width: size,
height: size, // Both dimensions are the same to make it a square
interpolation: img.Interpolation.linear,
);
}
Future<Uint8List> process({
int quality = 20,
int width = 1000,
}) async {
try {
// Use `compute` to run the image processing in an isolate
final compressed = await Isolate.run(
() {
return _processImageInIsolate(this, quality, width);
},
);
// Ensure that the compressed image isn't larger than the original
if (compressed.length > length) {
return this; // Return the original if the processed image is larger
}
return compressed;
} catch (e) {
FirebaseCrashlytics.instance.recordError(
'EasyCatch: Uint8List.process failed',
null,
reason: e.toString(),
information: [e],
);
return this;
}
}
}
但是这在
FlutterImageCompress.compressWithList
和 UnimplementedError
处失败了。
我在这里缺少什么?我是否以错误的方式使用
Isolate
?
之前,我简单地这样称呼它,并且运行得很好:
import 'dart:typed_data';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:image/image.dart' as img;
import 'package:wishlists/extensions/extensions.dart';
extension Uint8ListHelper on Uint8List {
static img.Image cropToSquare(img.Image image) {
int xOffset = 0;
int yOffset = 0;
int squareSize;
if (image.width > image.height) {
// If the image is wider than it is tall, crop the sides
squareSize = image.height;
xOffset = (image.width - image.height) ~/ 2;
} else {
// If the image is taller than it is wide, crop the top and bottom
squareSize = image.width;
yOffset = (image.height - image.width) ~/ 2;
}
// Crop the image to a square
return img.copyCrop(
image,
x: xOffset,
y: yOffset,
width: squareSize,
height: squareSize,
);
}
static img.Image resizeToSquare(img.Image image, int size) {
// Resize the image to the target square size
return img.copyResize(
image,
width: size,
height: size, // Both dimensions are the same to make it a square
interpolation: img.Interpolation.linear,
);
}
/// Resizes the image to 1000x1000 pixels and compresses it with 40% quality.
/// Returns a compressed Uint8List.
/// If the process fails, it returns the given Uint8List.
Future<Uint8List> process({
int quality = 20,
int width = 1000,
}) async {
try {
// return this;
final resized = resize(
width: width,
);
final compressed = await (resized ?? this).compress(
quality: quality,
);
debugPrint(
'Original size: ${length.toFormattedFileSize()}',
);
debugPrint(
'Compressed size: ${compressed?.length.toFormattedFileSize()}',
);
if ((compressed?.length ?? 0) > length) {
// Sometimes images are already small so resizing and compressing them,
// will actually make them bigger.
return this;
}
return compressed ?? resized ?? this;
} catch (e) {
return this;
}
}
// Compress Uint8List and get another Uint8List.
Future<Uint8List?> compress({
int quality = 20,
}) async {
try {
final result = await FlutterImageCompress.compressWithList(
this,
quality: quality,
);
return result;
} on Exception catch (e) {
FirebaseCrashlytics.instance.recordError(
'EasyCatch: Uint8List.compress failed',
null,
reason: e.toString(),
information: [e],
);
return null;
}
}
Uint8List? resize({
int width = 1000,
}) {
try {
img.Image? image = img.decodeImage(this);
if (image == null) {
throw Exception("Failed to decode image");
}
final squareImage = Uint8ListHelper.cropToSquare(
image,
);
final resizedImage = Uint8ListHelper.resizeToSquare(
squareImage,
width,
);
final byteList = Uint8List.fromList(
img.encodePng(resizedImage),
);
return byteList;
} on Exception catch (e) {
FirebaseCrashlytics.instance.recordError(
'EasyCatch: Uint8List.resize failed',
null,
reason: e.toString(),
information: [e],
);
return null;
}
}
}
flutter_image_compress
利用不同平台的平台实现,并且隔离具有已知的局限性。也许甚至不是“限制”,而是一些具体的:)描述here。
TLDR;在执行实际工作之前必须初始化BackgroundIsolateBinaryMessenger。
这里最大的问题是你必须在这里使用更底层的技术来分离,包括
spawn
和ports
。你不能只是 Isolate.run
,因为在这种情况下 RootIsolateToken
将为空,并且 BackgroundIsolateBinaryMessenger
不会被初始化。
在我看来,这里最可靠的方法是使用
RootIsolateToken
作为参数进行初始化。以下是您可以做到这一点的方法:
// First – initialize global port. It is kinda shortcut, there are more robust options but for now it'll work.
SendPort? _isolateSendPort;
// This type will be used as parameters for our isolate later.
typedef IsolateInitData = (RootIsolateToken rootToken, SendPort sendPort);
// This is actual method which will run in Isolate later. It's kinda yours
//_processImageInIsolate but with extra steps. This method will accept
//IsolateInitData as a parameters to perform initialization (to avoid
//UnimplementedError) but actual data will be gathered through port.
static void _isolateMain(IsolateInitData params) {
final ReceivePort receivePort = ReceivePort();
params.$2.send(receivePort.sendPort);
BackgroundIsolateBinaryMessenger.ensureInitialized(params.$1);
// Here is actual computation starts when someone sent us the image.
receivePort.listen((message) async {
// Parameters are not typed here so it's downside, yes.
if (message is Map<String, dynamic>) {
final Uint8List imageData = message['imageData'] as Uint8List;
final int quality = message['quality'] as int;
final int width = message['width'] as int;
final SendPort responsePort = message['port'] as SendPort;
// This code is just copied from your implementation.
try {
// Decode the image
img.Image? image = img.decodeImage(imageData);
if (image == null) {
throw Exception("Failed to decode image");
}
// Crop to square
final squareImage = Uint8ListHelper.cropToSquare(image);
// Resize the image to the desired size
final resizedImage =
Uint8ListHelper.resizeToSquare(squareImage, width);
// Compress the image
final compressed = await FlutterImageCompress.compressWithList(
Uint8List.fromList(img.encodePng(resizedImage)),
quality: quality,
);
// instead of returning data, send it through port.
responsePort.send(compressed);
print('Image sent!');
} catch (e) {
print('Error in isolate: $e');
responsePort.send(imageData); // Send back original data on error
}
}
});
}
}
准备工作结束了,我们来看看实际的使用情况。抱歉,整个代码被省略了,也许我稍后会在 GitHub 上发布。但关键点是:
// This happens inside your State object somewhere
@override
void initState() {
super.initState();
unawaited(_initializeIsolate());
}
Future<void> _initializeIsolate() async {
final ReceivePort receivePort = ReceivePort();
// here we collect RootIsolateToken.instance and port. Port later will be passed to global _isolateSendPort
final initData = (RootIsolateToken.instance!, receivePort.sendPort);
// isolate is created here. Pay attention that it's long-lived object,
// so maybe save isolate in your State and dispose in dispose method with Isolate.kill()
await Isolate.spawn(_isolateMain, initData);
// Now we have port which can receive image data from us.
_isolateSendPort = await receivePort.first as SendPort;
}
// Here is modified process method
Future<Uint8List> process({
int quality = 20,
int width = 1000,
}) async {
// Create a ReceivePort for this request
final responsePort = ReceivePort();
// Send the image data to the isolate
_isolateSendPort!.send(<String, dynamic>{
'imageData': this,
'quality': quality,
'width': width,
'port': responsePort.sendPort,
});
// Wait for the response from the isolate. It will be your compressed data.
return responsePort.first as Future<Uint8List>;
}
基本上就是这样。至少我设法完全摆脱了
UnimplementedError
,包括release
构建。但我只在Android上测试过。
这是隔离的一个复杂的方面,所以也许这个答案本身不足以让您立即使用它。没关系:)但我希望它能让你的生活轻松一点。祝你好运!