From 1c8b584f0fb95f6689b1bc33deeb1c489a3e8b7b Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Mon, 1 Nov 2021 10:56:04 -0700 Subject: [PATCH 01/12] Impl --- lib/web_ui/lib/src/ui/painting.dart | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index ccd4cc55903f0..80065a4a6aa03 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -508,7 +508,8 @@ Future _createBmp( PixelFormat format, ) { // See https://en.wikipedia.org/wiki/BMP_file_format for format examples. - final int bufferSize = 0x36 + (width * height * 4); + const int dibSize = 0x36; + final int bufferSize = dibSize + (width * height * 4); final ByteData bmpData = ByteData(bufferSize); // 'BM' header bmpData.setUint8(0x00, 0x42); @@ -521,8 +522,8 @@ Future _createBmp( bmpData.setUint32(0x0E, 0x28, Endian.little); // width bmpData.setUint32(0x12, width, Endian.little); - // height - bmpData.setUint32(0x16, height, Endian.little); + // height: Negative height is interpreted as "top-down" bitmap. + bmpData.setUint32(0x16, -height, Endian.little); // Color panes bmpData.setUint16(0x1A, 0x01, Endian.little); // 32 bpp @@ -540,7 +541,6 @@ Future _createBmp( // important colors bmpData.setUint32(0x32, 0x00, Endian.little); - int pixelDestinationIndex = 0; late bool swapRedBlue; switch (format) { @@ -555,13 +555,12 @@ Future _createBmp( 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]; + // Alpha channel is not supported by BMP. Discarded. // 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); + bmpData.setUint8(pixelDestinationIndex + dibSize + 0, b); + bmpData.setUint8(pixelDestinationIndex + dibSize + 1, g); + bmpData.setUint8(pixelDestinationIndex + dibSize + 2, r); pixelDestinationIndex += 4; if (rowBytes != width && pixelSourceIndex % width == 0) { pixelSourceIndex += rowBytes - width; @@ -801,4 +800,3 @@ class FragmentProgram { required Float32List floatUniforms, }) => throw UnsupportedError('FragmentProgram is not supported for the CanvasKit or HTML renderers.'); } - From a2efaed072ced594fb9521e096a9f8ecedefea7e Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Tue, 2 Nov 2021 05:41:06 -0700 Subject: [PATCH 02/12] Add test --- lib/web_ui/test/html/image_test.dart | 47 ++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 lib/web_ui/test/html/image_test.dart diff --git a/lib/web_ui/test/html/image_test.dart b/lib/web_ui/test/html/image_test.dart new file mode 100644 index 0000000000000..0c2842c7c4770 --- /dev/null +++ b/lib/web_ui/test/html/image_test.dart @@ -0,0 +1,47 @@ +// 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; + +import './testimage.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +typedef _ListPredicate = bool Function(List); +_ListPredicate deepEqualList(List a) { + return (List 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; + }; +} + +Future testMain() async { + test('Draws image with dstATop color filter', () async { + const List testImage = [0xFA000000, 0x00FA0030, 0x0000FA60, 0x000000FF]; + final Uint8List sourcePixels = Uint8List.sublistView(Uint32List.fromList(testImage)); + final ImageDescriptor encoded = ImageDescriptor.raw( + await ImmutableBuffer.fromUint8List(sourcePixels), + width: 2, + height: 2, + pixelFormat: PixelFormat.rgba8888, + ); + final Image decoded = (await (await encoded.instantiateCodec()).getNextFrame()).image; + final Uint8List actualPixels = Uint8List.sublistView( + (await decoded.toByteData(format: ImageByteFormat.rawStraightRgba))!); + expect(actualPixels, hasLength(sourcePixels.length)); + expect(actualPixels, predicate(deepEqualList(sourcePixels), sourcePixels.toString())); + }); +} From 095d214ce66470c5fb3259b7b96def409e15dc3a Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Tue, 2 Nov 2021 21:38:35 -0700 Subject: [PATCH 03/12] Fully fix --- lib/web_ui/lib/src/ui/painting.dart | 91 ++++++++++++++++++---------- lib/web_ui/test/html/image_test.dart | 68 +++++++++++++++++---- 2 files changed, 116 insertions(+), 43 deletions(-) diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index 80065a4a6aa03..8046d1aa00546 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -500,6 +500,17 @@ Future _decodeImageFromListAsync(Uint8List list, ImageDecoderCallback call final FrameInfo frameInfo = await codec.getNextFrame(); callback(frameInfo.image); } + +// Encodes the input pixels into a BMP file. +// +// This BMP file supports transparency. However, due to a known bug, if the +// image has any partially-opaque pixels (one with alpha between 1 and 254), the +// resulting colors (including alpha) might deviate by a small amount. See +// https://github.com/flutter/flutter/issues/92958. +// +// 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 _createBmp( Uint8List pixels, int width, @@ -508,38 +519,51 @@ Future _createBmp( PixelFormat format, ) { // See https://en.wikipedia.org/wiki/BMP_file_format for format examples. - const int dibSize = 0x36; - final int bufferSize = dibSize + (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: Negative height is interpreted as "top-down" bitmap. - bmpData.setUint32(0x16, -height, Endian.little); - // Color panes + // Height + bmpData.setUint32(0x16, height, Endian.little); + // 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); + // Bitmask R + bmpData.setUint32(0x36, 0x00FF0000, Endian.little); + // Bitmask G + bmpData.setUint32(0x3A, 0x0000FF00, Endian.little); + // Bitmask B + bmpData.setUint32(0x3E, 0x000000FF, Endian.little); + // Bitmask A + bmpData.setUint32(0x42, 0xFF000000, Endian.little); + // Color space + bmpData.setUint32(0x46, 0x206E6957, Endian.little); int pixelDestinationIndex = 0; late bool swapRedBlue; @@ -551,19 +575,22 @@ Future _createBmp( 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]; - // Alpha channel is not supported by BMP. Discarded. - - // Set the pixel past the header data. - bmpData.setUint8(pixelDestinationIndex + dibSize + 0, b); - bmpData.setUint8(pixelDestinationIndex + dibSize + 1, g); - bmpData.setUint8(pixelDestinationIndex + dibSize + 2, r); - pixelDestinationIndex += 4; - if (rowBytes != width && pixelSourceIndex % width == 0) { - pixelSourceIndex += rowBytes - width; + // BMP is scanlined from bottom to top. Rearrange here. + for (int rowCount = height - 1; rowCount >= 0; rowCount -= 1) { + int pixelSourceByte = rowCount * rowBytes * 4; + for (int colCount = 0; colCount < width; colCount += 1) { + final int r = swapRedBlue ? pixels[pixelSourceByte + 2] : pixels[pixelSourceByte]; + final int b = swapRedBlue ? pixels[pixelSourceByte] : pixels[pixelSourceByte + 2]; + final int g = pixels[pixelSourceByte + 1]; + final int a = pixels[pixelSourceByte + 3]; + + // Set the pixel past the header data. + bmpData.setUint8(pixelDestinationIndex + headerSize + 0, b); + bmpData.setUint8(pixelDestinationIndex + headerSize + 1, g); + bmpData.setUint8(pixelDestinationIndex + headerSize + 2, r); + bmpData.setUint8(pixelDestinationIndex + headerSize + 3, a); + pixelDestinationIndex += 4; + pixelSourceByte += 4; } } diff --git a/lib/web_ui/test/html/image_test.dart b/lib/web_ui/test/html/image_test.dart index 0c2842c7c4770..de62be897be25 100644 --- a/lib/web_ui/test/html/image_test.dart +++ b/lib/web_ui/test/html/image_test.dart @@ -9,8 +9,6 @@ import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' hide TextStyle; -import './testimage.dart'; - void main() { internalBootstrapBrowserTest(() => testMain); } @@ -28,20 +26,68 @@ _ListPredicate deepEqualList(List a) { }; } +Matcher listEqual(List source) { + return predicate( + (List target) { + if (source.length != target.length) + return false; + for (int i = 0; i < source.length; i += 1) { + if (source[i] != target[i]) + return false; + } + return true; + }, + source.toString(), + ); +} + Future testMain() async { - test('Draws image with dstATop color filter', () async { - const List testImage = [0xFA000000, 0x00FA0030, 0x0000FA60, 0x000000FF]; - final Uint8List sourcePixels = Uint8List.sublistView(Uint32List.fromList(testImage)); - final ImageDescriptor encoded = ImageDescriptor.raw( - await ImmutableBuffer.fromUint8List(sourcePixels), + test('Correctly encodes an opaque image', () async { + // A 2x2 testing image without transparency. + // Pixel order: Left to right, then top to bottom. + // Byte order: 0xAABBGGRR (because uint8 is placed in little endian.) + final Uint8List sourceImage = Uint8List.sublistView(Uint32List.fromList( + [0xFF0201FF, 0xFF05FE04, 0xFFFD0807, 0x000C0B0A], + )); + final ImageDescriptor descriptor = ImageDescriptor.raw( + await ImmutableBuffer.fromUint8List(sourceImage), + width: 2, + height: 2, + pixelFormat: PixelFormat.rgba8888, + ); + final Image encoded = (await (await descriptor.instantiateCodec()).getNextFrame()).image; + final Uint8List actualPixels = Uint8List.sublistView( + (await encoded.toByteData(format: ImageByteFormat.rawStraightRgba))!); + final Uint8List targetImage = Uint8List.sublistView(Uint32List.fromList( + [0xFF0201FF, 0xFF05FE04, 0xFFFD0807, 0x00000000], + )); + expect(actualPixels, listEqual(targetImage)); + }); + + test('Correctly encodes a transparent image', () async { + // A 2x2 testing image with transparency. + // Pixel order: Left to right, then top to bottom. + // Byte order: 0xAABBGGRR (because uint8 is placed in little endian.) + final Uint8List sourceImage = Uint8List.sublistView(Uint32List.fromList( + [0x030201FF, 0x0605FE04, 0x09FD0807, 0xFC0C0B0A], + )); + final ImageDescriptor descriptor = ImageDescriptor.raw( + await ImmutableBuffer.fromUint8List(sourceImage), width: 2, height: 2, pixelFormat: PixelFormat.rgba8888, ); - final Image decoded = (await (await encoded.instantiateCodec()).getNextFrame()).image; + final Image encoded = (await (await descriptor.instantiateCodec()).getNextFrame()).image; final Uint8List actualPixels = Uint8List.sublistView( - (await decoded.toByteData(format: ImageByteFormat.rawStraightRgba))!); - expect(actualPixels, hasLength(sourcePixels.length)); - expect(actualPixels, predicate(deepEqualList(sourcePixels), sourcePixels.toString())); + (await encoded.toByteData(format: ImageByteFormat.rawStraightRgba))!); + // TODO(dkwingsmt): Known bug: The `targetImage` is slight differnt from + // `sourceImage` due to unknown reasons (possibly because how + // canvas.drawImage blends transparent pixels). In an ideal world we should + // use `sourceImage` here. + // https://github.com/flutter/flutter/issues/92958 + final Uint8List targetImage = Uint8List.sublistView(Uint32List.fromList( + [0x030000FF, 0x0600FF00, 0x09FF0000, 0xFC0C0B0A], + )); + expect(actualPixels, listEqual(targetImage)); }); } From fed897f574f1a792aa953c841961f5c15a68244c Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Tue, 2 Nov 2021 21:55:34 -0700 Subject: [PATCH 04/12] Fix analysis --- lib/web_ui/lib/src/ui/painting.dart | 2 -- lib/web_ui/test/html/image_test.dart | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index 8046d1aa00546..962da3f3a8680 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -562,8 +562,6 @@ Future _createBmp( bmpData.setUint32(0x3E, 0x000000FF, Endian.little); // Bitmask A bmpData.setUint32(0x42, 0xFF000000, Endian.little); - // Color space - bmpData.setUint32(0x46, 0x206E6957, Endian.little); int pixelDestinationIndex = 0; late bool swapRedBlue; diff --git a/lib/web_ui/test/html/image_test.dart b/lib/web_ui/test/html/image_test.dart index de62be897be25..9fcdfd562e5fd 100644 --- a/lib/web_ui/test/html/image_test.dart +++ b/lib/web_ui/test/html/image_test.dart @@ -6,7 +6,6 @@ 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() { @@ -58,6 +57,8 @@ Future testMain() async { final Image encoded = (await (await descriptor.instantiateCodec()).getNextFrame()).image; final Uint8List actualPixels = Uint8List.sublistView( (await encoded.toByteData(format: ImageByteFormat.rawStraightRgba))!); + // The `targetImage` is identical to `sourceImage` except for the fully + // transparent last pixel, whose channels are turned 0. final Uint8List targetImage = Uint8List.sublistView(Uint32List.fromList( [0xFF0201FF, 0xFF05FE04, 0xFFFD0807, 0x00000000], )); @@ -80,7 +81,7 @@ Future testMain() async { final Image encoded = (await (await descriptor.instantiateCodec()).getNextFrame()).image; final Uint8List actualPixels = Uint8List.sublistView( (await encoded.toByteData(format: ImageByteFormat.rawStraightRgba))!); - // TODO(dkwingsmt): Known bug: The `targetImage` is slight differnt from + // TODO(dkwingsmt): Known bug: The `targetImage` is slightly differnt from // `sourceImage` due to unknown reasons (possibly because how // canvas.drawImage blends transparent pixels). In an ideal world we should // use `sourceImage` here. From 523570626762abc8051a748b2857031677014249 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Tue, 2 Nov 2021 23:05:07 -0700 Subject: [PATCH 05/12] Use a differnt test method --- lib/web_ui/test/html/image_test.dart | 110 +++++++++++++++++---------- 1 file changed, 71 insertions(+), 39 deletions(-) diff --git a/lib/web_ui/test/html/image_test.dart b/lib/web_ui/test/html/image_test.dart index 9fcdfd562e5fd..3b8922b70cb04 100644 --- a/lib/web_ui/test/html/image_test.dart +++ b/lib/web_ui/test/html/image_test.dart @@ -2,10 +2,12 @@ // 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() { @@ -40,55 +42,85 @@ Matcher listEqual(List source) { ); } +// 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 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 _encodeToHtmlThenDecode(Uint8List rawBytes, int width, int height) async { + final ImageDescriptor descriptor = ImageDescriptor.raw( + await ImmutableBuffer.fromUint8List(rawBytes), + width: 2, + height: 2, + pixelFormat: PixelFormat.rgba8888, + ); + return (await (await descriptor.instantiateCodec()).getNextFrame()).image; +} + Future testMain() async { test('Correctly encodes an opaque image', () async { // A 2x2 testing image without transparency. - // Pixel order: Left to right, then top to bottom. - // Byte order: 0xAABBGGRR (because uint8 is placed in little endian.) - final Uint8List sourceImage = Uint8List.sublistView(Uint32List.fromList( - [0xFF0201FF, 0xFF05FE04, 0xFFFD0807, 0x000C0B0A], - )); - final ImageDescriptor descriptor = ImageDescriptor.raw( - await ImmutableBuffer.fromUint8List(sourceImage), - width: 2, - height: 2, - pixelFormat: PixelFormat.rgba8888, + final Image sourceImage = await _encodeToHtmlThenDecode( + _pixelsToBytes( + [0xFF0102FF, 0x04FE05FF, 0x0708FDFF, 0x0A0B0C00], + ), 2, 2, ); - final Image encoded = (await (await descriptor.instantiateCodec()).getNextFrame()).image; final Uint8List actualPixels = Uint8List.sublistView( - (await encoded.toByteData(format: ImageByteFormat.rawStraightRgba))!); - // The `targetImage` is identical to `sourceImage` except for the fully + (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 targetImage = Uint8List.sublistView(Uint32List.fromList( - [0xFF0201FF, 0xFF05FE04, 0xFFFD0807, 0x00000000], - )); - expect(actualPixels, listEqual(targetImage)); + final Uint8List benchmarkPixels = _pixelsToBytes( + [0xFF0102FF, 0x04FE05FF, 0x0708FDFF, 0x00000000], + ); + expect(actualPixels, listEqual(benchmarkPixels)); }); test('Correctly encodes a transparent image', () async { // A 2x2 testing image with transparency. - // Pixel order: Left to right, then top to bottom. - // Byte order: 0xAABBGGRR (because uint8 is placed in little endian.) - final Uint8List sourceImage = Uint8List.sublistView(Uint32List.fromList( - [0x030201FF, 0x0605FE04, 0x09FD0807, 0xFC0C0B0A], - )); - final ImageDescriptor descriptor = ImageDescriptor.raw( - await ImmutableBuffer.fromUint8List(sourceImage), - width: 2, - height: 2, - pixelFormat: PixelFormat.rgba8888, + final Image sourceImage = await _encodeToHtmlThenDecode( + _pixelsToBytes( + [0xFF800006, 0xFF800080, 0xFF8000C0, 0xFF8000FF], + ), 2, 2, ); - final Image encoded = (await (await descriptor.instantiateCodec()).getNextFrame()).image; - final Uint8List actualPixels = Uint8List.sublistView( - (await encoded.toByteData(format: ImageByteFormat.rawStraightRgba))!); - // TODO(dkwingsmt): Known bug: The `targetImage` is slightly differnt from - // `sourceImage` due to unknown reasons (possibly because how - // canvas.drawImage blends transparent pixels). In an ideal world we should - // use `sourceImage` here. - // https://github.com/flutter/flutter/issues/92958 - final Uint8List targetImage = Uint8List.sublistView(Uint32List.fromList( - [0x030000FF, 0x0600FF00, 0x09FF0000, 0xFC0C0B0A], - )); - expect(actualPixels, listEqual(targetImage)); + final Image blueBackground = await _encodeToHtmlThenDecode( + _pixelsToBytes( + [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 actualPixels = imageData.data; + + final Uint8List benchmarkPixels = _pixelsToBytes( + [0x0603F9FF, 0x80407FFF, 0xC0603FFF, 0xFF8000FF], + ); + expect(actualPixels, listEqual(benchmarkPixels)); }); } From f66bc036aae2c88bb3cc3cce791389bc90d0ed99 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Tue, 2 Nov 2021 23:06:15 -0700 Subject: [PATCH 06/12] Better doc --- lib/web_ui/lib/src/ui/painting.dart | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index 962da3f3a8680..d14b48b5db1df 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -501,12 +501,7 @@ Future _decodeImageFromListAsync(Uint8List list, ImageDecoderCallback call callback(frameInfo.image); } -// Encodes the input pixels into a BMP file. -// -// This BMP file supports transparency. However, due to a known bug, if the -// image has any partially-opaque pixels (one with alpha between 1 and 254), the -// resulting colors (including alpha) might deviate by a small amount. See -// https://github.com/flutter/flutter/issues/92958. +// 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 From e1a20bfafa625a7197f752e973bd63db372b5be2 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Wed, 3 Nov 2021 00:12:04 -0700 Subject: [PATCH 07/12] add tolerance --- lib/web_ui/test/html/image_test.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/web_ui/test/html/image_test.dart b/lib/web_ui/test/html/image_test.dart index 3b8922b70cb04..a7dc1bb13ab3c 100644 --- a/lib/web_ui/test/html/image_test.dart +++ b/lib/web_ui/test/html/image_test.dart @@ -27,13 +27,13 @@ _ListPredicate deepEqualList(List a) { }; } -Matcher listEqual(List source) { +Matcher listEqual(List source, {int tolerance = 0}) { return predicate( - (List target) { + (List target) { if (source.length != target.length) return false; for (int i = 0; i < source.length; i += 1) { - if (source[i] != target[i]) + if ((source[i] - target[i]).abs() > tolerance) return false; } return true; @@ -121,6 +121,6 @@ Future testMain() async { final Uint8List benchmarkPixels = _pixelsToBytes( [0x0603F9FF, 0x80407FFF, 0xC0603FFF, 0xFF8000FF], ); - expect(actualPixels, listEqual(benchmarkPixels)); + expect(actualPixels, listEqual(benchmarkPixels, tolerance: 1)); }); } From 9f9d3757901120ce307256915de28e8b6d7c8787 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Wed, 3 Nov 2021 00:14:50 -0700 Subject: [PATCH 08/12] Fix --- lib/web_ui/test/html/image_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/test/html/image_test.dart b/lib/web_ui/test/html/image_test.dart index a7dc1bb13ab3c..93e91f489c100 100644 --- a/lib/web_ui/test/html/image_test.dart +++ b/lib/web_ui/test/html/image_test.dart @@ -27,7 +27,7 @@ _ListPredicate deepEqualList(List a) { }; } -Matcher listEqual(List source, {int tolerance = 0}) { +Matcher listEqual(List source, {int tolerance = 0}) { return predicate( (List target) { if (source.length != target.length) From 28f51a2ea3dc42c1856116b5cd45caf011f802e6 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Wed, 3 Nov 2021 14:34:21 -0700 Subject: [PATCH 09/12] Assign by pixel --- lib/web_ui/lib/src/ui/painting.dart | 41 ++++++++++++----------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index d14b48b5db1df..e4d4f9441b100 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -513,6 +513,16 @@ Future _createBmp( 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. // 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 @@ -550,40 +560,23 @@ Future _createBmp( // Important colors bmpData.setUint32(0x32, 0x00, Endian.little); // Bitmask R - bmpData.setUint32(0x36, 0x00FF0000, Endian.little); + bmpData.setUint32(0x36, swapRedBlue ? 0x00FF0000 : 0x000000FF, Endian.little); // Bitmask G bmpData.setUint32(0x3A, 0x0000FF00, Endian.little); // Bitmask B - bmpData.setUint32(0x3E, 0x000000FF, Endian.little); + bmpData.setUint32(0x3E, swapRedBlue ? 0x000000FF : 0x00FF0000, Endian.little); // Bitmask A bmpData.setUint32(0x42, 0xFF000000, Endian.little); - int pixelDestinationIndex = 0; - late bool swapRedBlue; - switch (format) { - case PixelFormat.bgra8888: - swapRedBlue = true; - break; - case PixelFormat.rgba8888: - swapRedBlue = false; - break; - } + int pixelDestinationIndex = 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 pixelSourceByte = rowCount * rowBytes * 4; + int pixelSourceByte = rowCount * rowBytes; for (int colCount = 0; colCount < width; colCount += 1) { - final int r = swapRedBlue ? pixels[pixelSourceByte + 2] : pixels[pixelSourceByte]; - final int b = swapRedBlue ? pixels[pixelSourceByte] : pixels[pixelSourceByte + 2]; - final int g = pixels[pixelSourceByte + 1]; - final int a = pixels[pixelSourceByte + 3]; - - // Set the pixel past the header data. - bmpData.setUint8(pixelDestinationIndex + headerSize + 0, b); - bmpData.setUint8(pixelDestinationIndex + headerSize + 1, g); - bmpData.setUint8(pixelDestinationIndex + headerSize + 2, r); - bmpData.setUint8(pixelDestinationIndex + headerSize + 3, a); + bmpData.setUint32(pixelDestinationIndex, combinedPixels[pixelSourceByte], Endian.little); pixelDestinationIndex += 4; - pixelSourceByte += 4; + pixelSourceByte += 1; } } From 24d637e3c3182925f534e42633549dd8bed2fe8e Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Wed, 3 Nov 2021 15:06:20 -0700 Subject: [PATCH 10/12] Add swap --- lib/web_ui/test/html/image_test.dart | 30 ++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/web_ui/test/html/image_test.dart b/lib/web_ui/test/html/image_test.dart index 93e91f489c100..6b6ec48bd312c 100644 --- a/lib/web_ui/test/html/image_test.dart +++ b/lib/web_ui/test/html/image_test.dart @@ -57,12 +57,17 @@ Uint8List _pixelsToBytes(List rawPixels) { })().toList()); } -Future _encodeToHtmlThenDecode(Uint8List rawBytes, int width, int height) async { +Future _encodeToHtmlThenDecode( + Uint8List rawBytes, + int width, + int height, { + PixelFormat pixelFormat = PixelFormat.rgba8888, +}) async { final ImageDescriptor descriptor = ImageDescriptor.raw( await ImmutableBuffer.fromUint8List(rawBytes), - width: 2, - height: 2, - pixelFormat: PixelFormat.rgba8888, + width: width, + height: height, + pixelFormat: pixelFormat, ); return (await (await descriptor.instantiateCodec()).getNextFrame()).image; } @@ -85,6 +90,23 @@ Future testMain() async { 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( + [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( + [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( From 095e7b9f6c3a326175dd691fcc649010968f2cff Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Wed, 3 Nov 2021 16:38:52 -0700 Subject: [PATCH 11/12] Better var name --- lib/web_ui/lib/src/ui/painting.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index e4d4f9441b100..23357044974f6 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -566,17 +566,17 @@ Future _createBmp( // Bitmask B bmpData.setUint32(0x3E, swapRedBlue ? 0x000000FF : 0x00FF0000, Endian.little); // Bitmask A - bmpData.setUint32(0x42, 0xFF000000, Endian.little); + bmpData.setUint32(0x42, 0xFF000000, Endian.lpixelSourceByteittle); - int pixelDestinationIndex = headerSize; + 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 pixelSourceByte = rowCount * rowBytes; + int sourcePixel = rowCount * rowBytes; for (int colCount = 0; colCount < width; colCount += 1) { - bmpData.setUint32(pixelDestinationIndex, combinedPixels[pixelSourceByte], Endian.little); - pixelDestinationIndex += 4; - pixelSourceByte += 1; + bmpData.setUint32(destinationByte, combinedPixels[sourcePixel], Endian.little); + destinationByte += 4; + sourcePixel += 1; } } From c0b1906ffc60fe0981d43c5778e6d8b9768fd43a Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Wed, 3 Nov 2021 23:26:31 -0700 Subject: [PATCH 12/12] Update painting.dart --- lib/web_ui/lib/src/ui/painting.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index 23357044974f6..05b976614f9d9 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -566,7 +566,7 @@ Future _createBmp( // Bitmask B bmpData.setUint32(0x3E, swapRedBlue ? 0x000000FF : 0x00FF0000, Endian.little); // Bitmask A - bmpData.setUint32(0x42, 0xFF000000, Endian.lpixelSourceByteittle); + bmpData.setUint32(0x42, 0xFF000000, Endian.little); int destinationByte = headerSize; final Uint32List combinedPixels = Uint32List.sublistView(pixels);