forked from flutter/flutter
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Web] Fix BMP encoder (flutter#29448)
This PR fixes 2 bugs of how an image is encoded into a BMP.
- Loading branch information
Showing
2 changed files
with
203 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
// 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<T> = bool Function(List<T>); | ||
_ListPredicate<T> deepEqualList<T>(List<T> a) { | ||
return (List<T> 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<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; | ||
} | ||
|
||
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 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<int> actualPixels = imageData.data; | ||
|
||
final Uint8List benchmarkPixels = _pixelsToBytes( | ||
<int>[0x0603F9FF, 0x80407FFF, 0xC0603FFF, 0xFF8000FF], | ||
); | ||
expect(actualPixels, listEqual(benchmarkPixels, tolerance: 1)); | ||
}); | ||
} |