Flutter 在 Isolate 中调整大小和压缩图像会抛出 UnimplementedError

问题描述 投票:0回答:1

我正在尝试

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 dart image-compression dart-isolates
1个回答
0
投票

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上测试过。

这是隔离的一个复杂的方面,所以也许这个答案本身不足以让您立即使用它。没关系:)但我希望它能让你的生活轻松一点。祝你好运!

© www.soinside.com 2019 - 2024. All rights reserved.