Commit 3ec049f5 authored by Teresa Kang's avatar Teresa Kang Committed by Commit Bot

Fix Incorrect Shadow Blur

When a pattern (with a transparent background) is used to fill or stroke
a rect, the ShadowBlur is incorrectly applied to the rect mask instead
of the pattern itself.

This is fixed by setting the last param of BaseRenderingContext2D::
Draw() to kNonOpaqueImage when the rect is drawn with a pattern.

Bug: 1020183
Change-Id: I62ae4e298185eb5b7699c2dbf4b9ad962939e464
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2219416Reviewed-by: default avatarFernando Serboncini <fserb@chromium.org>
Reviewed-by: default avatarAaron Krajeski <aaronhk@chromium.org>
Reviewed-by: default avatarJuanmi Huertas <juanmihd@chromium.org>
Commit-Queue: Teresa Kang <teresakang@google.com>
Cr-Commit-Position: refs/heads/master@{#775121}
parent 572d079f
......@@ -643,7 +643,10 @@ void BaseRenderingContext2D::DrawPathInternal(
{ c->drawPath(sk_path, *flags); },
[](const SkIRect& rect) // overdraw test lambda
{ return false; },
bounds, paint_type);
bounds, paint_type,
GetState().HasPattern(paint_type)
? CanvasRenderingContext2DState::kNonOpaqueImage
: CanvasRenderingContext2DState::kNoImage);
}
static SkPathFillType ParseWinding(const String& winding_rule_string) {
......@@ -700,15 +703,20 @@ void BaseRenderingContext2D::fillRect(double x,
// pattern was unaccelerated is because it was not possible to hold that image
// in an accelerated texture - that is, into the GPU). That's why we disable
// the acceleration to be sure that it will work.
if (IsAccelerated() && GetState().HasPattern() &&
!GetState().PatternIsAccelerated())
if (IsAccelerated() &&
GetState().HasPattern(CanvasRenderingContext2DState::kFillPaintType) &&
!GetState().PatternIsAccelerated(
CanvasRenderingContext2DState::kFillPaintType))
DisableAcceleration();
SkRect rect = SkRect::MakeXYWH(fx, fy, fwidth, fheight);
Draw([&rect](cc::PaintCanvas* c, const PaintFlags* flags) // draw lambda
{ c->drawRect(rect, *flags); },
[&rect, this](const SkIRect& clip_bounds) // overdraw test lambda
{ return RectContainsTransformedRect(rect, clip_bounds); },
rect, CanvasRenderingContext2DState::kFillPaintType);
rect, CanvasRenderingContext2DState::kFillPaintType,
GetState().HasPattern(CanvasRenderingContext2DState::kFillPaintType)
? CanvasRenderingContext2DState::kNonOpaqueImage
: CanvasRenderingContext2DState::kNoImage);
}
static void StrokeRectOnCanvas(const FloatRect& rect,
......@@ -756,7 +764,10 @@ void BaseRenderingContext2D::strokeRect(double x,
{ StrokeRectOnCanvas(rect, c, flags); },
[](const SkIRect& clip_bounds) // overdraw test lambda
{ return false; },
bounds, CanvasRenderingContext2DState::kStrokePaintType);
bounds, CanvasRenderingContext2DState::kStrokePaintType,
GetState().HasPattern(CanvasRenderingContext2DState::kStrokePaintType)
? CanvasRenderingContext2DState::kNonOpaqueImage
: CanvasRenderingContext2DState::kNoImage);
}
void BaseRenderingContext2D::ClipInternal(const Path& path,
......
......@@ -337,8 +337,7 @@ class MODULES_EXPORT BaseRenderingContext2D : public GarbageCollectedMixin,
const ContainsFunc&,
const SkRect& bounds,
CanvasRenderingContext2DState::PaintType,
CanvasRenderingContext2DState::ImageType =
CanvasRenderingContext2DState::kNoImage);
CanvasRenderingContext2DState::ImageType);
void InflateStrokeRect(FloatRect&) const;
......@@ -392,6 +391,19 @@ class MODULES_EXPORT BaseRenderingContext2D : public GarbageCollectedMixin,
bool ShouldDrawImageAntialiased(const FloatRect& dest_rect) const;
// When the canvas is stroked or filled with a pattern, which is assumed to
// have a transparent background, the shadow needs to be applied with
// DropShadowPaintFilter for kNonOpaqueImageType
// Used in Draw and CompositedDraw to avoid the shadow offset being modified
// by the transformation matrix
bool ShouldUseDropShadowPaintFilter(
CanvasRenderingContext2DState::PaintType paint_type,
CanvasRenderingContext2DState::ImageType image_type) const {
return (paint_type == CanvasRenderingContext2DState::kFillPaintType ||
paint_type == CanvasRenderingContext2DState::kStrokePaintType) &&
image_type == CanvasRenderingContext2DState::kNonOpaqueImage;
}
void DrawPathInternal(const Path&,
CanvasRenderingContext2DState::PaintType,
SkPathFillType = SkPathFillType::kWinding);
......@@ -466,7 +478,9 @@ void BaseRenderingContext2D::Draw(
return;
if (IsFullCanvasCompositeMode(GetState().GlobalComposite()) ||
StateHasFilter()) {
StateHasFilter() ||
(GetState().ShouldDrawShadows() &&
ShouldUseDropShadowPaintFilter(paint_type, image_type))) {
CompositedDraw(draw_func, GetPaintCanvas(), paint_type, image_type);
DidDraw(clip_bounds);
} else if (GetState().GlobalComposite() == SkBlendMode::kSrc) {
......@@ -495,8 +509,11 @@ void BaseRenderingContext2D::CompositedDraw(
cc::PaintCanvas* c,
CanvasRenderingContext2DState::PaintType paint_type,
CanvasRenderingContext2DState::ImageType image_type) {
sk_sp<PaintFilter> filter = StateGetFilter();
DCHECK(IsFullCanvasCompositeMode(GetState().GlobalComposite()) || filter);
sk_sp<PaintFilter> canvas_filter = StateGetFilter();
DCHECK(IsFullCanvasCompositeMode(GetState().GlobalComposite()) ||
canvas_filter ||
(GetState().ShouldDrawShadows() &&
ShouldUseDropShadowPaintFilter(paint_type, image_type)));
SkMatrix ctm = c->getTotalMatrix();
c->setMatrix(SkMatrix::I());
PaintFlags composite_flags;
......@@ -507,13 +524,14 @@ void BaseRenderingContext2D::CompositedDraw(
*GetState().GetFlags(paint_type, kDrawShadowOnly, image_type);
int save_count = c->getSaveCount();
c->save();
if (filter) {
if (canvas_filter ||
ShouldUseDropShadowPaintFilter(paint_type, image_type)) {
PaintFlags foreground_flags =
*GetState().GetFlags(paint_type, kDrawForegroundOnly, image_type);
shadow_flags.setImageFilter(sk_make_sp<ComposePaintFilter>(
sk_make_sp<ComposePaintFilter>(foreground_flags.getImageFilter(),
shadow_flags.getImageFilter()),
filter));
canvas_filter));
// Saving the shadow layer before setting the matrix, so the shadow offset
// does not get modified by the transformation matrix
c->saveLayer(nullptr, &shadow_flags);
......@@ -529,7 +547,7 @@ void BaseRenderingContext2D::CompositedDraw(
c->restoreToCount(save_count);
}
composite_flags.setImageFilter(std::move(filter));
composite_flags.setImageFilter(std::move(canvas_filter));
c->saveLayer(nullptr, &composite_flags);
PaintFlags foreground_flags =
*GetState().GetFlags(paint_type, kDrawForegroundOnly, image_type);
......
......@@ -946,7 +946,7 @@ void CanvasRenderingContext2D::DrawTextInternal(
},
[](const SkIRect& rect) // overdraw test lambda
{ return false; },
bounds, paint_type);
bounds, paint_type, CanvasRenderingContext2DState::kNoImage);
}
const Font& CanvasRenderingContext2D::AccessFont() {
......
......@@ -638,15 +638,16 @@ const PaintFlags* CanvasRenderingContext2DState::GetFlags(
return flags;
}
bool CanvasRenderingContext2DState::HasPattern() const {
return FillStyle() && FillStyle()->GetCanvasPattern() &&
FillStyle()->GetCanvasPattern()->GetPattern();
bool CanvasRenderingContext2DState::HasPattern(PaintType paint_type) const {
return Style(paint_type) && Style(paint_type)->GetCanvasPattern() &&
Style(paint_type)->GetCanvasPattern()->GetPattern();
}
// Only to be used if the CanvasRenderingContext2DState has Pattern
bool CanvasRenderingContext2DState::PatternIsAccelerated() const {
DCHECK(HasPattern());
return FillStyle()->GetCanvasPattern()->GetPattern()->IsTextureBacked();
bool CanvasRenderingContext2DState::PatternIsAccelerated(
PaintType paint_type) const {
DCHECK(HasPattern(paint_type));
return Style(paint_type)->GetCanvasPattern()->GetPattern()->IsTextureBacked();
}
} // namespace blink
......@@ -110,12 +110,16 @@ class CanvasRenderingContext2DState final
void SetFillStyle(CanvasStyle*);
CanvasStyle* FillStyle() const { return fill_style_.Get(); }
// Prefer to use Style() over StrokeStyle() and FillStyle()
// if properties of CanvasStyle are concerned
CanvasStyle* Style(PaintType) const;
bool HasPattern() const;
// Check the pattern in StrokeStyle or FillStyle depending on the PaintType
bool HasPattern(PaintType) const;
// Only to be used if the CanvasRenderingContext2DState has Pattern
bool PatternIsAccelerated() const;
// Pattern is in either StrokeStyle or FillStyle depending on the PaintType
bool PatternIsAccelerated(PaintType) const;
enum Direction { kDirectionInherit, kDirectionRTL, kDirectionLTR };
......
......@@ -583,7 +583,7 @@ void OffscreenCanvasRenderingContext2D::DrawTextInternal(
},
[](const SkIRect& rect) // overdraw test lambda
{ return false; },
bounds, paint_type);
bounds, paint_type, CanvasRenderingContext2DState::kNoImage);
paint_canvas->restoreToCount(save_count);
ValidateStateStack();
}
......
......@@ -64,25 +64,25 @@ function testPixelShadow(pixel, reference, alphaApprox) {
testScenarios =
[
['TestAlphaShadow 1', ctx.getImageData(400, 150, 1, 1).data, [ 0, 0, 0, 0, 0], 0],
['TestAlphaShadow 1', ctx.getImageData(400, 150, 1, 1).data, [ 0, 0, 0, 0], 0],
['TestAlphaShadow 2', ctx.getImageData(400, 75, 1, 1).data, [ 255, 0, 0, 64], 15],
['TestAlphaShadow 3', ctx.getImageData(400, 225, 1, 1).data, [ 255, 0, 0, 64], 15],
['TestAlphaShadow 4', ctx.getImageData(325, 150, 1, 1).data, [ 255, 0, 0, 64], 15],
['TestAlphaShadow 5', ctx.getImageData(475, 150, 1, 1).data, [ 255, 0, 0, 64], 15],
['TestBlurryShadow 1', ctx.getImageData(400, 400, 1, 1).data, [ 0, 0, 0, 0, 0], 0],
['TestBlurryShadow 1', ctx.getImageData(400, 400, 1, 1).data, [ 0, 0, 0, 0], 0],
['TestBlurryShadow 2', ctx.getImageData(400, 300, 1, 1).data, [ 255, 0, 0, 31], 15],
['TestBlurryShadow 3', ctx.getImageData(400, 500, 1, 1).data, [ 255, 0, 0, 31], 15],
['TestBlurryShadow 4', ctx.getImageData(300, 400, 1, 1).data, [ 255, 0, 0, 31], 15],
['TestBlurryShadow 5', ctx.getImageData(500, 400, 1, 1).data, [ 255, 0, 0, 31], 15],
['TestRotatedAlphaShadow 1', ctx.getImageData(400, 650, 1, 1).data, [ 0, 0, 0, 0, 0], 0],
['TestRotatedAlphaShadow 1', ctx.getImageData(400, 650, 1, 1).data, [ 0, 0, 0, 0], 0],
['TestRotatedAlphaShadow 2', ctx.getImageData(400, 575, 1, 1).data, [ 255, 0, 0, 64], 15],
['TestRotatedAlphaShadow 3', ctx.getImageData(400, 725, 1, 1).data, [ 255, 0, 0, 64], 15],
['TestRotatedAlphaShadow 4', ctx.getImageData(325, 650, 1, 1).data, [ 255, 0, 0, 64], 15],
['TestRotatedAlphaShadow 5', ctx.getImageData(475, 650, 1, 1).data, [ 255, 0, 0, 64], 15],
['TestRotatedBlurryShadow 1', ctx.getImageData(400, 900, 1, 1).data, [ 0, 0, 0, 0, 0], 0],
['TestRotatedBlurryShadow 1', ctx.getImageData(400, 900, 1, 1).data, [ 0, 0, 0, 0], 0],
['TestRotatedBlurryShadow 2', ctx.getImageData(400, 800, 1, 1).data, [ 255, 0, 0, 31], 15],
['TestRotatedBlurryShadow 3', ctx.getImageData(400, 1000, 1, 1).data, [ 255, 0, 0, 31], 15],
['TestRotatedBlurryShadow 4', ctx.getImageData(300, 900, 1, 1).data, [ 255, 0, 0, 31], 15],
......
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<body>
<script>
test(function(t) {
var SHADOW_OFFSET = 15;
var image = new Image();
image.src = "resources/html5.png";
image.onload = function() {
var canvas = document.createElement('canvas');
document.body.appendChild(canvas);
canvas.setAttribute('width', '1000');
canvas.setAttribute('height', '1000');
var ctx = canvas.getContext('2d');
var pattern = ctx.createPattern(image, 'repeat');
ctx.beginPath();
ctx.fillStyle = pattern;
ctx.shadowColor = 'rgba(0,0,0,0.5)';
ctx.shadowBlur = 10;
ctx.shadowOffsetX = SHADOW_OFFSET;
ctx.shadowOffsetY = SHADOW_OFFSET;
ctx.rect(0, 0, image.width, image.height);
// colored pixels of html5.png starts at 3 pixels away from the border of
// the image, they must be taken into account since the shadow is applied
// to the colored pixels.
const squareBorder = image.width + SHADOW_OFFSET + 3 * 2;
var imgDataBefore = [];
imgDataBefore.push(ctx.getImageData(25, squareBorder, 1, 1).data)
for (var i = 25; i < squareBorder; i += 10)
imgDataBefore.push(ctx.getImageData(squareBorder, i, 1, 1).data)
ctx.fill();
var imgDataAfter = [];
imgDataAfter.push(ctx.getImageData(25, squareBorder, 1, 1).data)
for (var i = 25; i < squareBorder; i += 10)
imgDataAfter.push(ctx.getImageData(squareBorder, i, 1, 1).data)
for (var i = 0; i < imgDataAfter.length; i++)
assert_array_equals(imgDataAfter[i], imgDataBefore[i]);
};
}, 'Test that the drop shadow properly falls off when a rect is drawn by filling with a pattern.');
</script>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<body>
<script>
test(function(t) {
var SHADOW_OFFSET = 15;
var image = new Image();
image.src = "resources/html5.png";
image.onload = function() {
var canvas = document.createElement('canvas');
document.body.appendChild(canvas);
canvas.setAttribute('width', '1000');
canvas.setAttribute('height', '1000');
var ctx = canvas.getContext('2d');
var pattern = ctx.createPattern(image, 'repeat');
ctx.beginPath();
ctx.fillStyle = pattern;
ctx.shadowColor = 'rgba(0,0,0,0.5)';
ctx.shadowBlur = 10;
ctx.shadowOffsetX = SHADOW_OFFSET;
ctx.shadowOffsetY = SHADOW_OFFSET;
// colored pixels of html5.png starts at 3 pixels away from the border of
// the image, they must be taken into account since the shadow is applied
// to the colored pixels.
const squareBorder = image.width + SHADOW_OFFSET + 3 * 2;
var imgDataBefore = [];
imgDataBefore.push(ctx.getImageData(25, squareBorder, 1, 1).data)
for (var i = 25; i < squareBorder; i += 10)
imgDataBefore.push(ctx.getImageData(squareBorder, i, 1, 1).data)
ctx.fillRect(0, 0, image.width, image.height);
var imgDataAfter = [];
imgDataAfter.push(ctx.getImageData(25, squareBorder, 1, 1).data)
for (var i = 25; i < squareBorder; i += 10)
imgDataAfter.push(ctx.getImageData(squareBorder, i, 1, 1).data)
for (var i = 0; i < imgDataAfter.length; i++)
assert_array_equals(imgDataAfter[i], imgDataBefore[i]);
};
}, 'Test that the drop shadow properly falls off when fillRect is drawn with a pattern.');
</script>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<body>
<script>
test(function(t) {
var STROKE_WIDTH = 20;
var SHADOW_OFFSET = 15;
// Do not start at (0, 0)
// otherwise half of the stroke on top left will be hidden
var POSITION = 20;
var SIDE_LENGTH = 100;
var image = new Image();
image.src = "resources/html5.png";
image.onload = function() {
var canvas = document.createElement('canvas');
document.body.appendChild(canvas);
canvas.setAttribute('width', '1000');
canvas.setAttribute('height', '1000');
var ctx = canvas.getContext('2d');
var pattern = ctx.createPattern(image, 'repeat');
ctx.beginPath();
ctx.strokeStyle = pattern;
ctx.lineWidth = STROKE_WIDTH;
ctx.shadowColor = 'rgba(0,0,0,0.5)';
ctx.shadowBlur = 10;
ctx.shadowOffsetX = SHADOW_OFFSET;
ctx.shadowOffsetY = SHADOW_OFFSET;
// colored pixels of html5.png starts at 3 pixels away from the border of
// the image, they must be taken into account since the shadow is applied
// to the colored pixels.
const squareBorder = POSITION + SIDE_LENGTH + STROKE_WIDTH / 2 + SHADOW_OFFSET + 3 * 2;
var imgDataBefore = [];
for (var i = 25; i < squareBorder; i += 10)
imgDataBefore.push(ctx.getImageData(squareBorder, i, 1, 1).data)
ctx.strokeRect(POSITION, POSITION, SIDE_LENGTH, SIDE_LENGTH);
var imgDataAfter = [];
for (var i = 25; i < squareBorder; i += 10)
imgDataAfter.push(ctx.getImageData(squareBorder, i, 1, 1).data)
for (var i = 0; i < imgDataAfter.length; i++)
assert_array_equals(imgDataAfter[i], imgDataBefore[i]);
};
}, 'Test that the drop shadow properly falls off when a rect is stroked with a pattern.');
</script>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment