Draw images directly into the canvas when compositing for data purposes (flutter/engine#36058)

This commit is contained in:
Jackson Gardner 2022-09-12 12:38:02 -07:00 committed by GitHub
parent 3508545994
commit 3699be7e55
3 changed files with 88 additions and 30 deletions

View File

@ -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) {

View File

@ -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<DomElement> 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<DomElement> 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;
}

View File

@ -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<void> 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<void> 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,
);