Skip to content
Boring Doc
BreakingHigh impactOfficial migration guidesince 2.10.0landed 2.9.0-0.0.preweb

Raw images on Web uses correct origin and colors

Framework · Official source

How raw images are rendered on Web has been corrected and is now consistent with that on other platforms. This breaks legacy apps that had to feed incorrect data to `ui.ImageDescriptor.raw` or `ui.decodeImageFromPixels`, causing the resulting images to be upside-down and incorrectly colored (whose red and blue channels are swapped.)

Original behavior

The "pixel stream" that Flutter uses internally has always been defined as the same format: for each pixel, four 8-bit channels are packed in the order defined by a `format` argument, then grouped in a row, from left to right, then rows from top to bottom.

New behavior

The `pixels` argument of `ui.ImageDescriptor.raw` or `ui.decodeImageFromPixels` now uses the correct pixel order described by `format`, and originates from the top left corner.

Who is affected
Applications and packages relying on the previous behaviour or signature. Code that uses ImageDescriptor.raw.
Practical impact
Behaviour or API differences that can break builds or change runtime behaviour.
What action should I take?
Follow the official migration guide below, update affected call sites, run `dart fix --apply` where a fix is available, then run your tests.
APIs mentioned:ImageDescriptor.raw

Before and after (from the official guide)

dart+18−5
Before
1 import 'dart:typed_data';
2 import 'dart:ui' as ui;
3  
4−// Parse `image` as a displayable image.
5−//
6−// Each byte in `image` is a pixel channel, in the order of blue, green, red,
7−// and alpha, starting from the bottom left corner and going row first.
 
 
 
 
 
 
 
 
 
 
 
 
8 Future<ui.Image> parseMyImage(Uint8List image, int width, int height) async {
 
9 final ui.ImageDescriptor descriptor = ui.ImageDescriptor.raw(
10− await ui.ImmutableBuffer.fromUint8List(image),
11 width: width,
12 height: height,
13 pixelFormat: ui.PixelFormat.rgba8888,
14 );
15 return (await (await descriptor.instantiateCodec()).getNextFrame()).image;
16 }
After
1 import 'dart:typed_data';
2 import 'dart:ui' as ui;
3  
4+Uint8List verticallyFlipImage(Uint8List sourceBytes, int width, int height) {
5+ final Uint32List source = Uint32List.sublistView(ByteData.sublistView(sourceBytes));
6+ final Uint32List result = Uint32List(source.length);
7+ int sourceOffset = 0;
8+ int resultOffset = 0;
9+ for (final int row = height - 1; row >= 0; row -= 1) {
10+ sourceOffset = width * row;
11+ for (final int col = 0; col < width; col += 1) {
12+ result[resultOffset] = source[sourceOffset];
13+ resultOffset += 1;
14+ sourceOffset += 1;
15+ }
16+ }
17+ return Uint8List.sublistView(ByteData.sublistView(sourceBytes))
18+}
19+ 
20 Future<ui.Image> parseMyImage(Uint8List image, int width, int height) async {
21+ final Uint8List correctedImage = verticallyFlipImage(image, width, height);
22 final ui.ImageDescriptor descriptor = ui.ImageDescriptor.raw(
23+ await ui.ImmutableBuffer.fromUint8List(correctedImage),
24 width: width,
25 height: height,
26 pixelFormat: ui.PixelFormat.rgba8888,
27 );
28 return (await (await descriptor.instantiateCodec()).getNextFrame()).image;
29 }
Raw before / after
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;
}
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;
}
Official migration guide (excerpt, © The Flutter Authors, CC BY 3.0)

If the app uses the latest version of Flutter and experiences this situation, the most direct solution is to manually flip the image, and use the alternate pixel format. However, this is unlikely the most optimized solution, since such pixel data are usually constructed from other sources, allowing flipping during the construction process.

Code before migration:

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&#x3C;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;
}

Code after migration:

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 &#x3C; width; col += 1) {
      result[resultOffset] = source[sourceOffset];
      resultOffset += 1;
      sourceOffset += 1;
    }
  }
  return Uint8List.sublistView(ByteData.sublistView(sourceBytes))
}

Future&#x3C;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;
}

A trickier situation is when you're writing a library, and you want this library to work on both the most recent Flutter and a pre-patch one. In that case, you can decide whether the behavior has been changed by letting it decode a single pixel first.

Code after migration:

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

late Future&#x3C;bool> imageRawUsesCorrectBehavior = (() async {
  final ui.ImageDescriptor descriptor = ui.ImageDescriptor.raw(
    await ui.ImmutableBuffer.fromUint8List(Uint8List.fromList(&#x3C;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&#x3C;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;
}
Migration assistantAI-generated · grounded in cited sources
AI explanations are not configured
Set AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.
Official migration guide·Verified classification (95%)

Listed in the official breaking-changes index (docs.flutter.dev/release/breaking-changes).

An official announcement or migration guide documents an intended behaviour or API change. A verified source change (git diff) shows code changed but does not by itself prove a public API is breaking. AI interpretations are labelled and never used to classify a change.

Source commit
b3045439b7c0
Source last updated
Oct 21, 2026
Last synchronized
Oct 21, 2026
Publication status
published
Publication history
  • Oct 21, 2026, 8:31 PM: new → published (ingestion) — Backed by an official breaking-change/migration guide.
AI explanationAI-generated · grounded in cited sources
AI explanations are not configured
Set AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.
Was this useful?