Reland: "Added wide-gamut color support for ui.Image.toByteData and ui.Image.colorSpace" (flutter/engine#40312)

Reland: "Added wide-gamut color support for `ui.Image.toByteData` and `ui.Image.colorSpace`"
This commit is contained in:
gaaclarke
2023-03-15 10:29:10 -07:00
committed by GitHub
parent 3dd20371cb
commit 695cfa5fe8
13 changed files with 163 additions and 8 deletions

View File

@@ -190,6 +190,7 @@ typedef CanvasPath Path;
V(Image, width, 1) \
V(Image, height, 1) \
V(Image, toByteData, 3) \
V(Image, colorSpace, 1) \
V(ImageDescriptor, bytesPerPixel, 1) \
V(ImageDescriptor, dispose, 1) \
V(ImageDescriptor, height, 1) \

View File

@@ -1568,6 +1568,31 @@ class Paint {
}
}
/// The color space describes the colors that are available to an [Image].
///
/// This value can help decide which [ImageByteFormat] to use with
/// [Image.toByteData]. Images that are in the [extendedSRGB] color space
/// should use something like [ImageByteFormat.rawExtendedRgba128] so that
/// colors outside of the sRGB gamut aren't lost.
///
/// This is also the result of [Image.colorSpace].
///
/// See also: https://en.wikipedia.org/wiki/Color_space
enum ColorSpace {
/// The sRGB color space.
///
/// You may know this as the standard color space for the web or the color
/// space of non-wide-gamut Flutter apps.
///
/// See also: https://en.wikipedia.org/wiki/SRGB
sRGB,
/// A color space that is backwards compatible with sRGB but can represent
/// colors outside of that gamut with values outside of [0..1]. In order to
/// see the extended values an [ImageByteFormat] like
/// [ImageByteFormat.rawExtendedRgba128] must be used.
extendedSRGB,
}
/// The format in which image bytes should be returned when using
/// [Image.toByteData].
// We do not expect to add more encoding formats to the ImageByteFormat enum,
@@ -1591,6 +1616,31 @@ enum ImageByteFormat {
/// image may use a single 8-bit channel for each pixel.
rawUnmodified,
/// Raw extended range RGBA format.
///
/// Unencoded bytes, in RGBA row-primary form with straight alpha, 32 bit
/// float (IEEE 754 binary32) per channel.
///
/// Example usage:
///
/// ```dart
/// import 'dart:ui' as ui;
/// import 'dart:typed_data';
///
/// Future<Map<String, double>> getFirstPixel(ui.Image image) async {
/// final ByteData data =
/// (await image.toByteData(format: ui.ImageByteFormat.rawExtendedRgba128))!;
/// final Float32List floats = Float32List.view(data.buffer);
/// return <String, double>{
/// 'r': floats[0],
/// 'g': floats[1],
/// 'b': floats[2],
/// 'a': floats[3],
/// };
/// }
/// ```
rawExtendedRgba128,
/// PNG format.
///
/// A loss-less compression format for images. This format is well suited for
@@ -1726,6 +1776,10 @@ class Image {
/// The [format] argument specifies the format in which the bytes will be
/// returned.
///
/// Using [ImageByteFormat.rawRgba] on an image in the color space
/// [ColorSpace.extendedSRGB] will result in the gamut being squished to fit
/// into the sRGB gamut, resulting in the loss of wide-gamut colors.
///
/// Returns a future that completes with the binary image data or an error
/// if encoding fails.
// We do not expect to add more encoding formats to the ImageByteFormat enum,
@@ -1737,6 +1791,29 @@ class Image {
return _image.toByteData(format: format);
}
/// The color space that is used by the [Image]'s colors.
///
/// This value is a consequence of how the [Image] has been created. For
/// example, loading a PNG that is in the Display P3 color space will result
/// in a [ColorSpace.extendedSRGB] image.
///
/// On rendering backends that don't support wide gamut colors (anything but
/// iOS impeller), wide gamut images will still report [ColorSpace.sRGB] if
/// rendering wide gamut colors isn't supported.
// Note: The docstring will become outdated as new platforms support wide
// gamut color, please keep it up to date.
ColorSpace get colorSpace {
final int colorSpaceValue = _image.colorSpace;
switch (colorSpaceValue) {
case 0:
return ColorSpace.sRGB;
case 1:
return ColorSpace.extendedSRGB;
default:
throw UnsupportedError('Unrecognized color space: $colorSpaceValue');
}
}
/// If asserts are enabled, returns the [StackTrace]s of each open handle from
/// [clone], in creation order.
///
@@ -1903,6 +1980,9 @@ class _Image extends NativeFieldWrapperClass1 {
final Set<Image> _handles = <Image>{};
@Native<Int32 Function(Pointer<Void>)>(symbol: 'Image::colorSpace')
external int get colorSpace;
@override
String toString() => '[$width\u00D7$height]';
}

View File

@@ -7,6 +7,9 @@
#include <algorithm>
#include <limits>
#if IMPELLER_SUPPORTS_RENDERING
#include "flutter/lib/ui/painting/image_encoding_impeller.h"
#endif
#include "flutter/lib/ui/painting/image_encoding.h"
#include "third_party/tonic/converter/dart_converter.h"
#include "third_party/tonic/dart_args.h"
@@ -35,4 +38,16 @@ void CanvasImage::dispose() {
ClearDartWrapper();
}
int CanvasImage::colorSpace() {
if (image_->skia_image()) {
return ColorSpace::kSRGB;
} else if (image_->impeller_texture()) {
#if IMPELLER_SUPPORTS_RENDERING
return ImageEncodingImpeller::GetColorSpace(image_->impeller_texture());
#endif // IMPELLER_SUPPORTS_RENDERING
}
return -1;
}
} // namespace flutter

View File

@@ -14,6 +14,12 @@
namespace flutter {
// Must be kept in sync with painting.dart.
enum ColorSpace {
kSRGB,
kExtendedSRGB,
};
class CanvasImage final : public RefCountedDartWrappable<CanvasImage> {
DEFINE_WRAPPERTYPEINFO();
FML_FRIEND_MAKE_REF_COUNTED(CanvasImage);
@@ -37,6 +43,8 @@ class CanvasImage final : public RefCountedDartWrappable<CanvasImage> {
void set_image(sk_sp<DlImage> image) { image_ = image; }
int colorSpace();
private:
CanvasImage();

View File

@@ -38,6 +38,7 @@ enum ImageByteFormat {
kRawRGBA,
kRawStraightRGBA,
kRawUnmodified,
kRawExtendedRgba128,
kPNG,
};
@@ -123,19 +124,21 @@ sk_sp<SkData> EncodeImage(const sk_sp<SkImage>& raster_image,
return nullptr;
};
return png_image;
} break;
case kRawRGBA: {
}
case kRawRGBA:
return CopyImageByteData(raster_image, kRGBA_8888_SkColorType,
kPremul_SkAlphaType);
} break;
case kRawStraightRGBA: {
case kRawStraightRGBA:
return CopyImageByteData(raster_image, kRGBA_8888_SkColorType,
kUnpremul_SkAlphaType);
} break;
case kRawUnmodified: {
case kRawUnmodified:
return CopyImageByteData(raster_image, raster_image->colorType(),
raster_image->alphaType());
} break;
case kRawExtendedRgba128:
return CopyImageByteData(raster_image, kRGBA_F32_SkColorType,
kUnpremul_SkAlphaType);
}
FML_LOG(ERROR) << "Unknown error encoding image.";

View File

@@ -163,4 +163,16 @@ void ImageEncodingImpeller::ConvertImageToRaster(
});
}
int ImageEncodingImpeller::GetColorSpace(
const std::shared_ptr<impeller::Texture>& texture) {
const impeller::TextureDescriptor& desc = texture->GetTextureDescriptor();
switch (desc.format) {
case impeller::PixelFormat::kB10G10R10XR: // intentional_fallthrough
case impeller::PixelFormat::kR16G16B16A16Float:
return ColorSpace::kExtendedSRGB;
default:
return ColorSpace::kSRGB;
}
}
} // namespace flutter

