blob: c7c70499878896aa7089e6a6c2079eb2461934f1 [file] [log] [blame]
// 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: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);
}
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;
}
// This utility function detects how the current Web engine decodes pixel data.
//
// The HTML renderer uses the BMP format to display pixel data, but it used to
// use a wrong implementation. The bug has been fixed, but the fix breaks apps
// that had to provide incorrect data to work around this issue. This function
// is used in the migration guide to assist libraries that would like to run on
// both pre- and post-patch engines by testing the current behavior on a single
// pixel, making use the fact that the patch fixes the pixel order.
//
// The `format` argument is used for testing. In the actual code it should be
// replaced by `PixelFormat.rgba8888`.
//
// See also:
//
// * Patch: https://github.com/flutter/engine/pull/29448
// * Migration guide: https://docs.flutter.dev/release/breaking-changes/raw-images-on-web-uses-correct-origin-and-colors
Future<bool> rawImageUsesCorrectBehavior(PixelFormat format) async {
final ImageDescriptor descriptor = ImageDescriptor.raw(
await ImmutableBuffer.fromUint8List(Uint8List.fromList(<int>[0xED, 0, 0, 0xFF])),
width: 1, height: 1, pixelFormat: format);
final Image image = (await (await descriptor.instantiateCodec()).getNextFrame()).image;
final Uint8List resultPixels = Uint8List.sublistView(
(await image.toByteData(format: ImageByteFormat.rawStraightRgba))!);
return resultPixels[0] == 0xED;
}
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 DomCanvasElement canvas = createDomCanvasElement()
..width = 2
..height = 2;
final DomCanvasRenderingContext2D ctx = canvas.context2D;
ctx.drawImage((blueBackground as HtmlImage).imgElement, 0, 0);
ctx.drawImage((sourceImage as HtmlImage).imgElement, 0, 0);
final DomImageData 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));
});
test('The behavior detector works correctly', () async {
expect(await rawImageUsesCorrectBehavior(PixelFormat.rgba8888), true);
expect(await rawImageUsesCorrectBehavior(PixelFormat.bgra8888), false);
});
}