| // 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(<int>[ | |
| for (final int pixel in rawPixels) | |
| ...<int>[ | |
| (pixel >> 24) & 0xff, // r | |
| (pixel >> 16) & 0xff, // g | |
| (pixel >> 8) & 0xff, // b | |
| (pixel >> 0) & 0xff, // a | |
| ] | |
| ]); | |
| } | |
| 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); | |
| }); | |
| } |