From c3267f5786c46a9390e68c5c29bef6d16191293c Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Sun, 7 Nov 2021 09:38:55 -0800 Subject: [PATCH] Revert "[Web] Fix BMP encoder (#29448)" This reverts commit 15d5a23cf4ad82404e5ef04f17dfac9061975f2a. --- lib/web_ui/lib/src/ui/painting.dart | 99 ++++++++---------- lib/web_ui/test/html/image_test.dart | 148 --------------------------- 2 files changed, 44 insertions(+), 203 deletions(-) delete mode 100644 lib/web_ui/test/html/image_test.dart diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index 8082670085a95..910f092a2b50b 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -500,12 +500,6 @@ Future _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 _createBmp( Uint8List pixels, int width, @@ -513,70 +507,64 @@ 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 - // they're not supported. - const int dibSize = 0x6C /* 108: BITMAPV4HEADER */; - const int headerSize = dibSize + 0x0E; - final int bufferSize = headerSize + (width * height * 4); + final int bufferSize = 0x36 + (width * height * 4); final ByteData bmpData = ByteData(bufferSize); // 'BM' header - bmpData.setUint16(0x00, 0x424D, Endian.big); + bmpData.setUint8(0x00, 0x42); + bmpData.setUint8(0x01, 0x4D); // Size of data bmpData.setUint32(0x02, bufferSize, Endian.little); // Offset where pixel array begins - bmpData.setUint32(0x0A, headerSize, Endian.little); + bmpData.setUint32(0x0A, 0x36, Endian.little); // Bytes in DIB header - bmpData.setUint32(0x0E, dibSize, Endian.little); - // Width + bmpData.setUint32(0x0E, 0x28, Endian.little); + // width bmpData.setUint32(0x12, width, Endian.little); - // Height + // height bmpData.setUint32(0x16, height, Endian.little); - // Color panes (always 1) + // Color panes bmpData.setUint16(0x1A, 0x01, Endian.little); - // 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 + // 32 bpp + bmpData.setUint16(0x1C, 0x20, Endian.little); + // no compression + bmpData.setUint32(0x1E, 0x00, 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, 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; + + + 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; } } @@ -815,3 +803,4 @@ class FragmentProgram { required Float32List floatUniforms, }) => throw UnsupportedError('FragmentProgram is not supported for the CanvasKit or HTML renderers.'); } + diff --git a/lib/web_ui/test/html/image_test.dart b/lib/web_ui/test/html/image_test.dart deleted file mode 100644 index 6b6ec48bd312c..0000000000000 --- a/lib/web_ui/test/html/image_test.dart +++ /dev/null @@ -1,148 +0,0 @@ -// 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 = 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; - }; -} - -Matcher listEqual(List source, {int tolerance = 0}) { - 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]).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 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, { - 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 testMain() async { - test('Correctly encodes an opaque image', () async { - // A 2x2 testing image without transparency. - final Image sourceImage = await _encodeToHtmlThenDecode( - _pixelsToBytes( - [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( - [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( - [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( - _pixelsToBytes( - [0xFF800006, 0xFF800080, 0xFF8000C0, 0xFF8000FF], - ), 2, 2, - ); - 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, tolerance: 1)); - }); -}