diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 8e31f539c527f..a8d951a137732 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -2007,6 +2007,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/picture.da ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_canvas.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_fonts.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_geometry.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_image.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_memory.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_paint.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_path.dart + ../../../flutter/LICENSE @@ -2077,6 +2078,7 @@ ORIGIN: ../../../flutter/lib/web_ui/skwasm/data.cpp + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/skwasm/export.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/skwasm/fonts.cpp + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/skwasm/helpers.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/skwasm/image.cpp + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/skwasm/paint.cpp + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/skwasm/path.cpp + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/skwasm/picture.cpp + ../../../flutter/LICENSE @@ -4626,6 +4628,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/picture.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_fonts.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_geometry.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_image.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_memory.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_paint.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_path.dart @@ -4696,6 +4699,7 @@ FILE: ../../../flutter/lib/web_ui/skwasm/data.cpp FILE: ../../../flutter/lib/web_ui/skwasm/export.h FILE: ../../../flutter/lib/web_ui/skwasm/fonts.cpp FILE: ../../../flutter/lib/web_ui/skwasm/helpers.h +FILE: ../../../flutter/lib/web_ui/skwasm/image.cpp FILE: ../../../flutter/lib/web_ui/skwasm/paint.cpp FILE: ../../../flutter/lib/web_ui/skwasm/path.cpp FILE: ../../../flutter/lib/web_ui/skwasm/picture.cpp diff --git a/lib/web_ui/lib/painting.dart b/lib/web_ui/lib/painting.dart index 59e3773ceabce..b974b4868968d 100644 --- a/lib/web_ui/lib/painting.dart +++ b/lib/web_ui/lib/painting.dart @@ -398,6 +398,7 @@ class MaskFilter { String toString() => 'MaskFilter.blur($_style, ${_sigma.toStringAsFixed(1)})'; } +// This needs to be kept in sync with the "_FilterQuality" enum in skwasm's canvas.cpp enum FilterQuality { none, low, @@ -438,6 +439,7 @@ enum ColorSpace { extendedSRGB, } +// This must be kept in sync with the `ImageByteFormat` enum in Skwasm's surface.cpp. enum ImageByteFormat { rawRgba, rawStraightRgba, @@ -445,9 +447,11 @@ enum ImageByteFormat { png, } +// This must be kept in sync with the `PixelFormat` enum in Skwasm's image.cpp. enum PixelFormat { rgba8888, bgra8888, + rgbaFloat32, } typedef ImageDecoderCallback = void Function(Image result); @@ -561,6 +565,8 @@ Future createBmp( swapRedBlue = true; case PixelFormat.rgba8888: swapRedBlue = false; + case PixelFormat.rgbaFloat32: + throw UnimplementedError('RGB conversion from rgbaFloat32 data is not implemented'); } // See https://en.wikipedia.org/wiki/BMP_file_format for format examples. @@ -737,9 +743,19 @@ class Shadow { } abstract class ImageShader implements Shader { - factory ImageShader(Image image, TileMode tmx, TileMode tmy, Float64List matrix4, { + factory ImageShader( + Image image, + TileMode tmx, + TileMode tmy, + Float64List matrix4, { FilterQuality? filterQuality, - }) => engine.renderer.createImageShader(image, tmx, tmy, matrix4, filterQuality); + }) => engine.renderer.createImageShader( + image, + tmx, + tmy, + matrix4, + filterQuality + ); @override void dispose(); diff --git a/lib/web_ui/lib/src/engine/canvaskit/image.dart b/lib/web_ui/lib/src/engine/canvaskit/image.dart index d100f5a83c5c1..c7dcd80d431f3 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/image.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -62,9 +62,7 @@ void skiaDecodeImageFromPixels( } if (targetWidth != null || targetHeight != null) { - if (!validUpscale(allowUpscaling, targetWidth, targetHeight, width, height)) { - domWindow.console.warn('Cannot apply targetWidth/targetHeight when allowUpscaling is false.'); - } else { + if (validUpscale(allowUpscaling, targetWidth, targetHeight, width, height)) { return callback(scaleImage(skImage, targetWidth, targetHeight)); } } diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl.dart index c1c5eade1687e..613c524417c56 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl.dart @@ -21,6 +21,7 @@ export 'skwasm_impl/picture.dart'; export 'skwasm_impl/raw/raw_canvas.dart'; export 'skwasm_impl/raw/raw_fonts.dart'; export 'skwasm_impl/raw/raw_geometry.dart'; +export 'skwasm_impl/raw/raw_image.dart'; export 'skwasm_impl/raw/raw_memory.dart'; export 'skwasm_impl/raw/raw_paint.dart'; export 'skwasm_impl/raw/raw_path.dart'; diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart index ddf1326effef2..ec1d7afd50289 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart @@ -183,21 +183,51 @@ class SkwasmCanvas implements ui.Canvas { } @override - void drawImage(ui.Image uiImage, ui.Offset offset, ui.Paint uiPaint) { - throw UnimplementedError(); - } + void drawImage(ui.Image image, ui.Offset offset, ui.Paint paint) => + canvasDrawImage( + _handle, + (image as SkwasmImage).handle, + offset.dx, + offset.dy, + (paint as SkwasmPaint).handle, + paint.filterQuality.index, + ); @override void drawImageRect( - ui.Image uiImage, ui.Rect src, ui.Rect dst, ui.Paint uiPaint) { - throw UnimplementedError(); - } + ui.Image image, + ui.Rect src, + ui.Rect dst, + ui.Paint paint) => withStackScope((StackScope scope) { + final Pointer sourceRect = scope.convertRectToNative(src); + final Pointer destRect = scope.convertRectToNative(dst); + canvasDrawImageRect( + _handle, + (image as SkwasmImage).handle, + sourceRect, + destRect, + (paint as SkwasmPaint).handle, + paint.filterQuality.index, + ); + }); @override void drawImageNine( - ui.Image uiImage, ui.Rect center, ui.Rect dst, ui.Paint uiPaint) { - throw UnimplementedError(); - } + ui.Image image, + ui.Rect center, + ui.Rect dst, + ui.Paint paint) => withStackScope((StackScope scope) { + final Pointer centerRect = scope.convertIRectToNative(center); + final Pointer destRect = scope.convertRectToNative(dst); + canvasDrawImageNine( + _handle, + (image as SkwasmImage).handle, + centerRect, + destRect, + (paint as SkwasmPaint).handle, + paint.filterQuality.index, + ); + }); @override void drawPicture(ui.Picture picture) { diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/image.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/image.dart index a019591f3dde3..2045248d7730b 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/image.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/image.dart @@ -2,25 +2,52 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ffi'; import 'dart:typed_data'; +import 'package:ui/src/engine.dart'; +import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; import 'package:ui/ui.dart' as ui; class SkwasmImage implements ui.Image { - @override - int get width { - throw UnimplementedError(); + SkwasmImage(this.handle); + + factory SkwasmImage.fromPixels( + Uint8List pixels, + int width, + int height, + ui.PixelFormat format, { + int? rowBytes, + }) { + final SkDataHandle dataHandle = skDataCreate(pixels.length); + final Pointer dataPointer = skDataGetPointer(dataHandle).cast(); + for (int i = 0; i < pixels.length; i++) { + dataPointer[i] = pixels[i]; + } + final ImageHandle imageHandle = imageCreateFromPixels( + dataHandle, + width, + height, + format.index, + rowBytes ?? 4 * width, + ); + skDataDispose(dataHandle); + return SkwasmImage(imageHandle); } + final ImageHandle handle; + bool _isDisposed = false; + @override - int get height { - throw UnimplementedError(); - } + int get width => imageGetWidth(handle); + + @override + int get height => imageGetHeight(handle); @override Future toByteData( {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) { - throw UnimplementedError(); + return (renderer as SkwasmRenderer).surface.rasterizeImage(this, format); } @override @@ -28,13 +55,14 @@ class SkwasmImage implements ui.Image { @override void dispose() { - throw UnimplementedError(); + if (!_isDisposed) { + imageDispose(handle); + _isDisposed = true; + } } @override - bool get debugDisposed { - throw UnimplementedError(); - } + bool get debugDisposed => _isDisposed; @override SkwasmImage clone() => this; diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paint.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paint.dart index 3c953df026394..5b070d835c6f5 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paint.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paint.dart @@ -76,31 +76,31 @@ class SkwasmPaint implements ui.Paint { @override set strokeMiterLimit(double limit) => paintSetMiterLimit(_handle, limit); - // Unimplemented stuff below @override - ui.ColorFilter? colorFilter; + ui.Shader? get shader => _shader; @override - ui.FilterQuality filterQuality = ui.FilterQuality.none; + set shader(ui.Shader? uiShader) { + final SkwasmShader? skwasmShader = uiShader as SkwasmShader?; + _shader = skwasmShader; + final ShaderHandle shaderHandle = + skwasmShader != null ? skwasmShader.handle : nullptr; + paintSetShader(_handle, shaderHandle); + } @override - ui.ImageFilter? imageFilter; + ui.FilterQuality filterQuality = ui.FilterQuality.none; + // Unimplemented stuff below @override - bool invertColors = false; + ui.ColorFilter? colorFilter; @override - ui.MaskFilter? maskFilter; + ui.ImageFilter? imageFilter; @override - ui.Shader? get shader => _shader; + bool invertColors = false; @override - set shader(ui.Shader? uiShader) { - final SkwasmShader? skwasmShader = uiShader as SkwasmShader?; - _shader = skwasmShader; - final ShaderHandle shaderHandle = - skwasmShader != null ? skwasmShader.handle : nullptr; - paintSetShader(_handle, shaderHandle); - } + ui.MaskFilter? maskFilter; } diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/picture.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/picture.dart index 00959d06e702a..2fbb2043f7f79 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/picture.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/picture.dart @@ -12,9 +12,7 @@ class SkwasmPicture implements ui.Picture { PictureHandle get handle => _handle; @override - Future toImage(int width, int height) { - throw UnimplementedError(); - } + Future toImage(int width, int height) async => toImageSync(width, height); @override void dispose() { @@ -30,10 +28,8 @@ class SkwasmPicture implements ui.Picture { bool debugDisposed = false; @override - ui.Image toImageSync(int width, int height) { - // TODO(jacksongardner): implement toImageSync - throw UnimplementedError(); - } + ui.Image toImageSync(int width, int height) => + SkwasmImage(imageCreateFromPicture(handle, width, height)); ui.Rect get cullRect { return withStackScope((StackScope s) { diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_canvas.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_canvas.dart index 8571c000f7338..a98f6cdf74c4e 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_canvas.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_canvas.dart @@ -9,9 +9,9 @@ import 'dart:ffi'; import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; -final class CanvasWrapper extends Opaque {} +final class RawCanvas extends Opaque {} -typedef CanvasHandle = Pointer; +typedef CanvasHandle = Pointer; @Native(symbol: 'canvas_destroy', isLeaf: true) external void canvasDestroy(CanvasHandle canvas); @@ -126,6 +126,57 @@ external void canvasDrawPath( symbol: 'canvas_drawPicture', isLeaf: true) external void canvasDrawPicture(CanvasHandle canvas, PictureHandle picture); +@Native(symbol: 'canvas_drawImage', isLeaf: true) +external void canvasDrawImage( + CanvasHandle handle, + ImageHandle image, + double offsetX, + double offsetY, + PaintHandle paint, + int filterQuality, +); + +@Native, + Pointer, + PaintHandle, + Int, +)>(symbol: 'canvas_drawImageRect', isLeaf: true) +external void canvasDrawImageRect( + CanvasHandle handle, + ImageHandle image, + Pointer sourceRect, + Pointer destRect, + PaintHandle paint, + int filterQuality, +); + +@Native, + Pointer, + PaintHandle, + Int, +)>(symbol: 'canvas_drawImageNine', isLeaf: true) +external void canvasDrawImageNine( + CanvasHandle handle, + ImageHandle image, + Pointer centerRect, + Pointer destRect, + PaintHandle paint, + int filterQuality, +); + @Native( symbol: 'canvas_drawShadow', isLeaf: true) external void canvasDrawShadow( diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_image.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_image.dart new file mode 100644 index 0000000000000..11db8650cdcd5 --- /dev/null +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_image.dart @@ -0,0 +1,48 @@ +// 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. + +@DefaultAsset('skwasm') +library skwasm_impl; + +import 'dart:ffi'; + +import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; + +final class RawImage extends Opaque {} +typedef ImageHandle = Pointer; + +@Native(symbol: 'image_createFromPicture', isLeaf: true) +external ImageHandle imageCreateFromPicture( + PictureHandle handle, + int width, + int height, +); + +@Native(symbol: 'image_createFromPixels', isLeaf: true) +external ImageHandle imageCreateFromPixels( + SkDataHandle pixelData, + int width, + int height, + int pixelFormat, + int rowByteCount, +); + +@Native(symbol: 'image_dispose', isLeaf: true) +external void imageDispose(ImageHandle handle); + +@Native(symbol: 'image_getWidth', isLeaf: true) +external int imageGetWidth(ImageHandle handle); + +@Native(symbol: 'image_getHeight', isLeaf: true) +external int imageGetHeight(ImageHandle handle); diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_shaders.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_shaders.dart index a999ddad28b30..9bda0b4e08e33 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_shaders.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_shaders.dart @@ -121,3 +121,18 @@ external ShaderHandle shaderCreateRuntimeEffectShader( Pointer childShaders, int childCount ); + +@Native(symbol: 'shader_createFromImage', isLeaf: true) +external ShaderHandle shaderCreateFromImage( + ImageHandle handle, + int tileModeX, + int tileModeY, + int quality, + RawMatrix33 localMatrix, +); diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_skdata.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_skdata.dart index d212fa56968e9..8d25154f870df 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_skdata.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_skdata.dart @@ -16,5 +16,11 @@ external SkDataHandle skDataCreate(int size); @Native Function(SkDataHandle)>(symbol: 'skData_getPointer', isLeaf: true) external Pointer skDataGetPointer(SkDataHandle handle); +@Native Function(SkDataHandle)>(symbol: 'skData_getConstPointer', isLeaf: true) +external Pointer skDataGetConstPointer(SkDataHandle handle); + +@Native(symbol: 'skData_getSize', isLeaf: true) +external int skDataGetSize(SkDataHandle handle); + @Native(symbol: 'skData_dispose', isLeaf: true) external void skDataDispose(SkDataHandle handle); diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart index 13ba6ac6078c1..e9c58b1f21864 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart @@ -22,9 +22,9 @@ external SurfaceHandle surfaceCreateFromCanvas( ); @Native( - symbol: 'surface_setOnRenderCallback', + symbol: 'surface_setCallbackHandler', isLeaf: true) -external void surfaceSetOnRenderCallback( +external void surfaceSetCallbackHandler( SurfaceHandle surface, OnRenderCallbackHandle callback, ); @@ -47,3 +47,14 @@ external void surfaceSetCanvasSize( symbol: 'surface_renderPicture', isLeaf: true) external int surfaceRenderPicture(SurfaceHandle surface, PictureHandle picture); + +@Native(symbol: 'surface_rasterizeImage', isLeaf: true) +external int surfaceRasterizeImage( + SurfaceHandle handle, + ImageHandle image, + int format, +); diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart index 59594feaf68ca..4a232ca5cfcae 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart @@ -75,9 +75,19 @@ class SkwasmRenderer implements Renderer { } @override - ui.ImageShader createImageShader(ui.Image image, ui.TileMode tmx, ui.TileMode tmy, Float64List matrix4, ui.FilterQuality? filterQuality) { - throw UnimplementedError('createImageShader not yet implemented'); - } + ui.ImageShader createImageShader( + ui.Image image, + ui.TileMode tmx, + ui.TileMode tmy, + Float64List matrix4, + ui.FilterQuality? filterQuality + ) => SkwasmImageShader.imageShader( + image as SkwasmImage, + tmx, + tmy, + matrix4, + filterQuality + ); @override ui.Gradient createLinearGradient( @@ -285,9 +295,81 @@ class SkwasmRenderer implements Renderer { indices: indices ); + ui.Size? _scaledSize( + int width, + int height, + int? targetWidth, + int? targetHeight, + ) { + if (targetWidth == width && targetHeight == height) { + // Not scaled + return null; + } + if (targetWidth == null) { + if (targetHeight == null || targetHeight == height) { + // Not scaled. + return null; + } + targetWidth = (width * targetHeight / height).round(); + } else if (targetHeight == null) { + if (targetWidth == targetWidth) { + // Not scaled. + return null; + } + targetHeight = (height * targetWidth / width).round(); + } + return ui.Size(targetWidth.toDouble(), targetHeight.toDouble()); + } + @override - void decodeImageFromPixels(Uint8List pixels, int width, int height, ui.PixelFormat format, ui.ImageDecoderCallback callback, {int? rowBytes, int? targetWidth, int? targetHeight, bool allowUpscaling = true}) { - throw UnimplementedError('decodeImageFromPixels not yet implemented'); + void decodeImageFromPixels( + Uint8List pixels, + int width, + int height, + ui.PixelFormat format, + ui.ImageDecoderCallback callback, { + int? rowBytes, + int? targetWidth, + int? targetHeight, + bool allowUpscaling = true + }) { + ui.Size? scaledSize = _scaledSize( + width, + height, + targetWidth, + targetHeight + ); + if (!allowUpscaling && scaledSize != null && + (scaledSize.width > width || scaledSize.height > height)) { + scaledSize = null; + } + final SkwasmImage pixelImage = SkwasmImage.fromPixels( + pixels, + width, + height, + format + ); + if (scaledSize == null) { + callback(pixelImage); + return; + } + + final ui.Rect outputRect = ui.Rect.fromLTWH(0, 0, scaledSize.width, scaledSize.height); + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder, outputRect); + + canvas.drawImageRect( + pixelImage, + ui.Rect.fromLTWH(0, 0, width.toDouble(), width.toDouble()), + outputRect, + ui.Paint(), + ); + final ui.Image finalImage = recorder.endRecording().toImageSync( + scaledSize.width.round(), + scaledSize.height.round() + ); + pixelImage.dispose(); + callback(finalImage); } @override diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/scene_builder.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/scene_builder.dart index f1ce1e0ff1b2b..c1da8c7f5de23 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/scene_builder.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/scene_builder.dart @@ -187,12 +187,10 @@ class SkwasmSceneBuilder implements ui.SceneBuilder { @override void setCheckerboardOffscreenLayers(bool checkerboard) { - // TODO(jacksongardner): implement setCheckerboardOffscreenLayers } @override void setCheckerboardRasterCacheImages(bool checkerboard) { - // TODO(jacksongardner): implement setCheckerboardRasterCacheImages } @override @@ -205,12 +203,10 @@ class SkwasmSceneBuilder implements ui.SceneBuilder { double insetLeft, bool focusable ) { - // TODO(jacksongardner): implement setProperties } @override void setRasterizerTracingThreshold(int frameInterval) { - // TODO(jacksongardner): implement setRasterizerTracingThreshold } @override diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/shaders.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/shaders.dart index ef2bf5ce2f1c5..08a867e13f423 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/shaders.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/shaders.dart @@ -155,8 +155,55 @@ class SkwasmGradient extends SkwasmShader implements ui.Gradient { } } +class SkwasmImageShader extends SkwasmShader implements ui.ImageShader { + SkwasmImageShader._(this.handle); + + factory SkwasmImageShader.imageShader( + SkwasmImage image, + ui.TileMode tmx, + ui.TileMode tmy, + Float64List? matrix4, + ui.FilterQuality? filterQuality, + ) { + if (matrix4 != null) { + return withStackScope((StackScope scope) { + final RawMatrix33 localMatrix = scope.convertMatrix4toSkMatrix(matrix4); + return SkwasmImageShader._(shaderCreateFromImage( + image.handle, + tmx.index, + tmy.index, + (filterQuality ?? ui.FilterQuality.medium).index, + localMatrix, + )); + }); + } else { + return SkwasmImageShader._(shaderCreateFromImage( + image.handle, + tmx.index, + tmy.index, + (filterQuality ?? ui.FilterQuality.medium).index, + nullptr, + )); + } + } + + @override + ShaderHandle handle; + + @override + void dispose() { + super.dispose(); + handle = nullptr; + } +} + class SkwasmFragmentProgram implements ui.FragmentProgram { - SkwasmFragmentProgram._(this.name, this.handle); + SkwasmFragmentProgram._( + this.name, + this.handle, + this.floatUniformCount, + this.childShaderCount, + ); factory SkwasmFragmentProgram.fromBytes(String name, Uint8List bytes) { final ShaderData shaderData = ShaderData.fromBytes(bytes); @@ -171,50 +218,58 @@ class SkwasmFragmentProgram implements ui.FragmentProgram { } final RuntimeEffectHandle handle = runtimeEffectCreate(sourceString); skStringFree(sourceString); - return SkwasmFragmentProgram._(name, handle); + return SkwasmFragmentProgram._( + name, + handle, + shaderData.floatCount, + shaderData.textureCount + ); } - RuntimeEffectHandle handle; - String name; + final RuntimeEffectHandle handle; + final String name; + final int floatUniformCount; + final int childShaderCount; + bool _isDisposed = false; @override - ui.FragmentShader fragmentShader() => - SkwasmFragmentShader(this); + ui.FragmentShader fragmentShader() => SkwasmFragmentShader(this); int get uniformSize => runtimeEffectGetUniformSize(handle); void dispose() { - runtimeEffectDispose(handle); + if (!_isDisposed) { + runtimeEffectDispose(handle); + _isDisposed = true; + } } } class SkwasmFragmentShader extends SkwasmShader implements ui.FragmentShader { SkwasmFragmentShader( - SkwasmFragmentProgram program, { - List? childShaders, - }) : _program = program, + SkwasmFragmentProgram program + ) : _program = program, _uniformData = skDataCreate(program.uniformSize), - _childShaders = childShaders; + _floatUniformCount = program.floatUniformCount, + _childShaders = List.filled(program.childShaderCount, null); @override ShaderHandle get handle { if (_handle == nullptr) { _handle = withStackScope((StackScope s) { Pointer childShaders = nullptr; - final int childCount = _childShaders != null ? _childShaders!.length : 0; - if (childCount != 0) { - childShaders = s.allocPointerArray(childCount) + if (_childShaders.isNotEmpty) { + childShaders = s.allocPointerArray(_childShaders.length) .cast(); - final List shaders = _childShaders!; - for (int i = 0; i < childCount; i++) { - childShaders[i] = shaders[i].handle; + for (int i = 0; i < _childShaders.length; i++) { + childShaders[i] = _childShaders[i]!.handle; } } return shaderCreateRuntimeEffectShader( _program.handle, _uniformData, childShaders, - childCount, + _childShaders.length, ); }); } @@ -224,7 +279,8 @@ class SkwasmFragmentShader extends SkwasmShader implements ui.FragmentShader { ShaderHandle _handle = nullptr; final SkwasmFragmentProgram _program; SkDataHandle _uniformData; - final List? _childShaders; + final int _floatUniformCount; + final List _childShaders; @override void setFloat(int index, double value) { @@ -240,7 +296,20 @@ class SkwasmFragmentShader extends SkwasmShader implements ui.FragmentShader { @override void setImageSampler(int index, ui.Image image) { - // TODO(jacksongardner): implement this when images are implemented + final SkwasmImageShader shader = SkwasmImageShader.imageShader( + image as SkwasmImage, + ui.TileMode.clamp, + ui.TileMode.clamp, + null, + ui.FilterQuality.none, + ); + final SkwasmShader? oldShader = _childShaders[index]; + _childShaders[index] = shader; + oldShader?.dispose(); + + final Pointer dataPointer = skDataGetPointer(_uniformData).cast(); + dataPointer[_floatUniformCount + index * 2] = image.width.toDouble(); + dataPointer[_floatUniformCount + index * 2 + 1] = image.height.toDouble(); } @override diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart index 9e98c8cbf090b..79fa506a9d099 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart @@ -5,8 +5,10 @@ import 'dart:async'; import 'dart:ffi'; import 'dart:js_interop'; +import 'dart:typed_data'; import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; +import 'package:ui/ui.dart' as ui; class SkwasmSurface { factory SkwasmSurface(String canvasQuerySelector) { @@ -22,33 +24,55 @@ class SkwasmSurface { SkwasmSurface._fromHandle(this._handle); final SurfaceHandle _handle; OnRenderCallbackHandle _callbackHandle = nullptr; - final Map> _pendingRenders = >{}; + final Map> _pendingCallbacks = >{}; void _initialize() { _callbackHandle = OnRenderCallbackHandle.fromAddress( skwasmInstance.addFunction( - _onRender.toJS, - 'vi'.toJS + _callbackHandler.toJS, + 'vii'.toJS ).toDart.toInt() ); - surfaceSetOnRenderCallback(_handle, _callbackHandle); + surfaceSetCallbackHandler(_handle, _callbackHandle); } void setSize(int width, int height) => surfaceSetCanvasSize(_handle, width, height); Future renderPicture(SkwasmPicture picture) { - final int renderId = surfaceRenderPicture(_handle, picture.handle); - final Completer completer = Completer(); - _pendingRenders[renderId] = completer; + final int callbackId = surfaceRenderPicture(_handle, picture.handle); + return _registerCallback(callbackId); + } + + Future rasterizeImage(SkwasmImage image, ui.ImageByteFormat format) async { + final int callbackId = surfaceRasterizeImage( + _handle, + image.handle, + format.index, + ); + final int context = await _registerCallback(callbackId); + final SkDataHandle dataHandle = SkDataHandle.fromAddress(context); + final int byteCount = skDataGetSize(dataHandle); + final Pointer dataPointer = skDataGetConstPointer(dataHandle).cast(); + final Uint8List output = Uint8List(byteCount); + for (int i = 0; i < byteCount; i++) { + output[i] = dataPointer[i]; + } + skDataDispose(dataHandle); + return ByteData.sublistView(output); + } + + Future _registerCallback(int callbackId) { + final Completer completer = Completer(); + _pendingCallbacks[callbackId] = completer; return completer.future; } - void _onRender(JSNumber jsRenderId) { - final int renderId = jsRenderId.toDart.toInt(); - final Completer completer = _pendingRenders.remove(renderId)!; - completer.complete(); + void _callbackHandler(JSNumber jsCallbackId, JSNumber jsPointer) { + final int callbackId = jsCallbackId.toDart.toInt(); + final Completer completer = _pendingCallbacks.remove(callbackId)!; + completer.complete(jsPointer.toDart.toInt()); } void dispose() { diff --git a/lib/web_ui/skwasm/BUILD.gn b/lib/web_ui/skwasm/BUILD.gn index dfdb8d369f089..d680dda866ebd 100644 --- a/lib/web_ui/skwasm/BUILD.gn +++ b/lib/web_ui/skwasm/BUILD.gn @@ -12,6 +12,7 @@ wasm_lib("skwasm") { "export.h", "fonts.cpp", "helpers.h", + "image.cpp", "paint.cpp", "path.cpp", "picture.cpp", diff --git a/lib/web_ui/skwasm/canvas.cpp b/lib/web_ui/skwasm/canvas.cpp index bad7efee94ec9..d07295259aaa7 100644 --- a/lib/web_ui/skwasm/canvas.cpp +++ b/lib/web_ui/skwasm/canvas.cpp @@ -29,167 +29,144 @@ constexpr SkScalar kShadowLightXOffset = 0; constexpr SkScalar kShadowLightYOffset = -450; } // namespace -SKWASM_EXPORT void canvas_destroy(CanvasWrapper* wrapper) { - delete wrapper; -} - -SKWASM_EXPORT void canvas_saveLayer(CanvasWrapper* wrapper, +SKWASM_EXPORT void canvas_saveLayer(SkCanvas* canvas, SkRect* rect, SkPaint* paint) { - wrapper->canvas->saveLayer(SkCanvas::SaveLayerRec(rect, paint, 0)); + canvas->saveLayer(SkCanvas::SaveLayerRec(rect, paint, 0)); } -SKWASM_EXPORT void canvas_save(CanvasWrapper* wrapper) { - wrapper->canvas->save(); +SKWASM_EXPORT void canvas_save(SkCanvas* canvas) { + canvas->save(); } -SKWASM_EXPORT void canvas_restore(CanvasWrapper* wrapper) { - wrapper->canvas->restore(); +SKWASM_EXPORT void canvas_restore(SkCanvas* canvas) { + canvas->restore(); } -SKWASM_EXPORT void canvas_restoreToCount(CanvasWrapper* wrapper, int count) { - wrapper->canvas->restoreToCount(count); +SKWASM_EXPORT void canvas_restoreToCount(SkCanvas* canvas, int count) { + canvas->restoreToCount(count); } -SKWASM_EXPORT int canvas_getSaveCount(CanvasWrapper* wrapper) { - return wrapper->canvas->getSaveCount(); +SKWASM_EXPORT int canvas_getSaveCount(SkCanvas* canvas) { + return canvas->getSaveCount(); } -SKWASM_EXPORT void canvas_translate(CanvasWrapper* wrapper, +SKWASM_EXPORT void canvas_translate(SkCanvas* canvas, SkScalar dx, SkScalar dy) { - wrapper->canvas->translate(dx, dy); + canvas->translate(dx, dy); } -SKWASM_EXPORT void canvas_scale(CanvasWrapper* wrapper, - SkScalar sx, - SkScalar sy) { - wrapper->canvas->scale(sx, sy); +SKWASM_EXPORT void canvas_scale(SkCanvas* canvas, SkScalar sx, SkScalar sy) { + canvas->scale(sx, sy); } -SKWASM_EXPORT void canvas_rotate(CanvasWrapper* wrapper, SkScalar degrees) { - wrapper->canvas->rotate(degrees); +SKWASM_EXPORT void canvas_rotate(SkCanvas* canvas, SkScalar degrees) { + canvas->rotate(degrees); } -SKWASM_EXPORT void canvas_skew(CanvasWrapper* wrapper, - SkScalar sx, - SkScalar sy) { - wrapper->canvas->skew(sx, sy); +SKWASM_EXPORT void canvas_skew(SkCanvas* canvas, SkScalar sx, SkScalar sy) { + canvas->skew(sx, sy); } -SKWASM_EXPORT void canvas_transform(CanvasWrapper* wrapper, - const SkM44* matrix44) { - wrapper->canvas->concat(*matrix44); +SKWASM_EXPORT void canvas_transform(SkCanvas* canvas, const SkM44* matrix44) { + canvas->concat(*matrix44); } -SKWASM_EXPORT void canvas_clipRect(CanvasWrapper* wrapper, +SKWASM_EXPORT void canvas_clipRect(SkCanvas* canvas, const SkRect* rect, SkClipOp op, bool antialias) { - wrapper->canvas->clipRect(*rect, op, antialias); + canvas->clipRect(*rect, op, antialias); } -SKWASM_EXPORT void canvas_clipRRect(CanvasWrapper* wrapper, +SKWASM_EXPORT void canvas_clipRRect(SkCanvas* canvas, const SkScalar* rrectValues, bool antialias) { - wrapper->canvas->clipRRect(createRRect(rrectValues), antialias); + canvas->clipRRect(createRRect(rrectValues), antialias); } -SKWASM_EXPORT void canvas_clipPath(CanvasWrapper* wrapper, +SKWASM_EXPORT void canvas_clipPath(SkCanvas* canvas, SkPath* path, bool antialias) { - wrapper->canvas->clipPath(*path, antialias); + canvas->clipPath(*path, antialias); } -SKWASM_EXPORT void canvas_drawColor(CanvasWrapper* wrapper, +SKWASM_EXPORT void canvas_drawColor(SkCanvas* canvas, SkColor color, SkBlendMode blendMode) { - makeCurrent(wrapper->context); - wrapper->canvas->drawColor(color, blendMode); + canvas->drawColor(color, blendMode); } -SKWASM_EXPORT void canvas_drawLine(CanvasWrapper* wrapper, +SKWASM_EXPORT void canvas_drawLine(SkCanvas* canvas, SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2, SkPaint* paint) { - makeCurrent(wrapper->context); - wrapper->canvas->drawLine(x1, y1, x2, y2, *paint); + canvas->drawLine(x1, y1, x2, y2, *paint); } -SKWASM_EXPORT void canvas_drawPaint(CanvasWrapper* wrapper, SkPaint* paint) { - makeCurrent(wrapper->context); - wrapper->canvas->drawPaint(*paint); +SKWASM_EXPORT void canvas_drawPaint(SkCanvas* canvas, SkPaint* paint) { + canvas->drawPaint(*paint); } -SKWASM_EXPORT void canvas_drawRect(CanvasWrapper* wrapper, +SKWASM_EXPORT void canvas_drawRect(SkCanvas* canvas, SkRect* rect, SkPaint* paint) { - makeCurrent(wrapper->context); - wrapper->canvas->drawRect(*rect, *paint); + canvas->drawRect(*rect, *paint); } -SKWASM_EXPORT void canvas_drawRRect(CanvasWrapper* wrapper, +SKWASM_EXPORT void canvas_drawRRect(SkCanvas* canvas, const SkScalar* rrectValues, SkPaint* paint) { - makeCurrent(wrapper->context); - wrapper->canvas->drawRRect(createRRect(rrectValues), *paint); + canvas->drawRRect(createRRect(rrectValues), *paint); } -SKWASM_EXPORT void canvas_drawDRRect(CanvasWrapper* wrapper, +SKWASM_EXPORT void canvas_drawDRRect(SkCanvas* canvas, const SkScalar* outerRrectValues, const SkScalar* innerRrectValues, SkPaint* paint) { - makeCurrent(wrapper->context); - wrapper->canvas->drawDRRect(createRRect(outerRrectValues), - createRRect(innerRrectValues), *paint); + canvas->drawDRRect(createRRect(outerRrectValues), + createRRect(innerRrectValues), *paint); } -SKWASM_EXPORT void canvas_drawOval(CanvasWrapper* wrapper, +SKWASM_EXPORT void canvas_drawOval(SkCanvas* canvas, const SkRect* rect, SkPaint* paint) { - makeCurrent(wrapper->context); - wrapper->canvas->drawOval(*rect, *paint); + canvas->drawOval(*rect, *paint); } -SKWASM_EXPORT void canvas_drawCircle(CanvasWrapper* wrapper, +SKWASM_EXPORT void canvas_drawCircle(SkCanvas* canvas, SkScalar x, SkScalar y, SkScalar radius, SkPaint* paint) { - makeCurrent(wrapper->context); - - wrapper->canvas->drawCircle(x, y, radius, *paint); + canvas->drawCircle(x, y, radius, *paint); } -SKWASM_EXPORT void canvas_drawArc(CanvasWrapper* wrapper, +SKWASM_EXPORT void canvas_drawArc(SkCanvas* canvas, const SkRect* rect, SkScalar startAngleDegrees, SkScalar sweepAngleDegrees, bool useCenter, SkPaint* paint) { - makeCurrent(wrapper->context); - wrapper->canvas->drawArc(*rect, startAngleDegrees, sweepAngleDegrees, - useCenter, *paint); + canvas->drawArc(*rect, startAngleDegrees, sweepAngleDegrees, useCenter, + *paint); } -SKWASM_EXPORT void canvas_drawPath(CanvasWrapper* wrapper, +SKWASM_EXPORT void canvas_drawPath(SkCanvas* canvas, SkPath* path, SkPaint* paint) { - makeCurrent(wrapper->context); - - wrapper->canvas->drawPath(*path, *paint); + canvas->drawPath(*path, *paint); } -SKWASM_EXPORT void canvas_drawShadow(CanvasWrapper* wrapper, +SKWASM_EXPORT void canvas_drawShadow(SkCanvas* canvas, SkPath* path, SkScalar elevation, SkScalar devicePixelRatio, SkColor color, bool transparentOccluder) { - makeCurrent(wrapper->context); - SkColor inAmbient = SkColorSetA(color, kShadowAmbientAlpha * SkColorGetA(color)); SkColor inSpot = SkColorSetA(color, kShadowSpotAlpha * SkColorGetA(color)); @@ -201,38 +178,64 @@ SKWASM_EXPORT void canvas_drawShadow(CanvasWrapper* wrapper, : SkShadowFlags::kNone_ShadowFlag; flags |= SkShadowFlags::kDirectionalLight_ShadowFlag; SkShadowUtils::DrawShadow( - wrapper->canvas, *path, - SkPoint3::Make(0.0f, 0.0f, elevation * devicePixelRatio), + canvas, *path, SkPoint3::Make(0.0f, 0.0f, elevation * devicePixelRatio), SkPoint3::Make(kShadowLightXOffset, kShadowLightYOffset, kShadowLightHeight * devicePixelRatio), devicePixelRatio * kShadowLightRadius, outAmbient, outSpot, flags); } -SKWASM_EXPORT void canvas_drawParagraph(CanvasWrapper* wrapper, +SKWASM_EXPORT void canvas_drawParagraph(SkCanvas* canvas, Paragraph* paragraph, SkScalar x, SkScalar y) { - paragraph->paint(wrapper->canvas, x, y); + paragraph->paint(canvas, x, y); } -SKWASM_EXPORT void canvas_drawPicture(CanvasWrapper* wrapper, - SkPicture* picture) { - makeCurrent(wrapper->context); +SKWASM_EXPORT void canvas_drawPicture(SkCanvas* canvas, SkPicture* picture) { + canvas->drawPicture(picture); +} + +SKWASM_EXPORT void canvas_drawImage(SkCanvas* canvas, + SkImage* image, + SkScalar offsetX, + SkScalar offsetY, + SkPaint* paint, + FilterQuality quality) { + canvas->drawImage(image, offsetX, offsetY, samplingOptionsForQuality(quality), + paint); +} + +SKWASM_EXPORT void canvas_drawImageRect(SkCanvas* canvas, + SkImage* image, + SkRect* sourceRect, + SkRect* destRect, + SkPaint* paint, + FilterQuality quality) { + canvas->drawImageRect(image, *sourceRect, *destRect, + samplingOptionsForQuality(quality), paint, + SkCanvas::kStrict_SrcRectConstraint); +} - wrapper->canvas->drawPicture(picture); +SKWASM_EXPORT void canvas_drawImageNine(SkCanvas* canvas, + SkImage* image, + SkIRect* centerRect, + SkRect* destinationRect, + SkPaint* paint, + FilterQuality quality) { + canvas->drawImageNine(image, *centerRect, *destinationRect, + filterModeForQuality(quality), paint); } -SKWASM_EXPORT void canvas_getTransform(CanvasWrapper* wrapper, - SkM44* outTransform) { - *outTransform = wrapper->canvas->getLocalToDevice(); +SKWASM_EXPORT void canvas_getTransform(SkCanvas* canvas, SkM44* outTransform) { + *outTransform = canvas->getLocalToDevice(); } -SKWASM_EXPORT void canvas_getLocalClipBounds(CanvasWrapper* wrapper, +SKWASM_EXPORT void canvas_getLocalClipBounds(SkCanvas* canvas, SkRect* outRect) { - *outRect = wrapper->canvas->getLocalClipBounds(); + *outRect = canvas->getLocalClipBounds(); } -SKWASM_EXPORT void canvas_getDeviceClipBounds(CanvasWrapper* wrapper, +SKWASM_EXPORT void canvas_getDeviceClipBounds(SkCanvas* canvas, SkIRect* outRect) { - *outRect = wrapper->canvas->getDeviceClipBounds(); + *outRect = canvas->getDeviceClipBounds(); } diff --git a/lib/web_ui/skwasm/data.cpp b/lib/web_ui/skwasm/data.cpp index 9c5e860d6d635..1f1b9b748aadc 100644 --- a/lib/web_ui/skwasm/data.cpp +++ b/lib/web_ui/skwasm/data.cpp @@ -14,6 +14,14 @@ SKWASM_EXPORT void* skData_getPointer(SkData* data) { return data->writable_data(); } +SKWASM_EXPORT const void* skData_getConstPointer(SkData* data) { + return data->data(); +} + +SKWASM_EXPORT size_t skData_getSize(SkData* data) { + return data->size(); +} + SKWASM_EXPORT void skData_dispose(SkData* data) { return data->unref(); } diff --git a/lib/web_ui/skwasm/helpers.h b/lib/web_ui/skwasm/helpers.h index f32055f9e44cc..c30122f6b3ab4 100644 --- a/lib/web_ui/skwasm/helpers.h +++ b/lib/web_ui/skwasm/helpers.h @@ -6,6 +6,7 @@ #include "third_party/skia/include/core/SkMatrix.h" #include "third_party/skia/include/core/SkRRect.h" +#include "third_party/skia/include/core/SkSamplingOptions.h" namespace Skwasm { @@ -23,4 +24,37 @@ inline SkRRect createRRect(const SkScalar* f) { return rr; } +// This needs to be kept in sync with the "FilterQuality" enum in dart:ui +enum class FilterQuality { + none, + low, + medium, + high, +}; + +inline SkFilterMode filterModeForQuality(FilterQuality quality) { + switch (quality) { + case FilterQuality::none: + case FilterQuality::low: + return SkFilterMode::kNearest; + case FilterQuality::medium: + case FilterQuality::high: + return SkFilterMode::kLinear; + } +} + +inline SkSamplingOptions samplingOptionsForQuality(FilterQuality quality) { + switch (quality) { + case FilterQuality::none: + return SkSamplingOptions(SkFilterMode::kNearest, SkMipmapMode::kNone); + case FilterQuality::low: + return SkSamplingOptions(SkFilterMode::kNearest, SkMipmapMode::kNearest); + case FilterQuality::medium: + return SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kLinear); + case FilterQuality::high: + // Cubic equation coefficients recommended by Mitchell & Netravali + // in their paper on cubic interpolation. + return SkSamplingOptions(SkCubicResampler::Mitchell()); + } +} } // namespace Skwasm diff --git a/lib/web_ui/skwasm/image.cpp b/lib/web_ui/skwasm/image.cpp new file mode 100644 index 0000000000000..31c219eb014e9 --- /dev/null +++ b/lib/web_ui/skwasm/image.cpp @@ -0,0 +1,77 @@ +// 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. + +#include "export.h" + +#include "third_party/skia/include/core/SkColorSpace.h" +#include "third_party/skia/include/core/SkData.h" +#include "third_party/skia/include/core/SkImage.h" +#include "third_party/skia/include/core/SkImageInfo.h" +#include "third_party/skia/include/core/SkPicture.h" + +using namespace SkImages; + +enum class PixelFormat { + rgba8888, + bgra8888, + rgbaFloat32, +}; + +SkColorType colorTypeForPixelFormat(PixelFormat format) { + switch (format) { + case PixelFormat::rgba8888: + return SkColorType::kRGBA_8888_SkColorType; + case PixelFormat::bgra8888: + return SkColorType::kBGRA_8888_SkColorType; + case PixelFormat::rgbaFloat32: + return SkColorType::kRGBA_F32_SkColorType; + } +} + +SkAlphaType alphaTypeForPixelFormat(PixelFormat format) { + switch (format) { + case PixelFormat::rgba8888: + case PixelFormat::bgra8888: + return SkAlphaType::kPremul_SkAlphaType; + case PixelFormat::rgbaFloat32: + return SkAlphaType::kUnpremul_SkAlphaType; + } +} + +SKWASM_EXPORT SkImage* image_createFromPicture(SkPicture* picture, + int32_t width, + int32_t height) { + picture->ref(); + return DeferredFromPicture(sk_sp(picture), {width, height}, + nullptr, nullptr, BitDepth::kU8, + SkColorSpace::MakeSRGB()) + .release(); +} + +SKWASM_EXPORT SkImage* image_createFromPixels(SkData* data, + int width, + int height, + PixelFormat pixelFormat, + size_t rowByteCount) { + data->ref(); + return SkImages::RasterFromData( + SkImageInfo::Make(width, height, + colorTypeForPixelFormat(pixelFormat), + alphaTypeForPixelFormat(pixelFormat), + SkColorSpace::MakeSRGB()), + sk_sp(data), rowByteCount) + .release(); +} + +SKWASM_EXPORT void image_dispose(SkImage* image) { + image->unref(); +} + +SKWASM_EXPORT int image_getWidth(SkImage* image) { + return image->width(); +} + +SKWASM_EXPORT int image_getHeight(SkImage* image) { + return image->height(); +} diff --git a/lib/web_ui/skwasm/picture.cpp b/lib/web_ui/skwasm/picture.cpp index c12d7d42fc53b..ec72b4cf68897 100644 --- a/lib/web_ui/skwasm/picture.cpp +++ b/lib/web_ui/skwasm/picture.cpp @@ -19,10 +19,10 @@ SKWASM_EXPORT void pictureRecorder_dispose(SkPictureRecorder* recorder) { delete recorder; } -SKWASM_EXPORT CanvasWrapper* pictureRecorder_beginRecording( +SKWASM_EXPORT SkCanvas* pictureRecorder_beginRecording( SkPictureRecorder* recorder, const SkRect* cullRect) { - return new CanvasWrapper{0, recorder->beginRecording(*cullRect, &bbhFactory)}; + return recorder->beginRecording(*cullRect, &bbhFactory); } SKWASM_EXPORT SkPicture* pictureRecorder_endRecording( diff --git a/lib/web_ui/skwasm/shaders.cpp b/lib/web_ui/skwasm/shaders.cpp index 5a521f8184512..d1afdadaea64b 100644 --- a/lib/web_ui/skwasm/shaders.cpp +++ b/lib/web_ui/skwasm/shaders.cpp @@ -4,6 +4,7 @@ #include "export.h" #include "helpers.h" +#include "third_party/skia/include/core/SkImage.h" #include "third_party/skia/include/effects/SkGradientShader.h" #include "third_party/skia/include/effects/SkRuntimeEffect.h" #include "wrappers.h" @@ -136,3 +137,21 @@ SKWASM_EXPORT SkShader* shader_createRuntimeEffectShader( childPointers.data(), childCount, nullptr) .release(); } + +SKWASM_EXPORT SkShader* shader_createFromImage(SkImage* image, + SkTileMode tileModeX, + SkTileMode tileModeY, + FilterQuality quality, + SkScalar* matrix33) { + if (matrix33) { + SkMatrix localMatrix = createMatrix(matrix33); + return image + ->makeShader(tileModeX, tileModeY, samplingOptionsForQuality(quality), + &localMatrix) + .release(); + } else { + return image + ->makeShader(tileModeX, tileModeY, samplingOptionsForQuality(quality)) + .release(); + } +} diff --git a/lib/web_ui/skwasm/surface.cpp b/lib/web_ui/skwasm/surface.cpp index 3f9f036f53057..4027d37a5f0ad 100644 --- a/lib/web_ui/skwasm/surface.cpp +++ b/lib/web_ui/skwasm/surface.cpp @@ -8,11 +8,13 @@ #include #include #include +#include #include "export.h" #include "third_party/skia/include/core/SkCanvas.h" #include "third_party/skia/include/core/SkColorSpace.h" #include "third_party/skia/include/core/SkPicture.h" #include "third_party/skia/include/core/SkSurface.h" +#include "third_party/skia/include/encode/SkPngEncoder.h" #include "third_party/skia/include/gpu/GrDirectContext.h" #include "third_party/skia/include/gpu/ganesh/SkSurfaceGanesh.h" #include "third_party/skia/include/gpu/gl/GrGLInterface.h" @@ -21,20 +23,38 @@ using namespace Skwasm; -using OnRenderCompleteCallback = void(uint32_t); - namespace { + +// This must be kept in sync with the `ImageByteFormat` enum in dart:ui. +enum class ImageByteFormat { + rawRgba, + rawStraightRgba, + rawUnmodified, + png, +}; + class Surface; void fDispose(Surface* surface); void fSetCanvasSize(Surface* surface, int width, int height); void fRenderPicture(Surface* surface, SkPicture* picture); -void fNotifyRenderComplete(Surface* surface, uint32_t renderId); -void fOnRenderComplete(Surface* surface, uint32_t renderId); +void fNotifyRenderComplete(Surface* surface, uint32_t callbackId); +void fOnRenderComplete(Surface* surface, uint32_t callbackId); +void fRasterizeImage(Surface* surface, + SkImage* image, + ImageByteFormat format, + uint32_t callbackId); +void fOnRasterizeComplete(Surface* surface, + SkData* imageData, + uint32_t callbackId); class Surface { public: + using CallbackHandler = void(uint32_t, void*); + // Main thread only Surface(const char* canvasID) : _canvasID(canvasID) { + assert(emscripten_is_main_browser_thread()); + pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); @@ -51,6 +71,7 @@ class Surface { // Main thread only void dispose() { + assert(emscripten_is_main_browser_thread()); emscripten_dispatch_to_thread(_thread, EM_FUNC_SIG_VI, reinterpret_cast(fDispose), nullptr, this); @@ -58,6 +79,7 @@ class Surface { // Main thread only void setCanvasSize(int width, int height) { + assert(emscripten_is_main_browser_thread()); emscripten_dispatch_to_thread(_thread, EM_FUNC_SIG_VIII, reinterpret_cast(fSetCanvasSize), nullptr, this, width, height); @@ -65,7 +87,8 @@ class Surface { // Main thread only uint32_t renderPicture(SkPicture* picture) { - uint32_t renderId = ++_currentRenderId; + assert(emscripten_is_main_browser_thread()); + uint32_t callbackId = ++_currentCallbackId; picture->ref(); emscripten_dispatch_to_thread(_thread, EM_FUNC_SIG_VII, reinterpret_cast(fRenderPicture), @@ -78,13 +101,24 @@ class Surface { emscripten_dispatch_to_thread( _thread, EM_FUNC_SIG_VII, reinterpret_cast(fNotifyRenderComplete), nullptr, this, - renderId); - return renderId; + callbackId); + return callbackId; + } + + uint32_t rasterizeImage(SkImage* image, ImageByteFormat format) { + uint32_t callbackId = ++_currentCallbackId; + image->ref(); + + emscripten_dispatch_to_thread(_thread, EM_FUNC_SIG_VIIII, + reinterpret_cast(fRasterizeImage), + nullptr, this, image, format, callbackId); + return callbackId; } // Main thread only - void setOnRenderCallback(OnRenderCompleteCallback* callback) { - _onRenderCompleteCallback = callback; + void setCallbackHandler(CallbackHandler* callbackHandler) { + assert(emscripten_is_main_browser_thread()); + _callbackHandler = callbackHandler; } private: @@ -177,20 +211,54 @@ class Surface { _surface->flush(); } + void _rasterizeImage(SkImage* image, + ImageByteFormat format, + uint32_t callbackId) { + sk_sp data; + if (format == ImageByteFormat::png) { + data = SkPngEncoder::Encode(_grContext.get(), image, {}); + } else { + SkAlphaType alphaType = format == ImageByteFormat::rawStraightRgba + ? SkAlphaType::kUnpremul_SkAlphaType + : SkAlphaType::kPremul_SkAlphaType; + SkImageInfo info = SkImageInfo::Make(image->width(), image->height(), + SkColorType::kRGBA_8888_SkColorType, + alphaType, SkColorSpace::MakeSRGB()); + size_t bytesPerRow = 4 * image->width(); + size_t byteSize = info.computeByteSize(bytesPerRow); + data = SkData::MakeUninitialized(byteSize); + uint8_t* pixels = reinterpret_cast(data->writable_data()); + bool success = image->readPixels(_grContext.get(), image->imageInfo(), + pixels, bytesPerRow, 0, 0); + if (!success) { + printf("Failed to read pixels from image!\n"); + data = nullptr; + } + } + emscripten_sync_run_in_main_runtime_thread(EM_FUNC_SIG_VIII, + fOnRasterizeComplete, this, + data.release(), callbackId); + } + + void _onRasterizeComplete(SkData* data, uint32_t callbackId) { + _callbackHandler(callbackId, data); + } + // Worker thread only - void _notifyRenderComplete(uint32_t renderId) { + void _notifyRenderComplete(uint32_t callbackId) { emscripten_sync_run_in_main_runtime_thread( - EM_FUNC_SIG_VII, fOnRenderComplete, this, renderId); + EM_FUNC_SIG_VII, fOnRenderComplete, this, callbackId); } // Main thread only - void _onRenderComplete(uint32_t renderId) { - _onRenderCompleteCallback(renderId); + void _onRenderComplete(uint32_t callbackId) { + assert(emscripten_is_main_browser_thread()); + _callbackHandler(callbackId, nullptr); } std::string _canvasID; - OnRenderCompleteCallback* _onRenderCompleteCallback = nullptr; - uint32_t _currentRenderId = 0; + CallbackHandler* _callbackHandler = nullptr; + uint32_t _currentCallbackId = 0; int _canvasWidth = 0; int _canvasHeight = 0; @@ -207,8 +275,15 @@ class Surface { friend void fDispose(Surface* surface); friend void fSetCanvasSize(Surface* surface, int width, int height); friend void fRenderPicture(Surface* surface, SkPicture* picture); - friend void fNotifyRenderComplete(Surface* surface, uint32_t renderId); - friend void fOnRenderComplete(Surface* surface, uint32_t renderId); + friend void fNotifyRenderComplete(Surface* surface, uint32_t callbackId); + friend void fOnRenderComplete(Surface* surface, uint32_t callbackId); + friend void fRasterizeImage(Surface* surface, + SkImage* image, + ImageByteFormat format, + uint32_t callbackId); + friend void fOnRasterizeComplete(Surface* surface, + SkData* imageData, + uint32_t callbackId); }; void fDispose(Surface* surface) { @@ -224,12 +299,26 @@ void fRenderPicture(Surface* surface, SkPicture* picture) { picture->unref(); } -void fNotifyRenderComplete(Surface* surface, uint32_t renderId) { - surface->_notifyRenderComplete(renderId); +void fNotifyRenderComplete(Surface* surface, uint32_t callbackId) { + surface->_notifyRenderComplete(callbackId); +} + +void fOnRenderComplete(Surface* surface, uint32_t callbackId) { + surface->_onRenderComplete(callbackId); +} + +void fOnRasterizeComplete(Surface* surface, + SkData* imageData, + uint32_t callbackId) { + surface->_onRasterizeComplete(imageData, callbackId); } -void fOnRenderComplete(Surface* surface, uint32_t renderId) { - surface->_onRenderComplete(renderId); +void fRasterizeImage(Surface* surface, + SkImage* image, + ImageByteFormat format, + uint32_t callbackId) { + surface->_rasterizeImage(image, format, callbackId); + image->unref(); } } // namespace @@ -237,10 +326,10 @@ SKWASM_EXPORT Surface* surface_createFromCanvas(const char* canvasID) { return new Surface(canvasID); } -SKWASM_EXPORT void surface_setOnRenderCallback( +SKWASM_EXPORT void surface_setCallbackHandler( Surface* surface, - OnRenderCompleteCallback* callback) { - surface->setOnRenderCallback(callback); + Surface::CallbackHandler* callbackHandler) { + surface->setCallbackHandler(callbackHandler); } SKWASM_EXPORT void surface_destroy(Surface* surface) { @@ -257,3 +346,9 @@ SKWASM_EXPORT uint32_t surface_renderPicture(Surface* surface, SkPicture* picture) { return surface->renderPicture(picture); } + +SKWASM_EXPORT uint32_t surface_rasterizeImage(Surface* surface, + SkImage* image, + ImageByteFormat format) { + return surface->rasterizeImage(image, format); +} diff --git a/lib/web_ui/skwasm/wrappers.h b/lib/web_ui/skwasm/wrappers.h index ffb356bba9c67..db4b31cf86301 100644 --- a/lib/web_ui/skwasm/wrappers.h +++ b/lib/web_ui/skwasm/wrappers.h @@ -18,11 +18,6 @@ struct SurfaceWrapper { sk_sp surface; }; -struct CanvasWrapper { - EMSCRIPTEN_WEBGL_CONTEXT_HANDLE context; - SkCanvas* canvas; -}; - inline void makeCurrent(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE handle) { if (!handle) return; diff --git a/lib/web_ui/test/common/fake_asset_manager.dart b/lib/web_ui/test/common/fake_asset_manager.dart index 2aea72c30315d..8b62c1ead6854 100644 --- a/lib/web_ui/test/common/fake_asset_manager.dart +++ b/lib/web_ui/test/common/fake_asset_manager.dart @@ -18,11 +18,11 @@ class FakeAssetManager implements AssetManager { @override Future load(String assetKey) async { - final ByteData? data = _assetMap[assetKey]; - if (data == null) { + final ByteData? assetData = await _currentScope?.getAssetData(assetKey); + if (assetData == null) { throw HttpFetchNoPayloadError(assetKey, status: 404); } - return data; + return assetData; } @override @@ -55,12 +55,7 @@ class FakeAssetManager implements AssetManager { _currentScope = scope._parent; } - void setAsset(String assetKey, ByteData assetData) { - _assetMap[assetKey] = assetData; - } - FakeAssetScope? _currentScope; - final Map _assetMap = {}; } class FakeAssetScope { diff --git a/lib/web_ui/test/ui/fragment_shader_test.dart b/lib/web_ui/test/ui/fragment_shader_test.dart index da69653d0a237..fc456dc68a794 100644 --- a/lib/web_ui/test/ui/fragment_shader_test.dart +++ b/lib/web_ui/test/ui/fragment_shader_test.dart @@ -48,11 +48,20 @@ Future testMain() async { const ui.Rect region = ui.Rect.fromLTWH(0, 0, 300, 300); - test('fragment shader', () async { - fakeAssetManager.setAsset( + late FakeAssetScope assetScope; + setUp(() { + assetScope = fakeAssetManager.pushAssetScope(); + assetScope.setAsset( 'voronoi_shader', Uint8List.fromList(utf8.encode(kVoronoiShaderSksl)).buffer.asByteData() ); + }); + + tearDown(() { + fakeAssetManager.popAssetScope(assetScope); + }); + + test('fragment shader', () async { final ui.FragmentProgram program = await renderer.createFragmentProgram('voronoi_shader'); final ui.FragmentShader shader = program.fragmentShader(); diff --git a/lib/web_ui/test/ui/image_golden_test.dart b/lib/web_ui/test/ui/image_golden_test.dart new file mode 100644 index 0000000000000..71884e6eebe6e --- /dev/null +++ b/lib/web_ui/test/ui/image_golden_test.dart @@ -0,0 +1,291 @@ +// 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:async'; +import 'dart:convert'; +import 'dart:math'; +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' as ui; +import 'package:web_engine_tester/golden_tester.dart'; + +import '../common/fake_asset_manager.dart'; +import '../common/test_initialization.dart'; +import 'utils.dart'; + +const String kGlitchShaderSksl = r''' +{ + "sksl": "// This SkSL shader is autogenerated by spirv-cross.\n\nfloat4 flutter_FragCoord;\n\nuniform vec2 uResolution;\nuniform float uTime;\nuniform shader uTex;\nuniform half2 uTex_size;\n\nvec4 oColor;\n\nvec2 FLT_flutter_local_FlutterFragCoord()\n{\n return flutter_FragCoord.xy;\n}\n\nfloat FLT_flutter_local_cubicPulse(float c, float w, inout float x)\n{\n x = abs(x - c);\n if (x > w)\n {\n return 0.0;\n }\n x /= w;\n return 1.0 - ((x * x) * (3.0 - (2.0 * x)));\n}\n\nfloat FLT_flutter_local_twoSin(inout float x)\n{\n x = (6.4899997711181640625 * x) - 0.64999997615814208984375;\n float t = ((-0.699999988079071044921875) * sin(6.80000019073486328125 * x)) + (1.39999997615814208984375 * sin(2.900000095367431640625 * x));\n t = (t / 4.099999904632568359375) + 0.5;\n return t;\n}\n\nfloat FLT_flutter_local_hash_1d(float v)\n{\n float u = 50.0 * sin(v * 3000.0);\n return (2.0 * fract((2.0 * u) * u)) - 1.0;\n}\n\nvoid FLT_main()\n{\n vec2 uv = vec2(FLT_flutter_local_FlutterFragCoord()) / uResolution;\n float param = 0.5;\n float param_1 = 0.0500000007450580596923828125;\n float param_2 = fract(uTime / 4.0);\n float _127 = FLT_flutter_local_cubicPulse(param, param_1, param_2);\n float t_2 = _127;\n float param_3 = fract(uTime / 5.0);\n float _134 = FLT_flutter_local_twoSin(param_3);\n float t_1 = _134;\n float glitchScale = mix(0.0, 8.0, t_1 + t_2);\n float aberrationSize = mix(0.0, 5.0, t_1 + t_2);\n float param_4 = uv.y;\n float h = FLT_flutter_local_hash_1d(param_4);\n float hs = sign(h);\n h = max(h, 0.0);\n h *= h;\n h = floor(h + float(0.5)) * hs;\n uv += (vec2(h * glitchScale, 0.0) / uResolution);\n vec2 redOffset = vec2(aberrationSize, 0.0) / uResolution;\n vec2 greenOffset = vec2(0.0) / uResolution;\n vec2 blueOffset = vec2(-aberrationSize, 0.0) / uResolution;\n vec2 redUv = uv + redOffset;\n vec2 greenUv = uv + greenOffset;\n vec2 blueUv = uv + blueOffset;\n vec2 ra = uTex.eval(uTex_size * redUv).xw;\n vec2 ga = uTex.eval(uTex_size * greenUv).yw;\n vec2 ba = uTex.eval(uTex_size * blueUv).zw;\n ra.x /= ra.y;\n ga.x /= ga.y;\n ba.x /= ba.y;\n float alpha = max(ra.y, max(ga.y, ba.y));\n oColor = vec4(ra.x, ga.x, ba.x, 1.0) * alpha;\n}\n\nhalf4 main(float2 iFragCoord)\n{\n flutter_FragCoord = float4(iFragCoord, 0, 0);\n FLT_main();\n return oColor;\n}\n", + "stage": 1, + "target_platform": 2, + "uniforms": [ + { + "array_elements": 0, + "bit_width": 32, + "columns": 1, + "location": 0, + "name": "uResolution", + "rows": 2, + "type": 10 + }, + { + "array_elements": 0, + "bit_width": 32, + "columns": 1, + "location": 1, + "name": "uTime", + "rows": 1, + "type": 10 + }, + { + "array_elements": 0, + "bit_width": 0, + "columns": 1, + "location": 2, + "name": "uTex", + "rows": 1, + "type": 12 + } + ] +} +'''; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +Future testMain() async { + setUpUnitTests( + setUpTestViewDimensions: false, + ); + + late FakeAssetScope assetScope; + setUp(() { + assetScope = fakeAssetManager.pushAssetScope(); + assetScope.setAsset( + 'glitch_shader', + Uint8List.fromList(utf8.encode(kGlitchShaderSksl)).buffer.asByteData() + ); + }); + + tearDown(() { + fakeAssetManager.popAssetScope(assetScope); + }); + + const ui.Rect drawRegion = ui.Rect.fromLTWH(0, 0, 300, 300); + const ui.Rect imageRegion = ui.Rect.fromLTWH(0, 0, 150, 150); + + // Emits a set of rendering tests for an image + // `imageGenerator` should produce an image that is 150x150 pixels. + void emitImageTests(String name, Future Function() imageGenerator) { + group(name, () { + test('drawImage', () async { + final ui.Image image = await imageGenerator(); + expect(image.width, 150); + expect(image.height, 150); + + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder, drawRegion); + canvas.drawImage(image, const ui.Offset(100, 100), ui.Paint()); + + await drawPictureUsingCurrentRenderer(recorder.endRecording()); + + await matchGoldenFile('${name}_canvas_drawImage.png', region: drawRegion); + }); + + test('drawImageRect', () async { + final ui.Image image = await imageGenerator(); + expect(image.width, 150); + expect(image.height, 150); + + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder, drawRegion); + canvas.drawImageRect( + image, + const ui.Rect.fromLTRB(50, 50, 100, 100), + const ui.Rect.fromLTRB(100, 100, 150, 150), + ui.Paint() + ); + + await drawPictureUsingCurrentRenderer(recorder.endRecording()); + + await matchGoldenFile('${name}_canvas_drawImageRect.png', region: drawRegion); + }); + + test('drawImageNine', () async { + final ui.Image image = await imageGenerator(); + expect(image.width, 150); + expect(image.height, 150); + + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder, drawRegion); + canvas.drawImageNine( + image, + const ui.Rect.fromLTRB(50, 50, 100, 100), + drawRegion, + ui.Paint() + ); + + await drawPictureUsingCurrentRenderer(recorder.endRecording()); + + await matchGoldenFile('${name}_canvas_drawImageNine.png', region: drawRegion); + }); + + test('image_shader_cubic_rotated', () async { + final ui.Image image = await imageGenerator(); + expect(image.width, 150); + expect(image.height, 150); + + final Float64List matrix = Matrix4.rotationZ(pi / 6).toFloat64(); + final ui.ImageShader shader = ui.ImageShader( + image, + ui.TileMode.repeated, + ui.TileMode.repeated, + matrix, + filterQuality: ui.FilterQuality.high, + ); + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder, drawRegion); + canvas.drawOval( + const ui.Rect.fromLTRB(0, 50, 300, 250), + ui.Paint()..shader = shader + ); + + await drawPictureUsingCurrentRenderer(recorder.endRecording()); + await matchGoldenFile('${name}_image_shader_cubic_rotated.png', region: drawRegion); + }); + + test('fragment_shader_sampler', () async { + final ui.Image image = await imageGenerator(); + expect(image.width, 150); + expect(image.height, 150); + + final ui.FragmentProgram program = await renderer.createFragmentProgram('glitch_shader'); + final ui.FragmentShader shader = program.fragmentShader(); + + // Resolution + shader.setFloat(0, 300); + shader.setFloat(1, 300); + + // Time + shader.setFloat(2, 2); + + // Image + shader.setImageSampler(0, image); + + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder, drawRegion); + canvas.drawCircle(const ui.Offset(150, 150), 100, ui.Paint()..shader = shader); + + await drawPictureUsingCurrentRenderer(recorder.endRecording()); + + await matchGoldenFile('${name}_fragment_shader_sampler.png', region: drawRegion); + }, skip: isHtml); // HTML doesn't support fragment shaders + + test('toByteData_rgba', () async { + final ui.Image image = await imageGenerator(); + expect(image.width, 150); + expect(image.height, 150); + + final ByteData? rgbaData = await image.toByteData(); + expect(rgbaData, isNotNull); + expect(rgbaData!.lengthInBytes, isNonZero); + }); + + test('toByteData_rgba', () async { + final ui.Image image = await imageGenerator(); + expect(image.width, 150); + expect(image.height, 150); + + final ByteData? pngData = await image.toByteData(format: ui.ImageByteFormat.png); + expect(pngData, isNotNull); + expect(pngData!.lengthInBytes, isNonZero); + }, skip: isHtml); // https://github.com/flutter/flutter/issues/126611 + }); + } + + emitImageTests('picture_toImage', () { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder, imageRegion); + for (int y = 0; y < 15; y++) { + for (int x = 0; x < 15; x++) { + final ui.Offset center = ui.Offset(x * 10 + 5, y * 10 + 5); + final ui.Color color = ui.Color.fromRGBO( + (center.dx * 256 / 150).round(), + (center.dy * 256 / 150).round(), 0, 1); + canvas.drawCircle(center, 5, ui.Paint()..color = color); + } + } + return recorder.endRecording().toImage(150, 150); + }); + + Uint8List generatePixelData( + int width, + int height, + ui.Color Function(double, double) generator + ) { + final Uint8List data = Uint8List(width * height * 4); + int outputIndex = 0; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final ui.Color pixelColor = generator( + (2.0 * x / width) - 1.0, + (2.0 * y / height) - 1.0, + ); + data[outputIndex++] = pixelColor.red; + data[outputIndex++] = pixelColor.green; + data[outputIndex++] = pixelColor.blue; + data[outputIndex++] = pixelColor.alpha; + } + } + return data; + } + + emitImageTests('decodeImageFromPixels_unscaled', () { + final Uint8List pixels = generatePixelData(150, 150, (double x, double y) { + final double r = sqrt(x * x + y * y); + final double theta = atan2(x, y); + return ui.Color.fromRGBO( + (255 * (sin(r * 10.0) + 1.0) / 2.0).round(), + (255 * (sin(theta * 10.0) + 1.0) / 2.0).round(), + 0, + 1, + ); + }); + final Completer completer = Completer(); + ui.decodeImageFromPixels(pixels, 150, 150, ui.PixelFormat.rgba8888, completer.complete); + return completer.future; + }); + + // https://github.com/flutter/flutter/issues/126603 + if (!isHtml) { + emitImageTests('decodeImageFromPixels_scaled', () { + final Uint8List pixels = generatePixelData(50, 50, (double x, double y) { + final double r = sqrt(x * x + y * y); + final double theta = atan2(x, y); + return ui.Color.fromRGBO( + (255 * (sin(r * 10.0) + 1.0) / 2.0).round(), + (255 * (sin(theta * 10.0) + 1.0) / 2.0).round(), + 0, + 1, + ); + }); + final Completer completer = Completer(); + ui.decodeImageFromPixels( + pixels, + 50, + 50, + ui.PixelFormat.rgba8888, + completer.complete, + targetWidth: 150, + targetHeight: 150, + ); + return completer.future; + }); + } +}