Refactor complex CTM management for image filters

This consolidates the scale decomposition and SkMatrixImageFilter logic
that SkCanvas did during a layer save into an applyCTM function. It is
expanded to handle the extra transformation steps for backdrops.

The backdrop logic in SkCanvas has also been updated to only snap the
necessary portion of the buffer, and also use applyCTM. Previously any
backdrop filter with a CTM beyond scale/translate would do no filtering.

Unfortunately, perspective has caused too many headaches to solve in a
single CL, so its issues are recorded at skbug.com/9074.

Other minor fixes that were encountered while working on this:
- Raster's CopyFromRaster() incorrectly held onto the subset after copying.
  (unfortunately it looks like snapBackImage() needs to copy; referencing
   the subset directly corrupted the output).
- SkLocalMatrixImageFilter now supports complex CTMs assuming its input
  supports CTMs.
- CropRects need to apply in the source coordinate system, but are not
  aware of complex CTMs when performing clipping. For a simple fix, any
  filter with a crop rect set cannot support complex CTMs until that's
  updated.

Bug: skia:9074, chromium:959412
Change-Id: I1276a4ec400dfefb958c14beda078bdf1d087baa
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/213080
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
This commit is contained in:
Michael Ludwig 2019-05-17 11:21:53 -04:00 committed by Skia Commit-Bot
parent 9756c36666
commit 08b260c27b
8 changed files with 231 additions and 62 deletions

View File

