Saltar al contenido principal

Las imágenes RAW en la Web usan el origen y los colores correctos

Las imágenes RAW decodificadas directamente llamando a las funciones del motor Web ahora utilizan el formato de píxeles correcto y comienzan desde la esquina superior izquierda.

Resumen

#

Se ha corregido cómo se renderizan las imágenes RAW en Web y ahora es consistente con el comportamiento en otras plataformas. Esto rompe las aplicaciones heredadas (legacy) que tenían que proporcionar datos incorrectos a ui.ImageDescriptor.raw o ui.decodeImageFromPixels, lo que provocaba que las imágenes resultantes estuvieran invertidas (de cabeza) y con colores incorrectos (donde los canales rojo y azul se intercambiaban).

Contexto

#

El "flujo de píxeles" (pixel stream) que Flutter utiliza internamente siempre se ha definido con el mismo formato: para cada píxel, se empaquetan cuatro canales de 8 bits en el orden definido por un argumento de format, luego se agrupan en una fila, de izquierda a derecha, y luego las filas de arriba a abajo.

Sin embargo, Flutter para Web, o más específicamente, el renderizador HTML, solía implementarlo de manera incorrecta debido a una comprensión errónea de la especificación del formato BMP. Como resultado, si la aplicación o biblioteca utiliza ui.ImageDescriptor.raw o ui.decodeImageFromPixels, tenía que proporcionar píxeles de abajo hacia arriba e intercambiar sus canales rojo y azul (por ejemplo, con el formato ui.PixelFormat.rgba8888, los primeros 4 bytes de los datos se consideraban los canales azul, verde, rojo y alfa del primer píxel en su lugar).

Este error ha sido corregido por engine#29593, pero las aplicaciones y bibliotecas deben corregir la forma en que se generan sus datos.

Descripción del cambio

#

El argumento pixels de ui.ImageDescriptor.raw o ui.decodeImageFromPixels ahora utiliza el orden de píxeles correcto descrito por format, y se origina desde la esquina superior izquierda.

Imágenes renderizadas llamando directamente a estas dos funciones: el código heredado que invoca estas funciones directamente podría encontrar sus imágenes al revés y coloreadas incorrectamente.

Guía de migración

#

Si la aplicación utiliza la última versión de Flutter y experimenta esta situación, la solución más directa es voltear la imagen manualmente y usar el formato de píxeles alternativo. Sin embargo, es poco probable que esta sea la solución más optimizada, ya que estos datos de píxeles generalmente se construyen a partir de otras fuentes, lo que permite voltearlos durante el proceso de construcción.

Código antes de la migración:

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;
}

Código después de la migración:

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;
}

Una situación más complicada es cuando estás escribiendo una biblioteca, y deseas que esta biblioteca funcione tanto en el Flutter más reciente como en uno anterior al parche. En ese caso, puedes determinar si el comportamiento ha cambiado haciendo que decodifique un solo píxel primero.

Código después de la migración:

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;
}

Timeline

#

Llegó en la versión: 2.9.0-0.0.pre
En lanzamiento estable: 2.10

Referencias

#

Documentación de la API:

Issues relevantes:

PRs relevantes: