diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index 0690e52138d48..8a67398f07181 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -4,7 +4,7 @@ /// Bindings for CanvasKit JavaScript API. /// -/// Prefer keeping the originl CanvasKit names so it is easier to locate +/// Prefer keeping the original CanvasKit names so it is easier to locate /// the API behind these bindings in the Skia source code. // @dart = 2.10 @@ -31,6 +31,8 @@ class CanvasKit { external SkBlurStyleEnum get BlurStyle; external SkTileModeEnum get TileMode; external SkFillTypeEnum get FillType; + external SkAlphaTypeEnum get AlphaType; + external SkColorTypeEnum get ColorType; external SkPathOpEnum get PathOp; external SkClipOpEnum get ClipOp; external SkPointModeEnum get PointMode; @@ -62,6 +64,13 @@ class CanvasKit { external SkParagraphStyle ParagraphStyle( SkParagraphStyleProperties properties); external SkTextStyle TextStyle(SkTextStyleProperties properties); + external SkSurface MakeSurface( + int width, + int height, + ); + external Uint8List getSkDataBytes( + SkData skData, + ); // Text decoration enum is embedded in the CanvasKit object itself. external int get NoDecoration; @@ -128,6 +137,7 @@ class SkSurface { external int width(); external int height(); external void dispose(); + external SkImage makeImageSnapshot(); } @JS() @@ -623,6 +633,38 @@ SkTileMode toSkTileMode(ui.TileMode mode) { return _skTileModes[mode.index]; } +@JS() +class SkAlphaTypeEnum { + external SkAlphaType get Opaque; + external SkAlphaType get Premul; + external SkAlphaType get Unpremul; +} + +@JS() +class SkAlphaType { + external int get value; +} + +@JS() +class SkColorTypeEnum { + external SkColorType get Alpha_8; + external SkColorType get RGB_565; + external SkColorType get ARGB_4444; + external SkColorType get RGBA_8888; + external SkColorType get RGB_888x; + external SkColorType get BGRA_8888; + external SkColorType get RGBA_1010102; + external SkColorType get RGB_101010x; + external SkColorType get Gray_8; + external SkColorType get RGBA_F16; + external SkColorType get RGBA_F32; +} + +@JS() +class SkColorType { + external int get value; +} + @JS() @anonymous class SkAnimatedImage { @@ -634,6 +676,8 @@ class SkAnimatedImage { external SkImage getCurrentFrame(); external int width(); external int height(); + external Uint8List readPixels(SkImageInfo imageInfo, int srcX, int srcY); + external SkData encodeToData(); /// Deletes the C++ object. /// @@ -652,6 +696,8 @@ class SkImage { SkTileMode tileModeY, Float32List? matrix, // 3x3 matrix ); + external Uint8List readPixels(SkImageInfo imageInfo, int srcX, int srcY); + external SkData encodeToData(); } @JS() @@ -1662,3 +1708,34 @@ external Object? get _finalizationRegistryConstructor; /// Whether the current browser supports `FinalizationRegistry`. bool browserSupportsFinalizationRegistry = _finalizationRegistryConstructor != null; + +@JS() +class SkData { + external int size(); + external bool isEmpty(); + external Uint8List bytes(); +} + +@JS() +@anonymous +class SkImageInfo { + external factory SkImageInfo({ + required int width, + required int height, + SkAlphaType alphaType, + SkColorSpace colorSpace, + SkColorType colorType, + }); + external SkAlphaType get alphaType; + external SkColorSpace get colorSpace; + external SkColorType get colorType; + external int get height; + external bool get isEmpty; + external bool get isOpaque; + external SkRect get bounds; + external int get width; + external SkImageInfo makeAlphaType(SkAlphaType alphaType); + external SkImageInfo makeColorSpace(SkColorSpace colorSpace); + external SkImageInfo makeColorType(SkColorType colorType); + external SkImageInfo makeWH(int width, int height); +} diff --git a/lib/web_ui/lib/src/engine/canvaskit/image.dart b/lib/web_ui/lib/src/engine/canvaskit/image.dart index 26006585dd647..8bcb210d961ef 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/image.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -75,7 +75,25 @@ class CkAnimatedImage implements ui.Image { @override Future toByteData( {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - throw 'unimplemented'; + Uint8List bytes; + + if (format == ui.ImageByteFormat.rawRgba) { + final SkImageInfo imageInfo = SkImageInfo( + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + colorSpace: SkColorSpaceSRGB, + width: width, + height: height, + ); + bytes = _skAnimatedImage.readPixels(imageInfo, 0, 0); + } else { + final SkData skData = _skAnimatedImage.encodeToData(); //defaults to PNG 100% + // make a copy that we can return + bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); + } + + final ByteData data = bytes.buffer.asByteData(0, bytes.length); + return Future.value(data); } } @@ -105,7 +123,25 @@ class CkImage implements ui.Image { @override Future toByteData( {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - throw 'unimplemented'; + Uint8List bytes; + + if (format == ui.ImageByteFormat.rawRgba) { + final SkImageInfo imageInfo = SkImageInfo( + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + colorSpace: SkColorSpaceSRGB, + width: width, + height: height, + ); + bytes = skImage.readPixels(imageInfo, 0, 0); + } else { + final SkData skData = skImage.encodeToData(); //defaults to PNG 100% + // make a copy that we can return + bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); + } + + final ByteData data = bytes.buffer.asByteData(0, bytes.length); + return Future.value(data); } } diff --git a/lib/web_ui/lib/src/engine/canvaskit/picture.dart b/lib/web_ui/lib/src/engine/canvaskit/picture.dart index bcce6fe1b7bef..2a0d292ebd45b 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/picture.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/picture.dart @@ -21,9 +21,13 @@ class CkPicture implements ui.Picture { } @override - Future toImage(int width, int height) { - throw UnsupportedError( - 'Picture.toImage not yet implemented for CanvasKit and HTML'); + Future toImage(int width, int height) async { + final SkSurface skSurface = canvasKit.MakeSurface(width, height); + final SkCanvas skCanvas = skSurface.getCanvas(); + skCanvas.drawPicture(skiaObject.skiaObject); + final SkImage skImage = skSurface.makeImageSnapshot(); + skSurface.dispose(); + return CkImage(skImage); } } diff --git a/lib/web_ui/lib/src/engine/html_image_codec.dart b/lib/web_ui/lib/src/engine/html_image_codec.dart index a42f4b4f1c746..6a0595d66ae50 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -129,13 +129,13 @@ class HtmlImage implements ui.Image { final int height; @override - Future toByteData( - {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - return futurize((Callback callback) { - return _toByteData(format.index, (Uint8List? encoded) { - callback(encoded?.buffer.asByteData()); - }); - }); + Future toByteData({ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { + if (imgElement.src?.startsWith('data:') == true) { + final data = UriData.fromUri(Uri.parse(imgElement.src!)); + return Future.value(data.contentAsBytes().buffer.asByteData()); + } else { + return Future.value(null); + } } // Returns absolutely positioned actual image element on first call and @@ -149,12 +149,4 @@ class HtmlImage implements ui.Image { return imgElement; } } - - // TODO(het): Support this for asset images and images generated from - // `Picture`s. - /// Returns an error message on failure, null on success. - String _toByteData(int format, Callback callback) { - callback(null); - return 'Image.toByteData is not supported in Flutter for Web'; - } } diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index 9847ca29a1dd0..8770120ac712c 100644 --- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -1188,4 +1188,29 @@ void _canvasTests() { 20, ); }); + + test('toImage.toByteData', () async { + final SkPictureRecorder otherRecorder = SkPictureRecorder(); + final SkCanvas otherCanvas = otherRecorder.beginRecording(SkRect( + fLeft: 0, + fTop: 0, + fRight: 1, + fBottom: 1, + )); + otherCanvas.drawRect( + SkRect( + fLeft: 0, + fTop: 0, + fRight: 1, + fBottom: 1, + ), + SkPaint(), + ); + final CkPicture picture = CkPicture(otherRecorder.finishRecordingAsPicture(), null); + final CkImage image = await picture.toImage(1, 1); + final ByteData rawData = await image.toByteData(format: ui.ImageByteFormat.rawRgba); + expect(rawData, isNotNull); + final ByteData pngData = await image.toByteData(format: ui.ImageByteFormat.png); + expect(pngData, isNotNull); + }); } diff --git a/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart new file mode 100644 index 0000000000000..02c5a9c16b05a --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/canvas_to_picture_test.dart @@ -0,0 +1,55 @@ +// 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. + +// @dart = 2.6 +import 'dart:html' as html; + +import 'package:ui/ui.dart'; +import 'package:ui/src/engine.dart'; + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { + final Rect region = Rect.fromLTWH(0, 0, 500, 500); + + setUp(() async { + debugShowClipLayers = true; + SurfaceSceneBuilder.debugForgetFrameScene(); + for (html.Node scene in html.document.querySelectorAll('flt-scene')) { + scene.remove(); + } + + await webOnlyInitializePlatform(); + webOnlyFontCollection.debugRegisterTestFonts(); + await webOnlyFontCollection.ensureFontsLoaded(); + }); + + test('Convert Canvas to Picture', () async { + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + final Picture testPicture = await _drawTestPictureWithCircle(region); + builder.addPicture(Offset.zero, testPicture); + + html.document.body.append(builder + .build() + .webOnlyRootElement); + + //await matchGoldenFile('canvas_to_picture.png', region: region, write: true); + }); +} + +Picture _drawTestPictureWithCircle(Rect region) { + final EnginePictureRecorder recorder = PictureRecorder(); + final RecordingCanvas canvas = recorder.beginRecording(region); + canvas.drawOval( + region, + Paint() + ..style = PaintingStyle.fill + ..color = Color(0xFF00FF00)); + return recorder.endRecording(); +}