View File

@@ -17,6 +17,8 @@ namespace flutter {
class ImageEncodingImpeller {
public:
static int GetColorSpace(const std::shared_ptr<impeller::Texture>& texture);
/// Converts a DlImage to a SkImage.
/// This should be called from the thread that corresponds to
/// `dl_image->owning_context()` when gpu access is guaranteed.

View File

@@ -351,6 +351,8 @@ abstract class Image {
List<StackTrace>? debugGetOpenHandleStackTraces() => null;
ColorSpace get colorSpace => ColorSpace.sRGB;
@override
String toString() => '[$width\u00D7$height]';
}
@@ -431,6 +433,11 @@ class ImageFilter {
engine.renderer.composeImageFilters(outer: outer, inner: inner);
}
enum ColorSpace {
sRGB,
extendedSRGB,
}
enum ImageByteFormat {
rawRgba,
rawStraightRgba,

View File

@@ -378,6 +378,9 @@ class CkImage implements ui.Image, StackTraceDebugger {
}
}
@override
ui.ColorSpace get colorSpace => ui.ColorSpace.sRGB;
Future<ByteData> _readPixelsFromSkImage(ui.ImageByteFormat format) {
final SkAlphaType alphaType = format == ui.ImageByteFormat.rawStraightRgba ? canvasKit.AlphaType.Unpremul : canvasKit.AlphaType.Premul;
final ByteData? data = _encodeImage(

View File

@@ -204,6 +204,9 @@ class HtmlImage implements ui.Image {
}
}
@override
ui.ColorSpace get colorSpace => ui.ColorSpace.sRGB;
DomHTMLImageElement cloneImageElement() {
if (!_didClone) {
_didClone = true;

View File

@@ -23,6 +23,9 @@ class SkwasmImage implements ui.Image {
throw UnimplementedError();
}
@override
ui.ColorSpace get colorSpace => ui.ColorSpace.sRGB;
@override
void dispose() {
throw UnimplementedError();

View File

@@ -7,7 +7,7 @@ import 'dart:typed_data';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/src/engine.dart' hide ColorSpace;
import 'package:ui/ui.dart' hide TextStyle;
import 'package:web_engine_tester/golden_tester.dart';
@@ -780,6 +780,9 @@ class TestImage implements Image {
@override
List<StackTrace>/*?*/ debugGetOpenHandleStackTraces() => <StackTrace>[];
@override
ColorSpace get colorSpace => ColorSpace.sRGB;
}
Paragraph createTestParagraph() {

View File

@@ -67,6 +67,21 @@ void main() {
final List<int> expected = await readFile('square.png');
expect(Uint8List.view(data.buffer), expected);
});
test('Image.toByteData ExtendedRGBA128', () async {
final Image image = await Square4x4Image.image;
final ByteData data = (await image.toByteData(format: ImageByteFormat.rawExtendedRgba128))!;
expect(image.width, _kWidth);
expect(image.height, _kWidth);
expect(data.lengthInBytes, _kWidth * _kWidth * 4 * 4);
// Top-left pixel should be black.
final Float32List floats = Float32List.view(data.buffer);
expect(floats[0], 0.0);
expect(floats[1], 0.0);
expect(floats[2], 0.0);
expect(floats[3], 1.0);
expect(image.colorSpace, ColorSpace.sRGB);
});
}
class Square4x4Image {