You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter Web头像裁剪:JS迁移图片解码操作遇异常求解决方案

解决Flutter Web端图片解码阻塞UI的问题

场景背景

  • Flutter应用头像设置流程:选择图片文件(仅支持图片)→弹出对话框裁剪圆形区域作为头像
  • 使用ExtendedImage.memory(...)加载头像,Web端原裁剪逻辑中decodeImage操作会阻塞UI,已将图片编码逻辑迁移至JavaScript端并运行正常,现需解决解码操作的UI阻塞问题

现有正常运行的JS编码实现

Dart外部函数定义

@JS('encodeImageToJpegFromRaw')
external JSPromise<JSString> encodeImageToJpegFromRaw(
  JSUint8Array rawPixels,
  int width,
  int height,
);

JavaScript实现代码

function encodeImageToJpegFromRaw(rawPixels, width, height) {
  return new Promise((resolve, reject) => {
    try {
      const canvas = document.createElement("canvas");
      const ctx = canvas.getContext("2d");
      canvas.width = width;
      canvas.height = height;

      const imageData = new ImageData(
        new Uint8ClampedArray(rawPixels),
        width,
        height
      );
      ctx.putImageData(imageData, 0, 0);

      const jpegData = canvas.toDataURL("image/jpeg", 0.9);
      resolve(jpegData);
    } catch (e) {
      reject(e);
    }
  });
}

尝试的JS解码方案(存在异常)

尝试将解码操作迁移至JS端,但返回全蓝/全绿等异常图像,代码如下:

Dart调用逻辑

try {
  List<int> rgba = await promiseToFuture(
    extractRGBAFromImageData(imageData.toJS),
  );

  int totalPixels =
      (imageData.lengthInBytes / 4)
          .toInt(); 

  print("totalPixels: $totalPixels");
  print("lengthInBytes: ${imageData.lengthInBytes}");

  ByteData byteData = ByteData.sublistView(Uint8List.fromList(rgba));

  int w = state.image!.width;
  int h = state.image!.height;

  print("w: $w, h: $h");

  src = Image.fromBytes(
    width: state.image!.width,
    height: state.image!.height,
    bytes: imageData.buffer, // 这里存在逻辑错误
  );
} catch (e) {
  print("Errore nella decodifica immagine con JS: $e");
  rethrow;
}

JavaScript解码实现

@JS('extractRGBAFromImageData')
external JSPromise extractRGBAFromImageData(JSUint8Array imageData);

function extractRGBAFromImageData(imageData) {
  return new Promise((resolve, reject) => {
    try {
      const canvas = document.createElement("canvas");
      const ctx = canvas.getContext("2d");

      const img = new Image();
      const blob = new Blob([imageData], { type: "image/png" });
      const url = URL.createObjectURL(blob);

      img.onload = function () {
        canvas.width = img.width;
        canvas.height = img.height;
        ctx.drawImage(img, 0, 0);

        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        const rgbaArray = imageData.data;
        resolve(rgbaArray);
      };

      img.onerror = function (e) {
        reject(e);
      };

      img.src = url;
    } catch (e) {
      reject(e);
    }
  });
}

可行解决思路

1. 修正现有解码逻辑的错误点

  • 核心问题:Dart代码中获取JS返回的RGBA数组后,仍使用原始imageData.buffer创建Image对象,并未使用JS解码后的RGBA数据,导致图像异常
  • 修正步骤:
    1. 将JS返回的RGBA数组转换为Dart的Uint8List
    2. 使用该Uint8List的buffer创建Image对象,同时指定格式为RGBA:
      Uint8List rgbaBytes = Uint8List.fromList(rgba);
      src = Image.fromBytes(
        width: state.image!.width,
        height: state.image!.height,
        bytes: rgbaBytes.buffer,
        format: Format.rgba, // 明确指定格式
      );
      
    3. 注意:JS返回的imageData.data是RGBA格式的Uint8ClampedArray,与Dart的Format.rgba兼容

2. 更优方案:将解码+裁剪合并到JS端完成

既然解码和裁剪都是耗时操作,可直接在JS端完成整个流程,减少Dart与JS的交互开销,步骤如下:

Dart端定义外部函数

@JS('cropImageInJS')
external JSPromise<JSString> cropImageInJS(
  JSUint8Array imageData,
  double cropLeft,
  double cropTop,
  double cropWidth,
  double cropHeight,
);

JavaScript实现解码+裁剪+编码

function cropImageInJS(imageData, cropLeft, cropTop, cropWidth, cropHeight) {
  return new Promise((resolve, reject) => {
    try {
      const img = new Image();
      const blob = new Blob([imageData], { type: "image/*" });
      const url = URL.createObjectURL(blob);

      img.onload = function () {
        // 创建裁剪用的canvas
        const cropCanvas = document.createElement("canvas");
        const cropCtx = cropCanvas.getContext("2d");
        cropCanvas.width = cropWidth;
        cropCanvas.height = cropHeight;

        // 绘制裁剪区域
        cropCtx.drawImage(
          img,
          cropLeft, cropTop, cropWidth, cropHeight,
          0, 0, cropWidth, cropHeight
        );

        // 直接转成JPG base64返回
        const jpegData = cropCanvas.toDataURL("image/jpeg", 0.9);
        URL.revokeObjectURL(url); // 释放资源
        resolve(jpegData);
      };

      img.onerror = function (e) {
        URL.revokeObjectURL(url);
        reject(e);
      };

      img.src = url;
    } catch (e) {
      reject(e);
    }
  });
}

Dart端调用

Future<Uint8List> cropImageDataWithJS(
  ImageEditorController imageEditorController,
) async {
  Rect cropRect = imageEditorController.getCropRect()!;
  final ExtendedImageEditorState state = imageEditorController.state!;
  Uint8List imageData = state.rawImageData;

  final jpegBase64 = await promiseToFuture(
    cropImageInJS(
      imageData.toJS,
      cropRect.left,
      cropRect.top,
      cropRect.width,
      cropRect.height,
    ),
  );

  return Uint8List.fromList(base64Decode(jpegBase64.split(",").last));
}

该方案直接在JS端完成解码、裁剪、编码,避免Dart端的耗时操作,彻底解决UI阻塞问题,同时减少数据交互次数。

内容的提问来源于stack exchange,提问作者loreeemartiii

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 06:52:33