概述

#

Web 上原始图像的渲染方式已得到纠正,现在与其它平台保持一致。这会影响那些不得不向 ui.ImageDescriptor.rawui.decodeImageFromPixels 提供不正确数据的旧版应用,导致生成的图像上下颠倒且颜色不正确(其红色和蓝色通道互换)。

背景

#

Flutter 内部使用的“像素流”始终定义为相同的格式:对于每个像素,四个 8 位通道按照 format 参数定义的顺序打包,然后从左到右按行分组,接着从上到下按行排列。

然而,Flutter for Web,更具体地说是 HTML 渲染器,由于对 BMP 格式规范的错误理解,曾以错误的方式实现。因此,如果应用或库使用了 ui.ImageDescriptor.rawui.decodeImageFromPixels,它必须从下到上馈送像素并交换它们的红色和蓝色通道(例如,对于 ui.PixelFormat.rgba8888 格式,数据的前 4 字节被错误地认为是第一个像素的蓝色、绿色、红色和 alpha 通道)。

这个错误已通过 engine#29593 修复,但应用和库必须纠正其数据的生成方式。

变更说明

#

ui.ImageDescriptor.rawui.decodeImageFromPixelspixels 参数现在使用 format 描述的正确像素顺序,并从左上角开始。

直接调用这两个函数渲染的图像:直接调用这些函数的旧版代码可能会发现其图像上下颠倒且颜色不正确。

迁移指南

#

如果应用使用最新版本的 Flutter 并遇到这种情况,最直接的解决方案是手动翻转图像,并使用备用像素格式。然而,这不太可能是最优化方案,因为此类像素数据通常是从其他来源构建的,可以在构建过程中进行翻转。

迁移前的代码

Dart
import 'dart:typed_data';
import 'dart:ui' as ui;

// Parse `image` as a displayable image.
//
// Each byte in `image` is a pixel channel, in the order of blue, green, red,
// and alpha, starting from the bottom left corner and going row first.
Future<ui.Image> parseMyImage(Uint8List image, int width, int height) async {
  final ui.ImageDescriptor descriptor = ui.ImageDescriptor.raw(
    await ui.ImmutableBuffer.fromUint8List(image),
    width: width,
    height: height,
    pixelFormat: ui.PixelFormat.rgba8888,
  );
  return (await (await descriptor.instantiateCodec()).getNextFrame()).image;
}

迁移后的代码

Dart
import 'dart:typed_data';
import 'dart:ui' as ui;

Uint8List verticallyFlipImage(Uint8List sourceBytes, int width, int height) {
  final Uint32List source = Uint32List.sublistView(ByteData.sublistView(sourceBytes));
  final Uint32List result = Uint32List(source.length);
  int sourceOffset = 0;
  int resultOffset = 0;
  for (final int row = height - 1; row >= 0; row -= 1) {
    sourceOffset = width * row;
    for (final int col = 0; col < width; col += 1) {
      result[resultOffset] = source[sourceOffset];
      resultOffset += 1;
      sourceOffset += 1;
    }
  }
  return Uint8List.sublistView(ByteData.sublistView(sourceBytes))
}

Future<ui.Image> parseMyImage(Uint8List image, int width, int height) async {
  final Uint8List correctedImage = verticallyFlipImage(image, width, height);
  final ui.ImageDescriptor descriptor = ui.ImageDescriptor.raw(
    await ui.ImmutableBuffer.fromUint8List(correctedImage),
    width: width,
    height: height,
    pixelFormat: ui.PixelFormat.rgba8888,
  );
  return (await (await descriptor.instantiateCodec()).getNextFrame()).image;
}

一个更棘手的情况是,当你正在编写一个库,并且希望这个库既能在最新版本的 Flutter 上工作,也能在补丁发布前的版本上工作。在这种情况下,你可以先让它解码单个像素,来判断行为是否已改变。

迁移后的代码

Dart
Uint8List verticallyFlipImage(Uint8List sourceBytes, int width, int height) {
  // Same as the example above.
}

late Future<bool> imageRawUsesCorrectBehavior = (() async {
  final ui.ImageDescriptor descriptor = ui.ImageDescriptor.raw(
    await ui.ImmutableBuffer.fromUint8List(Uint8List.fromList(<int>[0xED, 0, 0, 0xFF])),
    width: 1, height: 1, pixelFormat: ui.PixelFormat.rgba8888);
  final ui.Image image = (await (await descriptor.instantiateCodec()).getNextFrame()).image;
  final Uint8List resultPixels = Uint8List.sublistView(
    (await image.toByteData(format: ui.ImageByteFormat.rawStraightRgba))!);
  return resultPixels[0] == 0xED;
})();

Future<ui.Image> parseMyImage(Uint8List image, int width, int height) async {
  final Uint8List correctedImage = (await imageRawUsesCorrectBehavior) ?
    verticallyFlipImage(image, width, height) : image;
  final ui.ImageDescriptor descriptor = ui.ImageDescriptor.raw(
    await ui.ImmutableBuffer.fromUint8List(correctedImage), // Use the corrected image
    width: width,
    height: height,
    pixelFormat: ui.PixelFormat.bgra8888, // Use the alternate format
  );
  return (await (await descriptor.instantiateCodec()).getNextFrame()).image;
}

时间线

#

已在版本中发布: 2.9.0-0.0.pre
稳定版本: 2.10

参考资料

#

API 文档

相关问题

相关 PR