未验证 提交 8c855dba 编写于 作者: Y Yegor 提交者: GitHub

fix text blurriness in HTML-rendered text (#15649)

* fix text blurriness in HTML-rendered text
上级 09d892b3
......@@ -3,18 +3,33 @@
// found in the LICENSE file.
import 'dart:io' as io;
import 'package:image/image.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart';
import 'environment.dart';
import 'utils.dart';
/// How to compares pixels within the image.
///
/// Keep this enum in sync with the one defined in `golden_tester.dart`.
enum PixelComparison {
/// Allows minor blur and anti-aliasing differences by comparing a 3x3 grid
/// surrounding the pixel rather than direct 1:1 comparison.
fuzzy,
/// Compares one pixel at a time.
///
/// Anti-aliasing or blur will result in higher diff rate.
precise,
}
void main(List<String> args) {
final io.File fileA = io.File(args[0]);
final io.File fileB = io.File(args[1]);
final Image imageA = decodeNamedImage(fileA.readAsBytesSync(), 'a.png');
final Image imageB = decodeNamedImage(fileB.readAsBytesSync(), 'b.png');
final ImageDiff diff = ImageDiff(golden: imageA, other: imageB);
final ImageDiff diff = ImageDiff(golden: imageA, other: imageB, pixelComparison: PixelComparison.fuzzy);
print('Diff: ${(diff.rate * 100).toStringAsFixed(4)}%');
}
......@@ -28,6 +43,9 @@ class ImageDiff {
/// The image being compared
final Image other;
/// Algorithm used for comparing pixels.
final PixelComparison pixelComparison;
/// The output of the comparison
/// Pixels in the output image can have 3 different colors depending on the comparison
/// between golden pixels and other pixels:
......@@ -40,7 +58,11 @@ class ImageDiff {
/// This gets set to 1 (100% difference) when golden and other aren't the same size.
double get rate => _wrongPixels / _pixelCount;
ImageDiff({ Image this.golden, Image this.other }) {
ImageDiff({
@required this.golden,
@required this.other,
@required this.pixelComparison,
}) {
_computeDiff();
}
......@@ -84,26 +106,44 @@ class ImageDiff {
return values.reduce((a, b) => a + b) ~/ values.length;
}
/// Reads the RGBA values of the average of the 3x3 box of pixels centered at [x] and [y].
static List<int> _getFuzzyRgb(Image image, int x, int y) {
final List<int> pixels = <int>[
_reflectedPixel(image, x - 1, y - 1),
_reflectedPixel(image, x - 1, y),
_reflectedPixel(image, x - 1, y + 1),
_reflectedPixel(image, x, y - 1),
_reflectedPixel(image, x, y),
_reflectedPixel(image, x, y + 1),
_reflectedPixel(image, x + 1, y - 1),
_reflectedPixel(image, x + 1, y),
_reflectedPixel(image, x + 1, y + 1),
];
return <int>[
_average(pixels.map((p) => getRed(p))),
_average(pixels.map((p) => getGreen(p))),
_average(pixels.map((p) => getBlue(p))),
];
/// The value of the pixel at [x] and [y] coordinates.
///
/// If [pixelComparison] is [PixelComparison.precise], reads the RGB value of
/// the pixel.
///
/// If [pixelComparison] is [PixelComparison.fuzzy], reads the RGB values of
/// the average of the 3x3 box of pixels centered at [x] and [y].
List<int> _getPixelRgbForComparison(Image image, int x, int y) {
switch (pixelComparison) {
case PixelComparison.fuzzy:
final List<int> pixels = <int>[
_reflectedPixel(image, x - 1, y - 1),
_reflectedPixel(image, x - 1, y),
_reflectedPixel(image, x - 1, y + 1),
_reflectedPixel(image, x, y - 1),
_reflectedPixel(image, x, y),
_reflectedPixel(image, x, y + 1),
_reflectedPixel(image, x + 1, y - 1),
_reflectedPixel(image, x + 1, y),
_reflectedPixel(image, x + 1, y + 1),
];
return <int>[
_average(pixels.map((p) => getRed(p))),
_average(pixels.map((p) => getGreen(p))),
_average(pixels.map((p) => getBlue(p))),
];
case PixelComparison.precise:
final int pixel = image.getPixel(x, y);
return <int>[
getRed(pixel),
getGreen(pixel),
getBlue(pixel),
];
default:
throw 'Unrecognized pixel comparison value: ${pixelComparison}';
}
}
void _computeDiff() {
......@@ -117,10 +157,11 @@ class ImageDiff {
for(int y = 0; y < goldenHeight; y++) {
for (int x = 0; x < goldenWidth; x++) {
final bool isExactlySame = golden.getPixel(x, y) == other.getPixel(x, y);
final List<int> goldenPixel = _getFuzzyRgb(golden, x, y);
final List<int> otherPixel = _getFuzzyRgb(other, x, y);
final List<int> goldenPixel = _getPixelRgbForComparison(golden, x, y);
final List<int> otherPixel = _getPixelRgbForComparison(other, x, y);
final double colorDistance = Color.distance(goldenPixel, otherPixel, false) / _maxTheoreticalColorDistance;
if (isExactlySame || colorDistance < _kColorDistanceThreshold) {
final bool isFuzzySame = colorDistance < _kColorDistanceThreshold;
if (isExactlySame || isFuzzySame) {
diff.setPixel(x, y, _colorOk);
} else {
final int goldenLuminance = getLuminanceRgb(goldenPixel[0], goldenPixel[1], goldenPixel[2]);
......
repository: https://github.com/flutter/goldens.git
revision: c0032eeb9f9f064234991b8b5ddc15f714a53cf5
revision: b20fee88d8917af269e8073910c31aa373f8a188
......@@ -154,16 +154,18 @@ class BrowserPlatform extends PlatformPlugin {
final Map<String, dynamic> requestData = json.decode(payload);
final String filename = requestData['filename'];
final bool write = requestData['write'];
final double maxDiffRate = requestData['maxdiffrate'];
final double maxDiffRate = requestData.containsKey('maxdiffrate')
? requestData['maxdiffrate'].toDouble() // can be parsed as either int or double
: kMaxDiffRateFailure;
final Map<String, dynamic> region = requestData['region'];
final String result = await _diffScreenshot(
filename, write, maxDiffRate ?? kMaxDiffRateFailure, region);
final PixelComparison pixelComparison = PixelComparison.values.firstWhere((value) => value.toString() == requestData['pixelComparison']);
final String result = await _diffScreenshot(filename, write, maxDiffRate, region, pixelComparison);
return shelf.Response.ok(json.encode(result));
}
Future<String> _diffScreenshot(
String filename, bool write, double maxDiffRateFailure,
[Map<String, dynamic> region]) async {
Map<String, dynamic> region, PixelComparison pixelComparison) async {
if (doUpdateScreenshotGoldens) {
write = true;
}
......@@ -246,8 +248,10 @@ To automatically create this file call matchGoldenFile('$filename', write: true)
}
ImageDiff diff = ImageDiff(
golden: decodeNamedImage(file.readAsBytesSync(), filename),
other: screenshot);
golden: decodeNamedImage(file.readAsBytesSync(), filename),
other: screenshot,
pixelComparison: pixelComparison,
);
if (diff.rate > 0) {
// Images are different, so produce some debug info
......
......@@ -85,6 +85,13 @@ class BitmapCanvas extends EngineCanvas {
// with Widgets but CustomPainter(s) can hit this code path.
bool _childOverdraw = false;
/// Forces text to be drawn using HTML rather than bitmap.
///
/// Use this for tests only.
set debugChildOverdraw(bool value) {
_childOverdraw = value;
}
/// Allocates a canvas with enough memory to paint a picture within the given
/// [bounds].
///
......@@ -506,11 +513,10 @@ class BitmapCanvas extends EngineCanvas {
_children.add(clipElement);
}
} else {
final String cssTransform = matrix4ToCssTransform3d(
transformWithOffset(_canvasPool.currentTransform, offset));
paragraphElement.style
..transformOrigin = '0 0 0'
..transform = cssTransform;
setElementTransform(
paragraphElement,
transformWithOffset(_canvasPool.currentTransform, offset).storage,
);
rootElement.append(paragraphElement);
}
_children.add(paragraphElement);
......@@ -680,6 +686,7 @@ List<html.Element> _clipContent(List<_SaveClipEntry> clipStack,
for (int clipIndex = 0; clipIndex < len; clipIndex++) {
final _SaveClipEntry entry = clipStack[clipIndex];
final html.HtmlElement newElement = html.DivElement();
newElement.style.position = 'absolute';
if (root == null) {
root = newElement;
} else {
......@@ -695,10 +702,9 @@ List<html.Element> _clipContent(List<_SaveClipEntry> clipStack,
..translate(clipOffsetX, clipOffsetY);
curElement.style
..overflow = 'hidden'
..transform = matrix4ToCssTransform3d(newClipTransform)
..transformOrigin = '0 0 0'
..width = '${rect.right - clipOffsetX}px'
..height = '${rect.bottom - clipOffsetY}px';
setElementTransform(curElement, newClipTransform.storage);
} else if (entry.rrect != null) {
final ui.RRect roundRect = entry.rrect;
final String borderRadius =
......@@ -711,10 +717,9 @@ List<html.Element> _clipContent(List<_SaveClipEntry> clipStack,
curElement.style
..borderRadius = borderRadius
..overflow = 'hidden'
..transform = matrix4ToCssTransform3d(newClipTransform)
..transformOrigin = '0 0 0'
..width = '${roundRect.right - clipOffsetX}px'
..height = '${roundRect.bottom - clipOffsetY}px';
setElementTransform(curElement, newClipTransform.storage);
} else if (entry.path != null) {
curElement.style.transform = matrix4ToCssTransform(newClipTransform);
final String svgClipPath = _pathToSvgClipPath(entry.path);
......@@ -731,28 +736,24 @@ List<html.Element> _clipContent(List<_SaveClipEntry> clipStack,
// TODO(flutter_web): When we have more than a single clip element,
// reduce number of div nodes by merging (multiplying transforms).
final html.Element reverseTransformDiv = html.DivElement();
reverseTransformDiv.style
..transform =
_cssTransformAtOffset(newClipTransform.clone()..invert(), 0, 0)
..transformOrigin = '0 0 0';
reverseTransformDiv.style.position = 'absolute';
setElementTransform(
reverseTransformDiv,
(newClipTransform.clone()..invert()).storage,
);
curElement.append(reverseTransformDiv);
curElement = reverseTransformDiv;
}
root.style.position = 'absolute';
domRenderer.append(curElement, content);
content.style
..transformOrigin = '0 0 0'
..transform = _cssTransformAtOffset(currentTransform, offset.dx, offset.dy);
setElementTransform(
content,
transformWithOffset(currentTransform, offset).storage,
);
return <html.Element>[root]..addAll(clipDefs);
}
String _cssTransformAtOffset(
Matrix4 transform, double offsetX, double offsetY) {
return matrix4ToCssTransform3d(
transformWithOffset(transform, ui.Offset(offsetX, offsetY)));
}
String _maskFilterToCss(ui.MaskFilter maskFilter) {
if (maskFilter == null) return 'none';
return 'blur(${maskFilter.webOnlySigma}px)';
......
......@@ -262,10 +262,10 @@ html.Element _drawParagraphElement(
..width = '${paragraph.width}px';
if (transform != null) {
paragraphStyle
..transformOrigin = '0 0 0'
..transform =
matrix4ToCssTransform3d(transformWithOffset(transform, offset));
setElementTransform(
paragraphElement,
transformWithOffset(transform, offset).storage,
);
}
final ParagraphGeometricStyle style = paragraph._geometricStyle;
......
......@@ -79,15 +79,17 @@ class PersistedClipRect extends PersistedContainerSurface
@override
void apply() {
rootElement.style
..transform = 'translate(${rect.left}px, ${rect.top}px)'
..left = '${rect.left}px'
..top = '${rect.top}px'
..width = '${rect.right - rect.left}px'
..height = '${rect.bottom - rect.top}px';
// Translate the child container in the opposite direction to compensate for
// the shift in the coordinate system introduced by the translation of the
// rootElement. Clipping in Flutter has no effect on the coordinate system.
childContainer.style.transform =
'translate(${-rect.left}px, ${-rect.top}px)';
childContainer.style
..left = '${-rect.left}px'
..top = '${-rect.top}px';
}
@override
......@@ -126,7 +128,8 @@ class PersistedClipRRect extends PersistedContainerSurface
@override
void apply() {
rootElement.style
..transform = 'translate(${rrect.left}px, ${rrect.top}px)'
..left = '${rrect.left}px'
..top = '${rrect.top}px'
..width = '${rrect.width}px'
..height = '${rrect.height}px'
..borderTopLeftRadius = '${rrect.tlRadiusX}px'
......@@ -137,8 +140,9 @@ class PersistedClipRRect extends PersistedContainerSurface
// Translate the child container in the opposite direction to compensate for
// the shift in the coordinate system introduced by the translation of the
// rootElement. Clipping in Flutter has no effect on the coordinate system.
childContainer.style.transform =
'translate(${-rrect.left}px, ${-rrect.top}px)';
childContainer.style
..left = '${-rrect.left}px'
..top = '${-rrect.top}px';
}
@override
......@@ -218,12 +222,14 @@ class PersistedPhysicalShape extends PersistedContainerSurface
'${roundRect.brRadiusX}px ${roundRect.blRadiusX}px';
final html.CssStyleDeclaration style = rootElement.style;
style
..transform = 'translate(${roundRect.left}px, ${roundRect.top}px)'
..left = '${roundRect.left}px'
..top = '${roundRect.top}px'
..width = '${roundRect.width}px'
..height = '${roundRect.height}px'
..borderRadius = borderRadius;
childContainer.style.transform =
'translate(${-roundRect.left}px, ${-roundRect.top}px)';
childContainer.style
..left = '${-roundRect.left}px'
..top = '${-roundRect.top}px';
if (clipBehavior != ui.Clip.none) {
style.overflow = 'hidden';
}
......@@ -233,12 +239,14 @@ class PersistedPhysicalShape extends PersistedContainerSurface
if (rect != null) {
final html.CssStyleDeclaration style = rootElement.style;
style
..transform = 'translate(${rect.left}px, ${rect.top}px)'
..left = '${rect.left}px'
..top = '${rect.top}px'
..width = '${rect.width}px'
..height = '${rect.height}px'
..borderRadius = '';
childContainer.style.transform =
'translate(${-rect.left}px, ${-rect.top}px)';
childContainer.style
..left = '${-rect.left}px'
..top = '${-rect.top}px';
if (clipBehavior != ui.Clip.none) {
style.overflow = 'hidden';
}
......@@ -254,11 +262,14 @@ class PersistedPhysicalShape extends PersistedContainerSurface
final double left = ellipse.x - rx;
final double top = ellipse.y - ry;
style
..transform = 'translate(${left}px, ${top}px)'
..left = '${left}px'
..top = '${top}px'
..width = '${rx * 2}px'
..height = '${ry * 2}px'
..borderRadius = borderRadius;
childContainer.style.transform = 'translate(${-left}px, ${-top}px)';
childContainer.style
..left = '${-left}px'
..top = '${-top}px';
if (clipBehavior != ui.Clip.none) {
style.overflow = 'hidden';
}
......@@ -281,12 +292,14 @@ class PersistedPhysicalShape extends PersistedContainerSurface
final html.CssStyleDeclaration rootElementStyle = rootElement.style;
rootElementStyle
..overflow = ''
..transform = 'translate(${bounds.left}px, ${bounds.top}px)'
..left = '${bounds.left}px'
..top = '${bounds.top}px'
..width = '${bounds.width}px'
..height = '${bounds.height}px'
..borderRadius = '';
childContainer.style.transform =
'translate(${-bounds.left}px, ${-bounds.top}px)';
childContainer.style
..left = '-${bounds.left}px'
..top = '-${bounds.top}px';
}
@override
......@@ -305,6 +318,8 @@ class PersistedPhysicalShape extends PersistedContainerSurface
// rect/rrect and arbitrary path.
final html.CssStyleDeclaration style = rootElement.style;
style.transform = '';
style.left = '';
style.top = '';
style.borderRadius = '';
domRenderer.setElementStyle(rootElement, 'clip-path', '');
domRenderer.setElementStyle(rootElement, '-webkit-clip-path', '');
......
......@@ -59,11 +59,71 @@ String matrix4ToCssTransform3d(Matrix4 matrix) {
return float64ListToCssTransform3d(matrix.storage);
}
/// Returns `true` is the [matrix] describes an identity transformation.
bool isIdentityFloat64ListTransform(Float64List matrix) {
/// Applies a transform to the [element].
///
/// There are several ways to transform an element. This function chooses
/// between CSS "transform", "left", "top" or no transform, depending on the
/// [matrix4] and the current device's screen properties. This function
/// attempts to avoid issues with text blurriness on low pixel density screens.
///
/// See also:
/// * https://github.com/flutter/flutter/issues/32274
/// * https://bugs.chromium.org/p/chromium/issues/detail?id=1040222
void setElementTransform(html.Element element, Float64List matrix4) {
final TransformKind transformKind = transformKindOf(matrix4);
// On low device-pixel ratio screens using CSS "transform" causes text blurriness
// at least on Blink browsers. We therefore prefer using CSS "left" and "top" instead.
final bool isHighDevicePixelRatioScreen = EngineWindow.browserDevicePixelRatio > 1.0;
if (transformKind == TransformKind.complex || isHighDevicePixelRatioScreen) {
final String cssTransform = float64ListToCssTransform3d(matrix4);
element.style
..transformOrigin = '0 0 0'
..transform = cssTransform
..top = null
..left = null;
} else if (transformKind == TransformKind.translation2d) {
final double ty = matrix4[13];
final double tx = matrix4[12];
element.style
..transformOrigin = null
..transform = null
..left = '${tx}px'
..top = '${ty}px';
} else {
assert(transformKind == TransformKind.identity);
element.style
..transformOrigin = null
..transform = null
..left = null
..top = null;
}
}
/// The kind of effect a transform matrix performs.
enum TransformKind {
/// No effect.
identity,
/// A translation along either X or Y axes, or both.
translation2d,
/// All other kinds of transforms.
complex,
}
/// Detects the kind of transform the [matrix] performs.
TransformKind transformKindOf(Float64List matrix) {
assert(matrix.length == 16);
final Float64List m = matrix;
return m[0] == 1.0 &&
final double ty = m[13];
final double tx = m[12];
// If matrix contains scaling, rotation, z translation or
// perspective transform, it is not considered simple.
final bool isSimpleTransform =
m[0] == 1.0 &&
m[1] == 0.0 &&
m[2] == 0.0 &&
m[3] == 0.0 &&
......@@ -75,10 +135,26 @@ bool isIdentityFloat64ListTransform(Float64List matrix) {
m[9] == 0.0 &&
m[10] == 1.0 &&
m[11] == 0.0 &&
m[12] == 0.0 &&
m[13] == 0.0 &&
m[14] == 0.0 &&
// m[12] - x translation is simple
// m[13] - y translation is simple
m[14] == 0.0 && // z translation is NOT simple
m[15] == 1.0;
if (!isSimpleTransform) {
return TransformKind.complex;
}
if (ty != 0.0 || tx != 0.0) {
return TransformKind.translation2d;
}
return TransformKind.identity;
}
/// Returns `true` is the [matrix] describes an identity transformation.
bool isIdentityFloat64ListTransform(Float64List matrix) {
assert(matrix.length == 16);
return transformKindOf(matrix) == TransformKind.identity;
}
/// Converts [matrix] to CSS transform value.
......
// 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:typed_data';
import 'package:ui/src/engine.dart';
import 'package:test/test.dart';
final Float64List identityTransform = Matrix4.identity().storage;
final Float64List xTranslation = (Matrix4.identity()..translate(10)).storage;
final Float64List yTranslation = (Matrix4.identity()..translate(0, 10)).storage;
final Float64List zTranslation = (Matrix4.identity()..translate(0, 0, 10)).storage;
void main() {
test('transformKindOf and isIdentityFloat64ListTransform identify matrix kind', () {
expect(transformKindOf(identityTransform), TransformKind.identity);
expect(isIdentityFloat64ListTransform(identityTransform), isTrue);
expect(transformKindOf(xTranslation), TransformKind.translation2d);
expect(isIdentityFloat64ListTransform(xTranslation), isFalse);
expect(transformKindOf(yTranslation), TransformKind.translation2d);
expect(isIdentityFloat64ListTransform(yTranslation), isFalse);
expect(transformKindOf(zTranslation), TransformKind.complex);
expect(isIdentityFloat64ListTransform(zTranslation), isFalse);
});
}
......@@ -11,17 +11,25 @@ import 'package:test/test.dart';
import 'package:web_engine_tester/golden_tester.dart';
void main() async {
final Rect region = Rect.fromLTWH(8, 8, 500, 100); // Compensate for old scuba tester padding
final Rect region = Rect.fromLTWH(0, 0, 500, 100);
BitmapCanvas canvas;
setUp(() {
html.document.body.style.transform = 'translate(10px, 10px)';
void appendToScene() {
// Create a <flt-scene> element to make sure our CSS reset applies correctly.
final html.Element testScene = html.Element.tag('flt-scene');
testScene.append(canvas.rootElement);
html.document.querySelector('flt-scene-host').append(testScene);
}
setUp(() async {
await webOnlyInitializePlatform();
webOnlyFontCollection.debugRegisterTestFonts();
await webOnlyFontCollection.ensureFontsLoaded();
});
tearDown(() {
html.document.body.style.transform = 'none';
canvas.rootElement.remove();
html.document.querySelector('flt-scene').remove();
});
/// Draws several lines, some aligned precisely with the pixel grid, and some
......@@ -40,6 +48,8 @@ void main() async {
final SurfacePaintData fillPaint =
(SurfacePaint()..style = PaintingStyle.fill).paintData;
canvas.translate(10, 10);
canvas.drawRect(
const Rect.fromLTWH(0, 0, 40, 40),
linePaint,
......@@ -66,7 +76,7 @@ void main() async {
drawMisalignedLines(canvas);
html.document.body.append(canvas.rootElement);
appendToScene();
await matchGoldenFile('misaligned_pixels_in_canvas_test.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
......@@ -81,7 +91,7 @@ void main() async {
drawMisalignedLines(canvas);
html.document.body.append(canvas.rootElement);
appendToScene();
await matchGoldenFile('misaligned_canvas_test.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
......@@ -92,7 +102,7 @@ void main() async {
canvas.translate(25, 25);
canvas.drawColor(const Color.fromRGBO(0, 255, 0, 1.0), BlendMode.src);
html.document.body.append(canvas.rootElement);
appendToScene();
await matchGoldenFile('bitmap_canvas_fills_color_when_transformed.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
......@@ -105,8 +115,61 @@ void main() async {
..color = const Color.fromRGBO(0, 255, 0, 1.0)
..style = PaintingStyle.fill);
html.document.body.append(canvas.rootElement);
appendToScene();
await matchGoldenFile('bitmap_canvas_fills_paint_when_transformed.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
// This test reproduces text blurriness when two pieces of text appear inside
// two nested clips:
//
// ┌───────────────────────┐
// │ text in outer clip │
// │ ┌────────────────────┐│
// │ │ text in inner clip ││
// │ └────────────────────┘│
// └───────────────────────┘
//
// This test clips using canvas. See a similar test in `compositing_golden_test.dart`,
// which clips using layers.
//
// More details: https://github.com/flutter/flutter/issues/32274
test('renders clipped DOM text with high quality', () async {
final Paragraph paragraph =
(ParagraphBuilder(ParagraphStyle(fontFamily: 'Roboto'))..addText('Am I blurry?')).build();
paragraph.layout(const ParagraphConstraints(width: 1000));
final Rect canvasSize = Rect.fromLTRB(
0,
0,
paragraph.maxIntrinsicWidth + 16,
2 * paragraph.height + 32,
);
final Rect outerClip =
Rect.fromLTRB(0.5, 0.5, canvasSize.right, canvasSize.bottom);
final Rect innerClip = Rect.fromLTRB(0.5, canvasSize.bottom / 2 + 0.5,
canvasSize.right, canvasSize.bottom);
canvas = BitmapCanvas(canvasSize);
canvas.debugChildOverdraw = true;
canvas.clipRect(outerClip);
canvas.drawParagraph(paragraph, const Offset(8.5, 8.5));
canvas.clipRect(innerClip);
canvas.drawParagraph(paragraph, Offset(8.5, 8.5 + innerClip.top));
expect(
canvas.rootElement.querySelectorAll('p').map<String>((e) => e.innerText).toList(),
<String>['Am I blurry?', 'Am I blurry?'],
reason: 'Expected to render text using HTML',
);
appendToScene();
await matchGoldenFile(
'bitmap_canvas_draws_high_quality_text.png',
region: canvasSize,
maxDiffRate: 0.0,
pixelComparison: PixelComparison.precise,
);
}, timeout: const Timeout(Duration(seconds: 10)), testOn: 'chrome');
}
......@@ -15,13 +15,16 @@ import 'package:web_engine_tester/golden_tester.dart';
final Rect region = Rect.fromLTWH(0, 0, 500, 100);
void main() async {
debugShowClipLayers = true;
setUp(() {
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('pushClipRect', () async {
......@@ -54,7 +57,8 @@ void main() async {
html.document.body.append(builder.build().webOnlyRootElement);
await matchGoldenFile('compositing_clip_rect_with_offset_and_transform.png', region: region);
await matchGoldenFile('compositing_clip_rect_with_offset_and_transform.png',
region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
test('pushClipRRect', () async {
......@@ -95,7 +99,8 @@ void main() async {
html.document.body.append(builder.build().webOnlyRootElement);
await matchGoldenFile('compositing_shifted_physical_shape_clip.png', region: region);
await matchGoldenFile('compositing_shifted_physical_shape_clip.png',
region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
test('pushImageFilter', () async {
......@@ -235,7 +240,8 @@ void _testCullRectComputation() {
builder.pop(); // pushClipRect
html.document.body.append(builder.build().webOnlyRootElement);
await matchGoldenFile('compositing_cull_rect_fills_layer_clip.png', region: region);
await matchGoldenFile('compositing_cull_rect_fills_layer_clip.png',
region: region);
final PersistedStandardPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, const Rect.fromLTRB(40, 40, 70, 70));
......@@ -263,7 +269,9 @@ void _testCullRectComputation() {
builder.pop(); // pushClipRect
html.document.body.append(builder.build().webOnlyRootElement);
await matchGoldenFile('compositing_cull_rect_intersects_clip_and_paint_bounds.png', region: region);
await matchGoldenFile(
'compositing_cull_rect_intersects_clip_and_paint_bounds.png',
region: region);
final PersistedStandardPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, const Rect.fromLTRB(50, 40, 70, 70));
......@@ -293,7 +301,8 @@ void _testCullRectComputation() {
builder.pop(); // pushClipRect
html.document.body.append(builder.build().webOnlyRootElement);
await matchGoldenFile('compositing_cull_rect_offset_inside_layer_clip.png', region: region);
await matchGoldenFile('compositing_cull_rect_offset_inside_layer_clip.png',
region: region);
final PersistedStandardPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect,
......@@ -493,7 +502,8 @@ void _testCullRectComputation() {
await matchGoldenFile('compositing_3d_rotate1.png', region: region);
final PersistedStandardPicture picture = enumeratePictures().single; // ignore: unused_local_variable
// ignore: unused_local_variable
final PersistedStandardPicture picture = enumeratePictures().single;
// TODO(https://github.com/flutter/flutter/issues/40395):
// Needs ability to set iframe to 500,100 size. Current screen seems to be 500,500.
// expect(
......@@ -504,6 +514,91 @@ void _testCullRectComputation() {
// -140, -140, screenWidth - 360.0, screenHeight + 40.0)),
// );
}, timeout: const Timeout(Duration(seconds: 10)));
// This test reproduces text blurriness when two pieces of text appear inside
// two nested clips:
//
// ┌───────────────────────┐
// │ text in outer clip │
// │ ┌────────────────────┐│
// │ │ text in inner clip ││
// │ └────────────────────┘│
// └───────────────────────┘
//
// This test clips using layers. See a similar test in `canvas_golden_test.dart`,
// which clips using canvas.
//
// More details: https://github.com/flutter/flutter/issues/32274
test(
'renders clipped text with high quality',
() async {
// To reproduce blurriness we need real clipping.
debugShowClipLayers = false;
final Paragraph paragraph =
(ParagraphBuilder(ParagraphStyle(fontFamily: 'Roboto'))..addText('Am I blurry?')).build();
paragraph.layout(const ParagraphConstraints(width: 1000));
final Rect canvasSize = Rect.fromLTRB(
0,
0,
paragraph.maxIntrinsicWidth + 16,
2 * paragraph.height + 32,
);
final Rect outerClip =
Rect.fromLTRB(0.5, 0.5, canvasSize.right, canvasSize.bottom);
final Rect innerClip = Rect.fromLTRB(0.5, canvasSize.bottom / 2 + 0.5,
canvasSize.right, canvasSize.bottom);
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushClipRect(outerClip);
{
final EnginePictureRecorder recorder = PictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(outerClip);
canvas.drawParagraph(paragraph, const Offset(8.5, 8.5));
final Picture picture = recorder.endRecording();
expect(canvas.hasArbitraryPaint, false);
builder.addPicture(
Offset.zero,
picture,
);
}
builder.pushClipRect(innerClip);
{
final EnginePictureRecorder recorder = PictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(innerClip);
canvas.drawParagraph(paragraph, Offset(8.5, 8.5 + innerClip.top));
final Picture picture = recorder.endRecording();
expect(canvas.hasArbitraryPaint, false);
builder.addPicture(
Offset.zero,
picture,
);
}
builder.pop(); // inner clip
builder.pop(); // outer clip
final html.Element sceneElement = builder.build().webOnlyRootElement;
expect(
sceneElement.querySelectorAll('p').map<String>((e) => e.innerText).toList(),
<String>['Am I blurry?', 'Am I blurry?'],
reason: 'Expected to render text using HTML',
);
html.document.body.append(sceneElement);
await matchGoldenFile(
'compositing_draw_high_quality_text.png',
region: canvasSize,
maxDiffRate: 0.0,
pixelComparison: PixelComparison.precise,
);
},
timeout: const Timeout(Duration(seconds: 10)),
testOn: 'chrome',
);
}
void _drawTestPicture(SceneBuilder builder) {
......
......@@ -20,9 +20,36 @@ Future<dynamic> _callScreenshotServer(dynamic requestData) async {
return json.decode(request.responseText);
}
/// How to compare pixels within the image.
///
/// Keep this enum in sync with the one defined in `goldens.dart`.
enum PixelComparison {
/// Allows minor blur and anti-aliasing differences by comparing a 3x3 grid
/// surrounding the pixel rather than direct 1:1 comparison.
fuzzy,
/// Compares one pixel at a time.
///
/// Anti-aliasing or blur will result in higher diff rate.
precise,
}
/// Attempts to match the current browser state with the screenshot [filename].
///
/// If [write] is true, will overwrite the golden file and fail the test. Use
/// it to update golden files.
///
/// If [region] is not null, the golden will only include the part contained by
/// the rectangle.
///
/// [maxDiffRate] specifies the tolerance to the number of non-matching pixels
/// before the test is considered as failing. If [maxDiffRate] is null, applies
/// a default value defined in `test_platform.dart`.
///
/// [pixelComparison] determines the algorithm used to compare pixels. Uses
/// fuzzy comparison by default.
Future<void> matchGoldenFile(String filename,
{bool write = false, Rect region = null, double maxDiffRate = null}) async {
{bool write = false, Rect region = null, double maxDiffRate = null, PixelComparison pixelComparison = PixelComparison.fuzzy}) async {
Map<String, dynamic> serverParams = <String, dynamic>{
'filename': filename,
'write': write,
......@@ -33,7 +60,8 @@ Future<void> matchGoldenFile(String filename,
'y': region.top,
'width': region.width,
'height': region.height
}
},
'pixelComparison': pixelComparison.toString(),
};
if (maxDiffRate != null) {
serverParams['maxdiffrate'] = maxDiffRate;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册