diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvas_pool.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvas_pool.dart index 08e8aca8217..188cf7d0ec2 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvas_pool.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvas_pool.dart @@ -398,7 +398,13 @@ class CanvasPool extends _SaveStackTracking { /// Returns a "data://" URI containing a representation of the image in this /// canvas in PNG format. - String toDataUrl() => _canvas?.toDataURL() ?? ''; + String toDataUrl() { + if (_canvas == null) { + _createCanvas(); + } + return _canvas!.toDataURL(); + } + @override void save() { @@ -773,6 +779,10 @@ class CanvasPool extends _SaveStackTracking { contextHandle.paintPath(style, path.fillType); } + void drawImage(DomHTMLImageElement element, ui.Offset p) { + context.drawImage(element, p.dx, p.dy); + } + /// Draws a shadow for a Path representing the given material elevation. void drawShadow(ui.Path path, ui.Color color, double elevation, bool transparentOccluder) { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart index 3bebbd94b53..e0c70ebb703 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart @@ -366,27 +366,35 @@ class BitmapCanvas extends EngineCanvas { /// - Pictures typically have large rect/rounded rectangles as background /// prefer DOM if canvas has not been allocated yet. /// - bool _useDomForRenderingFill(SurfacePaintData paint) => - _renderStrategy.isInsideSvgFilterTree || - (_preserveImageData == false && _contains3dTransform) || + bool _useDomForRenderingFill(SurfacePaintData paint) { + if (_preserveImageData) { + return false; + } + return _renderStrategy.isInsideSvgFilterTree || + _contains3dTransform || (_childOverdraw && !_canvasPool.hasCanvas && paint.maskFilter == null && paint.shader == null && paint.style != ui.PaintingStyle.stroke); + } /// Same as [_useDomForRenderingFill] but allows stroke as well. /// /// DOM canvas is generated for simple strokes using borders. - bool _useDomForRenderingFillAndStroke(SurfacePaintData paint) => - _renderStrategy.isInsideSvgFilterTree || - (_preserveImageData == false && _contains3dTransform) || + bool _useDomForRenderingFillAndStroke(SurfacePaintData paint) { + if (_preserveImageData) { + return false; + } + return _renderStrategy.isInsideSvgFilterTree || + _contains3dTransform || ((_childOverdraw || _renderStrategy.hasImageElements || _renderStrategy.hasParagraphs) && !_canvasPool.hasCanvas && paint.maskFilter == null && paint.shader == null); + } @override void drawColor(ui.Color color, ui.BlendMode blendMode) { @@ -626,7 +634,9 @@ class BitmapCanvas extends EngineCanvas { _applyTargetSize( imageElement, image.width.toDouble(), image.height.toDouble()); } - _closeCanvas(); + if (!_preserveImageData) { + _closeCanvas(); + } } DomHTMLImageElement _reuseOrCreateImage(HtmlImage htmlImage) { @@ -668,26 +678,40 @@ class BitmapCanvas extends EngineCanvas { imgElement = _reuseOrCreateImage(htmlImage); } imgElement.style.mixBlendMode = blendModeToCssMixBlendMode(blendMode) ?? ''; - if (_canvasPool.isClipped) { - // Reset width/height since they may have been previously set. - imgElement.style..removeProperty('width')..removeProperty('height'); - final List clipElements = _clipContent( - _canvasPool.clipStack!, imgElement, p, _canvasPool.currentTransform); - for (final DomElement clipElement in clipElements) { - rootElement.append(clipElement); - _children.add(clipElement); - } + if (_preserveImageData && imgElement is DomHTMLImageElement) { + // If we're preserving image data, we have to actually draw the image + // element onto the canvas. + // TODO(jacksongardner): Make this actually work with color filters. + setUpPaint(paint, null); + _canvasPool.drawImage(imgElement, p); + tearDownPaint(); } else { - final String cssTransform = float64ListToCssTransform( - transformWithOffset(_canvasPool.currentTransform, p).storage); - imgElement.style - ..transformOrigin = '0 0 0' - ..transform = cssTransform + if (_canvasPool.isClipped) { // Reset width/height since they may have been previously set. - ..removeProperty('width') - ..removeProperty('height'); - rootElement.append(imgElement); - _children.add(imgElement); + imgElement.style + ..removeProperty('width') + ..removeProperty('height'); + final List clipElements = _clipContent( + _canvasPool.clipStack!, + imgElement, + p, + _canvasPool.currentTransform); + for (final DomElement clipElement in clipElements) { + rootElement.append(clipElement); + _children.add(clipElement); + } + } else { + final String cssTransform = float64ListToCssTransform( + transformWithOffset(_canvasPool.currentTransform, p).storage); + imgElement.style + ..transformOrigin = '0 0 0' + ..transform = cssTransform + // Reset width/height since they may have been previously set. + ..removeProperty('width') + ..removeProperty('height'); + rootElement.append(imgElement); + _children.add(imgElement); + } } return imgElement; } diff --git a/engine/src/flutter/lib/web_ui/test/html/drawing/canvas_draw_image_golden_test.dart b/engine/src/flutter/lib/web_ui/test/html/drawing/canvas_draw_image_golden_test.dart index 9783958f4c2..530b549cdbc 100644 --- a/engine/src/flutter/lib/web_ui/test/html/drawing/canvas_draw_image_golden_test.dart +++ b/engine/src/flutter/lib/web_ui/test/html/drawing/canvas_draw_image_golden_test.dart @@ -2,8 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; import 'dart:js_util' as js_util; import 'dart:math' as math; +import 'dart:typed_data'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; @@ -36,6 +38,28 @@ Future testMain() async { region: const Rect.fromLTWH(0, 0, 500, 500)); }); + test('Images from raw data are composited when picture is roundtripped through toImage', () async { + final Uint8List imageData = base64Decode(base64PngData); + final Codec codec = await instantiateImageCodec(imageData); + final FrameInfo frameInfo = await codec.getNextFrame(); + + const Rect bounds = Rect.fromLTRB(0, 0, 400, 300); + final EnginePictureRecorder recorder = EnginePictureRecorder(); + final RecordingCanvas scratchCanvas = recorder.beginRecording(bounds); + scratchCanvas.save(); + scratchCanvas.drawImage(frameInfo.image, Offset.zero, SurfacePaint()); + scratchCanvas.restore(); + final Picture picture = recorder.endRecording(); + final Image image = await picture.toImage(400, 300); + + final RecordingCanvas rc = RecordingCanvas(bounds); + rc.save(); + rc.drawImage(image, Offset.zero, SurfacePaint()); + rc.restore(); + await canvasScreenshot(rc, 'draw_raw_image', + region: const Rect.fromLTWH(0, 0, 500, 500)); + }); + test('Paints image with transform', () async { final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); @@ -592,9 +616,8 @@ Future testMain() async { setupPerspective: true); }); } - // 9 slice test image that has a shiny/glass look. -const String base64ImageData = 'data:image/png;base64,iVBORw0KGgoAAAANSUh' +const String base64PngData = 'iVBORw0KGgoAAAANSUh' 'EUgAAADwAAAA8CAYAAAA6/NlyAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPo' 'AAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAApGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQA' 'AARoABQAAAAEAAABKARsABQAAAAEAAABSATEAAgAAACAAAABah2kABAAAAAEAAAB6AAAAAAAA' @@ -721,11 +744,12 @@ const String base64ImageData = 'data:image/png;base64,iVBORw0KGgoAAAANSUh' 'Z2x0yEXi0PeqFO87AiFGCkemvOKYkSF/6yS1vnLOWTaHN/VcmnSWt0eHThELBHBiCGisGra' 'SI5l6R3qD0q05ZR4TNVHES7bnltEgcg6JF3uVlyFsBpdB+lzgdRTMHeejneZR0H1BrnKECH' '7GyVy1BAmcsr17WxYs78QNqQF4bppFXqX9BBqIrzmcExwueASjAFzlaWncpqEpJCXVc7wv' - 'Nj7eT/BbztCaofk+k0AAAAAElFTkSuQmCC'; + 'Nj7eT/BbztCaofk+k0AAAAAyBMj8AAAAAElFTkSuQmCC'; +const String base64ImageUrl = 'data:image/png;base64,$base64PngData'; HtmlImage createNineSliceImage() { return HtmlImage( - createDomHTMLImageElement()..src = base64ImageData, + createDomHTMLImageElement()..src = base64ImageUrl, 60, 60, );