Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 55 additions & 44 deletions lib/web_ui/lib/src/ui/painting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -500,71 +500,83 @@ Future<void> _decodeImageFromListAsync(Uint8List list, ImageDecoderCallback call
final FrameInfo frameInfo = await codec.getNextFrame();
callback(frameInfo.image);
}

// Encodes the input pixels into a BMP file that supports transparency.
//
// The `pixels` should be the scanlined raw pixels, 4 bytes per pixel, from left
// to right, then from top to down. The order of the 4 bytes of pixels is
// decided by `format`.
Future<Codec> _createBmp(
Uint8List pixels,
int width,
int height,
int rowBytes,
PixelFormat format,
) {
late bool swapRedBlue;
switch (format) {
case PixelFormat.bgra8888:
swapRedBlue = true;
break;
case PixelFormat.rgba8888:
swapRedBlue = false;
break;
}

// See https://en.wikipedia.org/wiki/BMP_file_format for format examples.
final int bufferSize = 0x36 + (width * height * 4);
// The header is in the 108-byte BITMAPV4HEADER format, or as called by
// Chromium, WindowsV4. Do not use the 56-byte or 52-byte Adobe formats, since
// they're not supported.
const int dibSize = 0x6C /* 108: BITMAPV4HEADER */;
const int headerSize = dibSize + 0x0E;
final int bufferSize = headerSize + (width * height * 4);
final ByteData bmpData = ByteData(bufferSize);
// 'BM' header
bmpData.setUint8(0x00, 0x42);
bmpData.setUint8(0x01, 0x4D);
bmpData.setUint16(0x00, 0x424D, Endian.big);
// Size of data
bmpData.setUint32(0x02, bufferSize, Endian.little);
// Offset where pixel array begins
bmpData.setUint32(0x0A, 0x36, Endian.little);
bmpData.setUint32(0x0A, headerSize, Endian.little);
// Bytes in DIB header
bmpData.setUint32(0x0E, 0x28, Endian.little);
// width
bmpData.setUint32(0x0E, dibSize, Endian.little);
// Width
bmpData.setUint32(0x12, width, Endian.little);
// height
// Height
bmpData.setUint32(0x16, height, Endian.little);
// Color panes
// Color panes (always 1)
bmpData.setUint16(0x1A, 0x01, Endian.little);
// 32 bpp
bmpData.setUint16(0x1C, 0x20, Endian.little);
// no compression
bmpData.setUint32(0x1E, 0x00, Endian.little);
// raw bitmap data size
// bpp: 32
bmpData.setUint16(0x1C, 32, Endian.little);
// Compression method is BITFIELDS to enable bit fields
bmpData.setUint32(0x1E, 3, Endian.little);
// Raw bitmap data size
bmpData.setUint32(0x22, width * height, Endian.little);
// print DPI width
// Print DPI width
bmpData.setUint32(0x26, width, Endian.little);
// print DPI height
// Print DPI height
bmpData.setUint32(0x2A, height, Endian.little);
// colors in the palette
// Colors in the palette
bmpData.setUint32(0x2E, 0x00, Endian.little);
// important colors
// Important colors
bmpData.setUint32(0x32, 0x00, Endian.little);


int pixelDestinationIndex = 0;
late bool swapRedBlue;
switch (format) {
case PixelFormat.bgra8888:
swapRedBlue = true;
break;
case PixelFormat.rgba8888:
swapRedBlue = false;
break;
}
for (int pixelSourceIndex = 0; pixelSourceIndex < pixels.length; pixelSourceIndex += 4) {
final int r = swapRedBlue ? pixels[pixelSourceIndex + 2] : pixels[pixelSourceIndex];
final int b = swapRedBlue ? pixels[pixelSourceIndex] : pixels[pixelSourceIndex + 2];
final int g = pixels[pixelSourceIndex + 1];
final int a = pixels[pixelSourceIndex + 3];

// Set the pixel past the header data.
bmpData.setUint8(pixelDestinationIndex + 0x36, r);
bmpData.setUint8(pixelDestinationIndex + 0x37, g);
bmpData.setUint8(pixelDestinationIndex + 0x38, b);
bmpData.setUint8(pixelDestinationIndex + 0x39, a);
pixelDestinationIndex += 4;
if (rowBytes != width && pixelSourceIndex % width == 0) {
pixelSourceIndex += rowBytes - width;
// Bitmask R
bmpData.setUint32(0x36, swapRedBlue ? 0x00FF0000 : 0x000000FF, Endian.little);
// Bitmask G
bmpData.setUint32(0x3A, 0x0000FF00, Endian.little);
// Bitmask B
bmpData.setUint32(0x3E, swapRedBlue ? 0x000000FF : 0x00FF0000, Endian.little);
// Bitmask A
bmpData.setUint32(0x42, 0xFF000000, Endian.little);

int destinationByte = headerSize;
final Uint32List combinedPixels = Uint32List.sublistView(pixels);
// BMP is scanlined from bottom to top. Rearrange here.
for (int rowCount = height - 1; rowCount >= 0; rowCount -= 1) {
int sourcePixel = rowCount * rowBytes;
for (int colCount = 0; colCount < width; colCount += 1) {
bmpData.setUint32(destinationByte, combinedPixels[sourcePixel], Endian.little);
destinationByte += 4;
sourcePixel += 1;
}
}

Expand Down Expand Up @@ -803,4 +815,3 @@ class FragmentProgram {
required Float32List floatUniforms,
}) => throw UnsupportedError('FragmentProgram is not supported for the CanvasKit or HTML renderers.');
}

148 changes: 148 additions & 0 deletions lib/web_ui/test/html/image_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:html';
import 'dart:typed_data';

import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide TextStyle;

void main() {
internalBootstrapBrowserTest(() => testMain);
}

typedef _ListPredicate<T> = bool Function(List<T>);
_ListPredicate<T> deepEqualList<T>(List<T> a) {
return (List<T> b) {
if (a.length != b.length)
return false;
for (int i = 0; i < a.length; i += 1) {
if (a[i] != b[i])
return false;
}
return true;
};
}

Matcher listEqual(List<int> source, {int tolerance = 0}) {
return predicate(
(List<int> target) {
if (source.length != target.length)
return false;
for (int i = 0; i < source.length; i += 1) {
if ((source[i] - target[i]).abs() > tolerance)
return false;
}
return true;
},
source.toString(),
);
}

// Converts `rawPixels` into a list of bytes that represent raw pixels in rgba8888.
//
// Each element of `rawPixels` represents a bytes in order 0xRRGGBBAA, with
// pixel order Left to right, then top to bottom.
Uint8List _pixelsToBytes(List<int> rawPixels) {
return Uint8List.fromList((() sync* {
for (final int pixel in rawPixels) {
yield (pixel >> 24) & 0xff; // r
yield (pixel >> 16) & 0xff; // g
yield (pixel >> 8) & 0xff; // b
yield (pixel >> 0) & 0xff; // a
}
})().toList());
}

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

Future<void> testMain() async {
test('Correctly encodes an opaque image', () async {
// A 2x2 testing image without transparency.
final Image sourceImage = await _encodeToHtmlThenDecode(
_pixelsToBytes(
<int>[0xFF0102FF, 0x04FE05FF, 0x0708FDFF, 0x0A0B0C00],
), 2, 2,
);
final Uint8List actualPixels = Uint8List.sublistView(
(await sourceImage.toByteData(format: ImageByteFormat.rawStraightRgba))!);
// The `benchmarkPixels` is identical to `sourceImage` except for the fully
// transparent last pixel, whose channels are turned 0.
final Uint8List benchmarkPixels = _pixelsToBytes(
<int>[0xFF0102FF, 0x04FE05FF, 0x0708FDFF, 0x00000000],
);
expect(actualPixels, listEqual(benchmarkPixels));
});

test('Correctly encodes an opaque image in bgra8888', () async {
// A 2x2 testing image without transparency.
final Image sourceImage = await _encodeToHtmlThenDecode(
_pixelsToBytes(
<int>[0xFF0102FF, 0x04FE05FF, 0x0708FDFF, 0x0A0B0C00],
), 2, 2, pixelFormat: PixelFormat.bgra8888,
);
final Uint8List actualPixels = Uint8List.sublistView(
(await sourceImage.toByteData(format: ImageByteFormat.rawStraightRgba))!);
// The `benchmarkPixels` is the same as `sourceImage` except that the R and
// G channels are swapped and the fully transparent last pixel is turned 0.
final Uint8List benchmarkPixels = _pixelsToBytes(
<int>[0x0201FFFF, 0x05FE04FF, 0xFD0807FF, 0x00000000],
);
expect(actualPixels, listEqual(benchmarkPixels));
});

test('Correctly encodes a transparent image', () async {
// A 2x2 testing image with transparency.
final Image sourceImage = await _encodeToHtmlThenDecode(
_pixelsToBytes(
<int>[0xFF800006, 0xFF800080, 0xFF8000C0, 0xFF8000FF],
), 2, 2,
);
final Image blueBackground = await _encodeToHtmlThenDecode(
_pixelsToBytes(
<int>[0x0000FFFF, 0x0000FFFF, 0x0000FFFF, 0x0000FFFF],
), 2, 2,
);
// The standard way of testing the raw bytes of `sourceImage` is to draw
// the image onto a canvas and fetch its data (see HtmlImage.toByteData).
// But here, we draw an opaque background first before drawing the image,
// and test if the blended result is expected.
//
// This is because, if we only draw the `sourceImage`, the resulting pixels
// will be slightly off from the raw pixels. The reason is unknown, but
// very likely because the canvas.getImageData introduces rounding errors
// if any pixels are left semi-transparent, which might be caused by
// converting to and from pre-multiplied values. See
// https://github.com/flutter/flutter/issues/92958 .
final CanvasElement canvas = CanvasElement()
..width = 2
..height = 2;
final CanvasRenderingContext2D ctx = canvas.context2D;
ctx.drawImage((blueBackground as HtmlImage).imgElement, 0, 0);
ctx.drawImage((sourceImage as HtmlImage).imgElement, 0, 0);

final ImageData imageData = ctx.getImageData(0, 0, 2, 2);
final List<int> actualPixels = imageData.data;

final Uint8List benchmarkPixels = _pixelsToBytes(
<int>[0x0603F9FF, 0x80407FFF, 0xC0603FFF, 0xFF8000FF],
);
expect(actualPixels, listEqual(benchmarkPixels, tolerance: 1));
});
}