@ -11,10 +11,10 @@
#include "include/effects/SkBlurImageFilter.h"
#include "include/effects/SkColorFilterImageFilter.h"
// This draws correctly if there's a small cyan rectangle above a much larger magenta rectangle.
// There should be no red around the cyan rectangle and no green within the magenta rectangle.
typedef sk_sp<SkImageFilter> (*FilterFactory)(const SkImageFilter::CropRect* crop);
DEF_SIMPLE_GM(backdrop_imagefilter_croprect, canvas, 600, 500) {
static void draw_backdrop_filter_gm(SkCanvas* canvas, float outsetX, float outsetY,
FilterFactory factory) {
// CTM translates to (150, 150)
SkPoint origin = SkPoint::Make(150.f, 150.f);
// The save layer specified after the CTM has negative coordinates, but
@ -25,20 +25,10 @@ DEF_SIMPLE_GM(backdrop_imagefilter_croprect, canvas, 600, 500) {
// the layer's image space.
SkRect cropInLocal = SkRect::MakeLTRB(50.f, 10.f, 250.f, 40.f);
SkImageFilter::CropRect cropRect(cropInLocal);
float matrix[20] = {-1.f, 0.f, 0.f, 0.f, 1.f,
0.f, -1.f, 0.f, 0.f, 1.f,
0.f, 0.f, -1.f, 0.f, 1.f,
0.f, 0.f, 0.f, 1.f, 0.f};
sk_sp<SkColorFilter> colorFilter = SkColorFilters::Matrix(matrix);
sk_sp<SkImageFilter> imageFilter = SkColorFilterImageFilter::Make(colorFilter, nullptr,
&cropRect);
SkImageFilter::CropRect cropRect(cropInLocal.makeOutset(outsetX, outsetY));
sk_sp<SkImageFilter> imageFilter = factory(&cropRect);
SkPaint p;
SkPaint l;
l.setStyle(SkPaint::kStroke_Style);
l.setStrokeWidth(0.f);
for (int i = 0; i < 2; ++i) {
canvas->save();
canvas->translate(origin.fX, origin.fY);
@ -77,3 +67,58 @@ DEF_SIMPLE_GM(backdrop_imagefilter_croprect, canvas, 600, 500) {
origin.fY += 150.f;
}
}
static sk_sp<SkImageFilter> make_invert_filter(const SkImageFilter::CropRect* crop) {
static const float matrix[20] = {-1.f, 0.f, 0.f, 0.f, 1.f,
0.f, -1.f, 0.f, 0.f, 1.f,
0.f, 0.f, -1.f, 0.f, 1.f,
0.f, 0.f, 0.f, 1.f, 0.f};
return SkColorFilterImageFilter::Make(SkColorFilters::Matrix(matrix), nullptr, crop);
}
static sk_sp<SkImageFilter> make_blur_filter(const SkImageFilter::CropRect* crop) {
// Use different sigmas for x and y so rotated CTM is apparent
return SkBlurImageFilter::Make(16.f, 4.f, nullptr, crop);
}
// This draws correctly if there's a small cyan rectangle above a much larger magenta rectangle.
// There should be no red around the cyan rectangle and no green within the magenta rectangle.
DEF_SIMPLE_GM(backdrop_imagefilter_croprect, canvas, 600, 500) {
draw_backdrop_filter_gm(canvas, 0.f, 0.f, make_invert_filter);
}
// This draws correctly if there's a blurred red rectangle inside a cyan rectangle, above a blurred
// green rectangle inside a larger magenta rectangle. All rectangles and the blur direction are
// consistently rotated.
DEF_SIMPLE_GM(backdrop_imagefilter_croprect_rotated, canvas, 600, 500) {
canvas->translate(140.f, -180.f);
canvas->rotate(30.f);
draw_backdrop_filter_gm(canvas, 32.f, 32.f, make_blur_filter);
}
// This draws correctly if there's a blurred red rectangle inside a cyan rectangle, above a blurred
// green rectangle inside a larger magenta rectangle. All rectangles and the blur direction are
// under consistent perspective.
// NOTE: Currently renders incorrectly, see skbug.com/9074
DEF_SIMPLE_GM(backdrop_imagefilter_croprect_persp, canvas, 600, 500) {
SkMatrix persp = SkMatrix::I();
persp.setPerspY(0.001f);
persp.setSkewX(8.f / 25.f);
canvas->concat(persp);
draw_backdrop_filter_gm(canvas, 32.f, 32.f, make_blur_filter);
}
// This draws correctly if there's a small cyan rectangle above a much larger magenta rectangle.
// There should be no red around the cyan rectangle and no green within the magenta rectangle, and
// everything should be 50% transparent.
DEF_SIMPLE_GM(backdrop_imagefilter_croprect_nested, canvas, 600, 500) {
SkPaint p;
p.setAlphaf(0.5f);
// This ensures there is a non-root device on the stack with a non-zero origin.
canvas->translate(15.f, 10.f);
canvas->clipRect(SkRect::MakeWH(600.f, 500.f));
canvas->saveLayer(nullptr, &p);
draw_backdrop_filter_gm(canvas, 0.f, 0.f, make_invert_filter);
canvas->restore();
}

View File

@ -447,6 +447,7 @@ protected:
private:
friend class SkGraphics;
friend bool SkIsSameFilter(const SkImageFilter* a, const SkImageFilter* b);
static void PurgeCache();

View File

@ -29,6 +29,7 @@
#include "src/core/SkDraw.h"
#include "src/core/SkGlyphRun.h"
#include "src/core/SkImageFilterCache.h"
#include "src/core/SkImageFilterPriv.h"
#include "src/core/SkLatticeIter.h"
#include "src/core/SkMSAN.h"
#include "src/core/SkMakeUnique.h"
@ -929,27 +930,42 @@ int SkCanvas::only_axis_aligned_saveBehind(const SkRect* bounds) {
void SkCanvas::DrawDeviceWithFilter(SkBaseDevice* src, const SkImageFilter* filter,
SkBaseDevice* dst, const SkIPoint& dstOrigin,
const SkMatrix& ctm) {
SkDraw draw;
SkRasterClip rc;
rc.setRect(SkIRect::MakeWH(dst->width(), dst->height()));
if (!dst->accessPixels(&draw.fDst)) {
draw.fDst.reset(dst->imageInfo(), nullptr, 0);
}
draw.fMatrix = &SkMatrix::I();
draw.fRC = &rc;
int x = src->getOrigin().x() - dstOrigin.x();
int y = src->getOrigin().y() - dstOrigin.y();
SkPaint p;
SkIRect snapBounds = SkIRect::MakeXYWH(dstOrigin.x() - src->getOrigin().x(),
dstOrigin.y() - src->getOrigin().y(),
dst->width(), dst->height());
int x = 0;
int y = 0;
if (filter) {
SkMatrix actm = ctm;
// Account for the origin offset in the local matrix
actm.postTranslate(x, y);
p.setImageFilter(filter->makeWithLocalMatrix(actm));
// Calculate expanded snap bounds
SkIRect newBounds = filter->filterBounds(
snapBounds, ctm, SkImageFilter::kReverse_MapDirection, &snapBounds);
// Must clamp to valid src since the filter or rotations may expand beyond what's readable
SkIRect srcR = SkIRect::MakeWH(src->width(), src->height());
if (!newBounds.intersect(srcR)) {
return;
}
x = newBounds.fLeft - snapBounds.fLeft;
y = newBounds.fTop - snapBounds.fTop;
snapBounds = newBounds;
SkMatrix localCTM;
sk_sp<SkImageFilter> modifiedFilter = SkApplyCTMToBackdropFilter(filter, ctm, &localCTM);
// Account for the origin offset in the CTM
localCTM.postTranslate(-dstOrigin.x(), -dstOrigin.y());
// In this case we always wrap the filter (even when it's the original) with 'localCTM'
// since there's no device CTM stack that provides it to the image filter context.
// FIXME skbug.com/9074 - once perspective is properly supported, drop the
// localCTM.hasPerspective condition from assert.
SkASSERT(localCTM.isScaleTranslate() || filter->canHandleComplexCTM() ||
localCTM.hasPerspective());
p.setImageFilter(modifiedFilter->makeWithLocalMatrix(localCTM));
}
auto special = src->snapSpecial();
auto special = src->snapBackImage(snapBounds);
if (special) {
dst->drawSpecial(special.get(), x, y, p, nullptr, SkMatrix::I());
}
@ -991,36 +1007,41 @@ void SkCanvas::internalSaveLayer(const SaveLayerRec& rec, SaveLayerStrategy stra
SkImageFilter* imageFilter = paint ? paint->getImageFilter() : nullptr;
SkMatrix stashedMatrix = fMCRec->fMatrix;
MCRec* modifiedRec = nullptr;
SkMatrix remainder;
SkSize scale;
/*
* ImageFilters (so far) do not correctly handle matrices (CTM) that contain rotation/skew/etc.
* but they do handle scaling. To accommodate this, we do the following:
* Many ImageFilters (so far) do not (on their own) correctly handle matrices (CTM) that
* contain rotation/skew/etc. We rely on applyCTM to create a new image filter DAG as needed to
* accommodate this, but it requires update the CTM we use when drawing into the layer.
*
* 1. Stash off the current CTM
* 2. Decompose the CTM into SCALE and REMAINDER
* 3. Wack the CTM to be just SCALE, and wrap the imagefilter with a MatrixImageFilter that
* contains the REMAINDER
* 2. Apply the CTM to imagefilter, which decomposes it into simple and complex transforms
* if necessary.
* 3. Wack the CTM to be the remaining scale matrix and use the modified imagefilter, which
* is a MatrixImageFilter that contains the complex matrix.
* 4. Proceed as usual, allowing the client to draw into the layer (now with a scale-only CTM)
* 5. During restore, we process the MatrixImageFilter, which applies REMAINDER to the output
* 5. During restore, the MatrixImageFilter automatically applies complex stage to the output
* of the original imagefilter, and draw that (via drawSprite)
* 6. Unwack the CTM to its original state (i.e. stashedMatrix)
*
* Perhaps in the future we could augment #5 to apply REMAINDER as part of the draw (no longer
* a sprite operation) to avoid the extra buffer/overhead of MatrixImageFilter.
*/
if (imageFilter && !stashedMatrix.isScaleTranslate() && !imageFilter->canHandleComplexCTM() &&
stashedMatrix.decomposeScale(&scale, &remainder))
{
// We will restore the matrix (which we are overwriting here) in restore via fStashedMatrix
modifiedRec = fMCRec;
this->internalSetMatrix(SkMatrix::MakeScale(scale.width(), scale.height()));
SkPaint* p = lazyP.set(*paint);
p->setImageFilter(SkImageFilter::MakeMatrixFilter(remainder,
SkFilterQuality::kLow_SkFilterQuality,
sk_ref_sp(imageFilter)));
imageFilter = p->getImageFilter();
paint = p;
if (imageFilter) {
SkMatrix modifiedCTM;
sk_sp<SkImageFilter> modifiedFilter = SkApplyCTMToFilter(imageFilter, stashedMatrix,
&modifiedCTM);
if (!SkIsSameFilter(modifiedFilter.get(), imageFilter)) {
// The original filter couldn't support the CTM entirely
SkASSERT(modifiedCTM.isScaleTranslate() || imageFilter->canHandleComplexCTM());
modifiedRec = fMCRec;
this->internalSetMatrix(modifiedCTM);
SkPaint* p = lazyP.set(*paint);
p->setImageFilter(std::move(modifiedFilter));
imageFilter = p->getImageFilter();
paint = p;
}
// Else the filter didn't change, so modifiedCTM == stashedMatrix and there's nothing
// left to do since the stack already has that as the CTM.
}
// do this before we create the layer. We don't call the public save() since

View File

@ -9,6 +9,7 @@
#include "include/core/SkCanvas.h"
#include "include/core/SkRect.h"
#include "include/effects/SkComposeImageFilter.h"
#include "include/private/SkSafe32.h"
#include "src/core/SkFuzzLogging.h"
#include "src/core/SkImageFilterCache.h"
@ -289,7 +290,10 @@ bool SkImageFilter::asAColorFilter(SkColorFilter** filterPtr) const {
}
bool SkImageFilter::canHandleComplexCTM() const {
if (!this->onCanHandleComplexCTM()) {
// CropRects need to apply in the source coordinate system, but are not aware of complex CTMs
// when performing clipping. For a simple fix, any filter with a crop rect set cannot support
// complex CTMs until that's updated.
if (this->cropRectIsSet() || !this->onCanHandleComplexCTM()) {
return false;
}
const int count = this->countInputs();
@ -444,11 +448,7 @@ sk_sp<SkImageFilter> SkImageFilter::MakeMatrixFilter(const SkMatrix& matrix,
}
sk_sp<SkImageFilter> SkImageFilter::makeWithLocalMatrix(const SkMatrix& matrix) const {
// SkLocalMatrixImageFilter takes SkImage* in its factory, but logically that parameter
// is *always* treated as a const ptr. Hence the const-cast here.
//
SkImageFilter* nonConstThis = const_cast<SkImageFilter*>(this);
return SkLocalMatrixImageFilter::Make(matrix, sk_ref_sp<SkImageFilter>(nonConstThis));
return SkLocalMatrixImageFilter::Make(matrix, this->refMe());
}
sk_sp<SkSpecialImage> SkImageFilter::filterInput(int index,
@ -492,3 +492,71 @@ SkIRect SkImageFilter::DetermineRepeatedSrcBound(const SkIRect& srcBounds,
return tmp;
}
/////////////////////////////////////////////////////////////////////////////////////////////////
static sk_sp<SkImageFilter> apply_ctm_to_filter(sk_sp<SkImageFilter> input, const SkMatrix& ctm,
SkMatrix* remainder, bool asBackdrop) {
if (ctm.isScaleTranslate() || input->canHandleComplexCTM()) {
// The filter supports the CTM, so leave it as-is and 'remainder' stores the whole CTM
*remainder = ctm;
return input;
}
// We have a complex CTM and a filter that can't support them, so it needs to use the matrix
// transform filter that resamples the image contents. Decompose the simple portion of the ctm
// into 'remainder'
SkMatrix ctmToEmbed;
SkSize scale;
if (ctm.decomposeScale(&scale, &ctmToEmbed)) {
// decomposeScale splits ctm into scale * ctmToEmbed, so bake ctmToEmbed into DAG
// with a matrix filter and return scale as the remaining matrix for the real CTM.
remainder->setScale(scale.fWidth, scale.fHeight);
} else {
// Unable to decompose
// FIXME Ideally we'd embed the entire CTM as part of the matrix image filter, but
// the device <-> src bounds calculations for filters are very brittle under perspective,
// and can easily run into precision issues (wrong bounds that clip), or performance issues
// (producing large source-space images where 80% of the image is compressed into a few
// device pixels). A longer term solution for perspective-space image filtering is needed
// see skbug.com/9074
if (ctm.hasPerspective()) {
*remainder = ctm;
return input;
}
ctmToEmbed = ctm;
remainder->setIdentity();
}
if (asBackdrop) {
// In the backdrop case we also have to transform the existing device-space buffer content
// into the source coordinate space prior to the filtering. Non-backdrop filter inputs are
// already in the source space because of how the layer is drawn by SkCanvas.
SkMatrix invEmbed;
if (ctmToEmbed.invert(&invEmbed)) {
input = SkComposeImageFilter::Make(std::move(input),
SkMatrixImageFilter::Make(invEmbed, kLow_SkFilterQuality, nullptr));
}
}
return SkMatrixImageFilter::Make(ctmToEmbed, kLow_SkFilterQuality, std::move(input));
}
sk_sp<SkImageFilter> SkApplyCTMToFilter(const SkImageFilter* filter, const SkMatrix& ctm,
SkMatrix* remainder) {
return apply_ctm_to_filter(sk_ref_sp(filter), ctm, remainder, false);
}
sk_sp<SkImageFilter> SkApplyCTMToBackdropFilter(const SkImageFilter* filter, const SkMatrix& ctm,
SkMatrix* remainder) {
return apply_ctm_to_filter(sk_ref_sp(filter), ctm, remainder, true);
}
bool SkIsSameFilter(const SkImageFilter* a, const SkImageFilter* b) {
if (!a || !b) {
// The filters are the "same" if they're both null
return !a && !b;
} else {
return a->fUniqueID == b->fUniqueID;
}
}

View File

@ -21,4 +21,30 @@
} \
} while (0)
/**
* Return an image filter representing this filter applied with the given ctm. This will modify the
* DAG as needed if this filter does not support complex CTMs and 'ctm' is not simple. The ctm
* matrix will be decomposed such that ctm = A*B; B will be incorporated directly into the DAG and A
* must be the ctm set on the context passed to filterImage(). 'remainder' will be set to A.
*
* If this filter supports complex ctms, or 'ctm' is not complex, then A = ctm and B = I. When the
* filter does not support complex ctms, and the ctm is complex, then A represents the extracted
* simple portion of the ctm, and the complex portion is baked into a new DAG using a matrix filter.
*
* This will never return null.
*/
sk_sp<SkImageFilter> SkApplyCTMToFilter(const SkImageFilter* filter, const SkMatrix& ctm,
SkMatrix* remainder);
/**
* Similar to SkApplyCTMToFilter except this assumes the input content is an existing backdrop image
* to be filtered. As such, the input to this filter will also be transformed by B^-1 if the filter
* can't support complex CTMs, since backdrop content is already in device space and must be
* transformed back into the CTM's local space.
*/
sk_sp<SkImageFilter> SkApplyCTMToBackdropFilter(const SkImageFilter* filter, const SkMatrix& ctm,
SkMatrix* remainder);
bool SkIsSameFilter(const SkImageFilter* a, const SkImageFilter* b);
#endif

View File

@ -16,12 +16,13 @@ sk_sp<SkImageFilter> SkLocalMatrixImageFilter::Make(const SkMatrix& localM,
if (!input) {
return nullptr;
}
if (localM.getType() & (SkMatrix::kAffine_Mask | SkMatrix::kPerspective_Mask)) {
return nullptr;
}
if (localM.isIdentity()) {
return input;
}
if (!input->canHandleComplexCTM() && !localM.isScaleTranslate()) {
// Nothing we can do at this point
return nullptr;
}
return sk_sp<SkImageFilter>(new SkLocalMatrixImageFilter(localM, input));
}

View File

@ -26,6 +26,8 @@ protected:
SkIRect onFilterBounds(const SkIRect& src, const SkMatrix& ctm,
MapDirection, const SkIRect* inputRect) const override;
bool onCanHandleComplexCTM() const override { return true; }
private:
SK_FLATTENABLE_HOOKS(SkLocalMatrixImageFilter)

View File

@ -344,7 +344,12 @@ sk_sp<SkSpecialImage> SkSpecialImage::CopyFromRaster(const SkIRect& subset,
if (!bm.readPixels(tmp.info(), tmp.getPixels(), tmp.rowBytes(), subset.x(), subset.y())) {
return nullptr;
}
return sk_make_sp<SkSpecialImage_Raster>(subset, tmp, props);
// Since we're making a copy of the raster, the resulting special image is the exact size
// of the requested subset of the original and no longer needs to be offset by subset's left
// and top, since those were relative to the original's buffer.
return sk_make_sp<SkSpecialImage_Raster>(
SkIRect::MakeWH(subset.width(), subset.height()), tmp, props);
}
#if SK_SUPPORT_GPU