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