Implement separate crop filter

Right now the crop image filter is hidden in src/ and is only used in
a new GM to test out its functionality. In later CLs the plan will be to
migrate individual filter effects away from the built-in crop rect to
composing themselves with this image filter.

Eventually, the crop filter will be made public as part of the
SkImageFilters factories so there can be independent control on input
and output cropping. Its features will also be expanded to support more
tile modes than just kDecal.

Once all other effects rely on this crop filter, the legacy system can
be deleted and the effects can be more easily updated to the rest of the
new image filter API. This crop filter is actually the first image
filter to be implemented only in terms of the new API (and skif coord
space types). This highlighted a few pain points that still exist in
terms of the API, where I have added comments to capture how they could
be improved. Unfortunately, they aren't addressable until the rest of
the implementations have been migrated.

Bug: skia:9296
Change-Id: I12e96b679c246a9ec4e1e61f640414a11f1b6bb5
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/451597
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
This commit is contained in:
Michael Ludwig 2021-09-24 14:45:35 -04:00 committed by SkCQ
parent b05bbd03f9
commit 397fdfdf18
11 changed files with 624 additions and 6 deletions

372
gm/crop_imagefilter.cpp Normal file
View File

@ -0,0 +1,372 @@
/*
* Copyright 2021 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "gm/gm.h"
#include "include/core/SkBlendMode.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkColor.h"
#include "include/core/SkImageFilter.h"
#include "include/core/SkPaint.h"
#include "include/core/SkRect.h"
#include "include/core/SkSurface.h"
#include "include/effects/SkDashPathEffect.h"
#include "include/effects/SkGradientShader.h"
#include "include/effects/SkImageFilters.h"
#include "tools/Resources.h"
// TODO(michaelludwig) - This will be made public within SkImageFilters.h at some point
#include "src/effects/imagefilters/SkCropImageFilter.h"
namespace {
static constexpr SkColor kOutputBoundsColor = SK_ColorRED;
static constexpr SkColor kCropRectColor = SK_ColorGREEN;
static constexpr SkColor kContentBoundsColor = SK_ColorBLUE;
static constexpr SkRect kExampleBounds = {0.f, 0.f, 100.f, 100.f};
// "Crop" refers to the rect passed to the crop image filter, "Rect" refers to some other rect
// from context, likely the output bounds or the content bounds.
enum class CropRelation {
kCropOverlapsRect, // Intersect but doesn't fully contain one way or the other
kCropContainsRect,
kRectContainsCrop,
kCropRectDisjoint,
};
SkRect make_overlap(const SkRect& r, float amountX, float amountY) {
return r.makeOffset(r.width() * amountX, r.height() * amountY);
}
SkRect make_inset(const SkRect& r, float amountX, float amountY) {
return r.makeInset(r.width() * amountX, r.height() * amountY);
}
SkRect make_outset(const SkRect& r, float amountX, float amountY) {
return r.makeOutset(r.width() * amountX, r.height() * amountY);
}
SkRect make_disjoint(const SkRect& r, float amountX, float amountY) {
float xOffset = (amountX > 0.f ? (r.width() + r.width() * amountX) :
(amountX < 0.f ? (-r.width() + r.width() * amountX) : 0.f));
float yOffset = (amountY > 0.f ? (r.height() + r.height() * amountY) :
(amountY < 0.f ? (-r.height() + r.height() * amountY) : 0.f));
return r.makeOffset(xOffset, yOffset);
}
void get_example_rects(CropRelation outputRelation, CropRelation inputRelation, bool hintContent,
SkRect* outputBounds, SkRect* cropRect, SkRect* contentBounds) {
*outputBounds = kExampleBounds.makeInset(20.f, 20.f);
switch(outputRelation) {
case CropRelation::kCropOverlapsRect:
*cropRect = make_overlap(*outputBounds, -0.15f, 0.15f);
SkASSERT(cropRect->intersects(*outputBounds) &&
!cropRect->contains(*outputBounds) &&
!outputBounds->contains(*cropRect));
break;
case CropRelation::kCropContainsRect:
*cropRect = make_outset(*outputBounds, 0.15f, 0.15f);
SkASSERT(cropRect->contains(*outputBounds));
break;
case CropRelation::kRectContainsCrop:
*cropRect = make_inset(*outputBounds, 0.15f, 0.15f);
SkASSERT(outputBounds->contains(*cropRect));
break;
case CropRelation::kCropRectDisjoint:
*cropRect = make_disjoint(*outputBounds, 0.15f, 0.0f);
SkASSERT(!cropRect->intersects(*outputBounds));
break;
}
// Determine content bounds for example based on computed crop rect and input relation
if (hintContent) {
switch(inputRelation) {
case CropRelation::kCropOverlapsRect:
*contentBounds = make_overlap(*cropRect, 0.075f, -0.75f);
SkASSERT(contentBounds->intersects(*cropRect) &&
!contentBounds->contains(*cropRect) &&
!cropRect->contains(*contentBounds));
break;
case CropRelation::kCropContainsRect:
*contentBounds = make_inset(*cropRect, 0.075f, 0.075f);
SkASSERT(cropRect->contains(*contentBounds));
break;
case CropRelation::kRectContainsCrop:
*contentBounds = make_outset(*cropRect, 0.1f, 0.1f);
SkASSERT(contentBounds->contains(*cropRect));
break;
case CropRelation::kCropRectDisjoint:
*contentBounds = make_disjoint(*cropRect, 0.0f, 0.075f);
SkASSERT(!contentBounds->intersects(*cropRect));
break;
}
} else {
*contentBounds = kExampleBounds;
}
}
// TODO(michaelludwig) - This is a useful test pattern for tile modes and filtering; should
// consolidate it with the similar version in gpu_blur_utils if the GMs remain separate at the end.
sk_sp<SkImage> make_image(SkCanvas* canvas, const SkRect* contentBounds) {
const float w = kExampleBounds.width();
const float h = kExampleBounds.height();
const auto srcII = SkImageInfo::Make(SkISize::Make(SkScalarCeilToInt(w), SkScalarCeilToInt(h)),
kRGBA_8888_SkColorType, kPremul_SkAlphaType);
auto surf = SkSurface::MakeRaster(srcII);
surf->getCanvas()->drawColor(SK_ColorDKGRAY);
SkPaint paint;
paint.setAntiAlias(true);
paint.setStyle(SkPaint::kStroke_Style);
// Draw four horizontal lines at 1/4, 3/8, 5/8, 3/4.
paint.setStrokeWidth(h/16.f);
paint.setColor(SK_ColorRED);
surf->getCanvas()->drawLine({0.f, 1.f*h/4.f}, {w, 1.f*h/4.f}, paint);
paint.setColor(/* sea foam */ 0xFF71EEB8);
surf->getCanvas()->drawLine({0.f, 3.f*h/8.f}, {w, 3.f*h/8.f}, paint);
paint.setColor(SK_ColorYELLOW);
surf->getCanvas()->drawLine({0.f, 5.f*h/8.f}, {w, 5.f*h/8.f}, paint);
paint.setColor(SK_ColorCYAN);
surf->getCanvas()->drawLine({0.f, 3.f*h/4.f}, {w, 3.f*h/4.f}, paint);
// Draw four vertical lines at 1/4, 3/8, 5/8, 3/4.
paint.setStrokeWidth(w/16.f);
paint.setColor(/* orange */ 0xFFFFA500);
surf->getCanvas()->drawLine({1.f*w/4.f, 0.f}, {1.f*h/4.f, h}, paint);
paint.setColor(SK_ColorBLUE);
surf->getCanvas()->drawLine({3.f*w/8.f, 0.f}, {3.f*h/8.f, h}, paint);
paint.setColor(SK_ColorMAGENTA);
surf->getCanvas()->drawLine({5.f*w/8.f, 0.f}, {5.f*h/8.f, h}, paint);
paint.setColor(SK_ColorGREEN);
surf->getCanvas()->drawLine({3.f*w/4.f, 0.f}, {3.f*h/4.f, h}, paint);
// Fill everything outside of the content bounds with red since it shouldn't be sampled from.
if (contentBounds) {
surf->getCanvas()->clipRect(*contentBounds, SkClipOp::kDifference);
surf->getCanvas()->clear(SK_ColorRED);
}
return surf->makeImageSnapshot();
}
void draw_example(
SkCanvas* canvas,
SkTileMode inputMode, // the tile mode of the input to the crop filter
SkTileMode outputMode, // the tile mode that the crop filter outputs
CropRelation outputRelation, // how crop rect relates to output bounds
CropRelation inputRelation, // how crop rect relates to content bounds
bool hintContent) { // whether or not contentBounds is hinted to saveLayer()
SkASSERT(inputMode == SkTileMode::kDecal && outputMode == SkTileMode::kDecal);
// Determine crop rect for example based on output relation
SkRect outputBounds, cropRect, contentBounds;
get_example_rects(outputRelation, inputRelation, hintContent,
&outputBounds, &cropRect, &contentBounds);
auto image = make_image(canvas, hintContent ? &contentBounds : nullptr);
canvas->save();
canvas->clipRect(kExampleBounds);
// Visualize the image tiled on the content bounds, semi-transparent
{
SkRect clippedContentBounds;
if (clippedContentBounds.intersect(contentBounds, kExampleBounds)) {
auto contentImage = image->makeSubset(clippedContentBounds.roundOut());
SkPaint tiledPaint;
tiledPaint.setShader(contentImage->makeShader(
inputMode, inputMode, SkSamplingOptions(SkFilterMode::kLinear)));
tiledPaint.setAlphaf(0.15f);
canvas->save();
canvas->translate(clippedContentBounds.fLeft, clippedContentBounds.fTop);
canvas->drawPaint(tiledPaint);
canvas->restore();
}
}
// Build filter, clip, save layer, draw, restore - the interesting part is in the tile modes
// and how the various bounds intersect each other.
{
sk_sp<SkImageFilter> filter = SkImageFilters::Blur(4.f, 4.f, nullptr);
filter = SkMakeCropImageFilter(cropRect, std::move(filter));
SkPaint layerPaint;
layerPaint.setImageFilter(std::move(filter));
canvas->save();
canvas->clipRect(outputBounds);
canvas->saveLayer(hintContent ? &contentBounds : nullptr, &layerPaint);
canvas->drawImageRect(image.get(), contentBounds, contentBounds,
SkSamplingOptions(SkFilterMode::kLinear), nullptr,
SkCanvas::kFast_SrcRectConstraint);
canvas->restore();
canvas->restore();
}
// Visualize bounds after the actual rendering.
{
SkPaint border;
border.setStyle(SkPaint::kStroke_Style);
border.setColor(kOutputBoundsColor);
canvas->drawRect(outputBounds, border);
border.setColor(kCropRectColor);
canvas->drawRect(cropRect, border);
if (hintContent) {
border.setColor(kContentBoundsColor);
canvas->drawRect(contentBounds, border);
}
}
canvas->restore();
}
// Draws 2x2 examples for a given input/output tile mode that show 4 relationships between the
// output bounds and the crop rect (intersect, output contains crop, crop contains output, and
// no intersection).
static constexpr SkRect kPaddedTileBounds = {kExampleBounds.fLeft,
kExampleBounds.fTop,
2.f * (kExampleBounds.fRight + 1.f),
2.f * (kExampleBounds.fBottom + 1.f)};
void draw_example_tile(
SkCanvas* canvas,
SkTileMode inputMode,
SkTileMode outputMode,
CropRelation inputRelation,
bool hintContent) {
auto drawQuadrant = [&](int tx, int ty, CropRelation outputRelation) {
canvas->save();
canvas->translate(tx * (kExampleBounds.fRight + 1.f), ty * (kExampleBounds.fBottom + 1.f));
draw_example(canvas, inputMode, outputMode, outputRelation, inputRelation, hintContent);
canvas->restore();
};
// The 4 examples, here Rect refers to the output bounds
drawQuadrant(0, 0, CropRelation::kCropOverlapsRect); // top left
drawQuadrant(1, 0, CropRelation::kRectContainsCrop); // top right
drawQuadrant(0, 1, CropRelation::kCropRectDisjoint); // bot left
drawQuadrant(1, 1, CropRelation::kCropContainsRect); // bot right
// Draw dotted lines in the 1px gap between examples
SkPaint dottedLine;
dottedLine.setColor(SK_ColorGRAY);
dottedLine.setStyle(SkPaint::kStroke_Style);
dottedLine.setStrokeCap(SkPaint::kSquare_Cap);
static const float kDots[2] = {0.f, 5.f};
dottedLine.setPathEffect(SkDashPathEffect::Make(kDots, 2, 0.f));
canvas->drawLine({kPaddedTileBounds.fLeft + 0.5f, kPaddedTileBounds.centerY() - 0.5f},
{kPaddedTileBounds.fRight - 0.5f, kPaddedTileBounds.centerY() - 0.5f},
dottedLine);
canvas->drawLine({kPaddedTileBounds.centerX() - 0.5f, kPaddedTileBounds.fTop + 0.5f},
{kPaddedTileBounds.centerX() - 0.5f, kPaddedTileBounds.fBottom - 0.5f},
dottedLine);
}
// Draw 5 example tiles in a column for 5 relationships between content bounds and crop rect:
// no content hint, intersect, content contains crop, crop contains content, and no intersection
static constexpr SkRect kPaddedColumnBounds = {kPaddedTileBounds.fLeft,
kPaddedTileBounds.fTop,
kPaddedTileBounds.fRight,
5.f * kPaddedTileBounds.fBottom - 1.f};
void draw_example_column(
SkCanvas* canvas,
SkTileMode inputMode,
SkTileMode outputMode) {
const std::pair<CropRelation, bool> inputRelations[5] = {
{ CropRelation::kCropOverlapsRect, false },
{ CropRelation::kCropOverlapsRect, true },
{ CropRelation::kCropContainsRect, true },
{ CropRelation::kRectContainsCrop, true },
{ CropRelation::kCropRectDisjoint, true }
};
canvas->save();
for (auto [inputRelation, hintContent] : inputRelations) {
draw_example_tile(canvas, inputMode, outputMode, inputRelation, hintContent);
canvas->translate(0.f, kPaddedTileBounds.fBottom);
}
canvas->restore();
}
// Draw 5x1 grid of examples covering supported input tile modes and crop rect relations
static constexpr int kNumRows = 5;
static constexpr int kNumCols = 1;
static constexpr float kGridWidth = kNumCols * kPaddedColumnBounds.fRight - 1.f;
void draw_example_grid(
SkCanvas* canvas,
SkTileMode outputMode) {
canvas->save();
for (auto inputMode : {SkTileMode::kDecal}) {
draw_example_column(canvas, inputMode, outputMode);
canvas->translate(kPaddedColumnBounds.fRight, 0.f);
}
canvas->restore();
// Draw dashed lines between rows and columns
SkPaint dashedLine;
dashedLine.setColor(SK_ColorGRAY);
dashedLine.setStyle(SkPaint::kStroke_Style);
dashedLine.setStrokeCap(SkPaint::kSquare_Cap);
static const float kDashes[2] = {5.f, 15.f};
dashedLine.setPathEffect(SkDashPathEffect::Make(kDashes, 2, 0.f));
for (int y = 1; y < kNumRows; ++y) {
canvas->drawLine({0.5f, y * kPaddedTileBounds.fBottom - 0.5f},
{kGridWidth - 0.5f, y * kPaddedTileBounds.fBottom - 0.5f}, dashedLine);
}
for (int x = 1; x < kNumCols; ++x) {
canvas->drawLine({x * kPaddedTileBounds.fRight - 0.5f, 0.5f},
{x * kPaddedTileBounds.fRight - 0.5f, kPaddedColumnBounds.fBottom - 0.5f},
dashedLine);
}
}
} // namespace
namespace skiagm {
class CropImageFilterGM : public GM {
public:
CropImageFilterGM(SkTileMode outputMode) : fOutputMode(outputMode) {}
protected:
SkISize onISize() override {
return {SkScalarRoundToInt(kGridWidth), SkScalarRoundToInt(kPaddedColumnBounds.fBottom)};
}
SkString onShortName() override {
SkString name("crop_imagefilter_");
switch (fOutputMode) {
case SkTileMode::kDecal: name.append("decal"); break;
case SkTileMode::kClamp: name.append("clamp"); break;
case SkTileMode::kRepeat: name.append("repeat"); break;
case SkTileMode::kMirror: name.append("mirror"); break;
}
return name;
}
void onDraw(SkCanvas* canvas) override {
draw_example_grid(canvas, fOutputMode);
}
private:
SkTileMode fOutputMode;
};
DEF_GM( return new CropImageFilterGM(SkTileMode::kDecal); )
// TODO(michaelludwig) - will add GM defs for other output tile modes once supported
} // namespace skiagm

View File

@ -16,6 +16,7 @@ skia_effects_imagefilter_sources = [
"$_src/effects/imagefilters/SkBlurImageFilter.cpp",
"$_src/effects/imagefilters/SkColorFilterImageFilter.cpp",
"$_src/effects/imagefilters/SkComposeImageFilter.cpp",
"$_src/effects/imagefilters/SkCropImageFilter.cpp",
"$_src/effects/imagefilters/SkDisplacementMapImageFilter.cpp",
"$_src/effects/imagefilters/SkDropShadowImageFilter.cpp",
"$_src/effects/imagefilters/SkImageImageFilter.cpp",

View File

@ -131,6 +131,7 @@ gm_sources = [
"$_gm/crbug_946965.cpp",
"$_gm/crbug_947055.cpp",
"$_gm/crbug_996140.cpp",
"$_gm/crop_imagefilter.cpp",
"$_gm/croppedrects.cpp",
"$_gm/crosscontextimage.cpp",
"$_gm/cubicpaths.cpp",

View File

@ -703,7 +703,7 @@ static std::pair<skif::Mapping, skif::LayerSpace<SkIRect>> get_layer_mapping_and
bool mustCoverDst = true) {
auto failedMapping = []() {
return std::make_pair<skif::Mapping, skif::LayerSpace<SkIRect>>(
{}, skif::LayerSpace<SkIRect>(SkIRect::MakeEmpty()));
{}, skif::LayerSpace<SkIRect>::Empty());
};
SkMatrix dstToLocal;

View File

@ -282,6 +282,11 @@ skif::LayerSpace<SkIRect> SkImageFilter_Base::getInputBounds(
mapping, desiredBounds, contentBounds);
// If we know what's actually going to be drawn into the layer, and we don't change transparent
// black, then we can further restrict the layer to what the known content is
// TODO (michaelludwig) - This logic could be moved into visitInputLayerBounds() when an input
// filter is null. Additionally, once all filters are robust to FilterResults with tile modes,
// we can always restrict the required input by content bounds since any additional transparent
// black is handled when producing the output result and sampling outside the input image with
// a decal tile mode.
if (knownContentBounds && !this->affectsTransparentBlack()) {
if (!requiredInput.intersect(contentBounds)) {
// Nothing would be output by the filter, so return empty rect
@ -473,7 +478,8 @@ skif::LayerSpace<SkIRect> SkImageFilter_Base::visitInputLayerBounds(
// TODO (michaelludwig) - if a filter doesn't have any inputs, it doesn't need any
// implicit source image, so arguably we could return an empty rect here. 'desiredOutput' is
// consistent with original behavior, so empty bounds may have unintended side effects
// but should be explored later.
// but should be explored later. Of note is that right now an empty layer bounds assumes
// that there's no need to filter on restore, which is not the case for these filters.
return desiredOutput;
}
@ -482,6 +488,10 @@ skif::LayerSpace<SkIRect> SkImageFilter_Base::visitInputLayerBounds(
const SkImageFilter* filter = this->getInput(i);
// The required input for this input filter, or 'targetOutput' if the filter is null and
// the source image is used (so must be sized to cover 'targetOutput').
// TODO (michaelludwig) - Right now contentBounds is applied conditionally at the end of
// the root getInputLayerBounds() based on affecting transparent black. Once that bit only
// changes output behavior, we can have the required bounds for a null input filter be the
// intersection of the desired output and the content bounds.
skif::LayerSpace<SkIRect> requiredInput =
filter ? as_IFB(filter)->onGetInputLayerBounds(mapping, desiredOutput,
contentBounds)
@ -527,7 +537,7 @@ skif::LayerSpace<SkIRect> SkImageFilter_Base::onGetInputLayerBounds(
const skif::Mapping& mapping, const skif::LayerSpace<SkIRect>& desiredOutput,
const skif::LayerSpace<SkIRect>& contentBounds, VisitChildren recurse) const {
// Call old functions for now since they may have been overridden by a subclass that's not been
// updated yet; normally this would just default to visitInputLayerBounds()
// updated yet; eventually this will be a pure virtual and impls control visiting children
SkIRect content = SkIRect(contentBounds);
SkIRect input = this->onFilterNodeBounds(SkIRect(desiredOutput), mapping.layerMatrix(),
kReverse_MapDirection, &content);
@ -542,7 +552,7 @@ skif::LayerSpace<SkIRect> SkImageFilter_Base::onGetInputLayerBounds(
skif::LayerSpace<SkIRect> SkImageFilter_Base::onGetOutputLayerBounds(
const skif::Mapping& mapping, const skif::LayerSpace<SkIRect>& contentBounds) const {
// Call old functions for now; normally this would default to visitOutputLayerBounds()
// Call old functions for now; eventually this will be a pure virtual
SkIRect aggregate = this->onFilterBounds(SkIRect(contentBounds), mapping.layerMatrix(),
kForward_MapDirection, nullptr);
SkIRect output = this->onFilterNodeBounds(aggregate, mapping.layerMatrix(),

View File

@ -140,4 +140,22 @@ SkSize Mapping::map<SkSize>(const SkSize& geom, const SkMatrix& matrix) {
return SkSize::Make(v.fX, v.fY);
}
FilterResult FilterResult::resolveToBounds(const LayerSpace<SkIRect>& newBounds) const {
// NOTE(michaelludwig) - This implementation is based on the assumption that an image resolved
// to 'newBounds' will be decal tiled and that the current image is decal tiled. Because of this
// simplification, the resolved image is always a subset of 'fImage' that matches the
// intersection of 'newBounds' and 'layerBounds()' so no rendering/copying is needed.
LayerSpace<SkIRect> tightBounds = newBounds;
if (!fImage || !tightBounds.intersect(this->layerBounds())) {
return {}; // Fully transparent
}
// Calculate offset from old origin to new origin, representing the relative subset in the image
LayerSpace<IVector> originShift = tightBounds.topLeft() - fOrigin;
auto subsetImage = fImage->makeSubset(SkIRect::MakeXYWH(originShift.x(), originShift.y(),
tightBounds.width(), tightBounds.height()));
return {std::move(subsetImage), tightBounds.topLeft()};
}
} // end namespace skif

View File

@ -346,6 +346,8 @@ public:
explicit LayerSpace(SkIRect&& geometry) : fData(std::move(geometry)) {}
explicit operator const SkIRect&() const { return fData; }
static LayerSpace<SkIRect> Empty() { return LayerSpace<SkIRect>(SkIRect::MakeEmpty()); }
// Parrot the SkIRect API while preserving coord space
bool isEmpty() const { return fData.isEmpty(); }
@ -378,6 +380,8 @@ public:
explicit LayerSpace(SkRect&& geometry) : fData(std::move(geometry)) {}
explicit operator const SkRect&() const { return fData; }
static LayerSpace<SkRect> Empty() { return LayerSpace<SkRect>(SkRect::MakeEmpty()); }
// Parrot the SkRect API while preserving coord space and usage
bool isEmpty() const { return fData.isEmpty(); }
@ -395,6 +399,9 @@ public:
LayerSpace<SkSize> size() const {
return LayerSpace<SkSize>(SkSize::Make(fData.width(), fData.height()));
}
LayerSpace<SkIRect> round() const { return LayerSpace<SkIRect>(fData.round()); }
LayerSpace<SkIRect> roundIn() const { return LayerSpace<SkIRect>(fData.roundIn()); }
LayerSpace<SkIRect> roundOut() const { return LayerSpace<SkIRect>(fData.roundOut()); }
bool intersect(const LayerSpace<SkRect>& r) { return fData.intersect(r.fData); }
@ -528,6 +535,17 @@ public:
// Get the layer-space coordinate of this image's top left pixel.
const LayerSpace<SkIPoint>& layerOrigin() const { return fOrigin; }
// Produce a new FilterResult that can correctly cover 'newBounds' when it's drawn with its
// tile mode at its origin. When possible, the returned FilterResult can reuse the same image
// data and adjust its access subset, origin, and tile mode. If 'forcePad' is true or if the
// 'newTileMode' that applies at the 'newBounds' geometry is incompatible with the current
// bounds and tile mode, then a new image is created that resolves this image's data and tiling.
// TODO (michaelludwig): All FilterResults are decal mode and there are no current usages that
// require force-padding a decal FilterResult so these arguments aren't implemented yet.
FilterResult resolveToBounds(const LayerSpace<SkIRect>& newBounds) const;
// SkTileMode newTileMode=SkTileMode::kDecal,
// bool forcePad=false) const;
// Extract image and origin, safely when the image is null.
// TODO (michaelludwig) - This is intended for convenience until all call sites of
// SkImageFilter_Base::filterImage() have been updated to work in the new type system
@ -545,7 +563,8 @@ public:
private:
sk_sp<SkSpecialImage> fImage;
LayerSpace<SkIPoint> fOrigin;
LayerSpace<SkIPoint> fOrigin;
// SkTileMode fTileMode = SkTileMode::kDecal;
};
// The context contains all necessary information to describe how the image filter should be

View File

@ -203,7 +203,9 @@ protected:
void flatten(SkWriteBuffer&) const override;
// DEPRECATED - Use the private context-only variant
virtual sk_sp<SkSpecialImage> onFilterImage(const Context&, SkIPoint* offset) const = 0;
virtual sk_sp<SkSpecialImage> onFilterImage(const Context&, SkIPoint* offset) const {
return nullptr;
}
// DEPRECATED - Override onGetOutputLayerBounds and onGetInputLayerBounds instead. The
// node-specific and aggregation functions are no longer separated in the current API. A helper
@ -469,6 +471,7 @@ void SkRegisterBlendImageFilterFlattenable();
void SkRegisterBlurImageFilterFlattenable();
void SkRegisterColorFilterImageFilterFlattenable();
void SkRegisterComposeImageFilterFlattenable();
void SkRegisterCropImageFilterFlattenable();
void SkRegisterDisplacementMapImageFilterFlattenable();
void SkRegisterDropShadowImageFilterFlattenable();
void SkRegisterImageImageFilterFlattenable();

View File

@ -0,0 +1,174 @@
/*
* Copyright 2021 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "src/effects/imagefilters/SkCropImageFilter.h"
#include "src/core/SkImageFilter_Base.h"
#include "src/core/SkReadBuffer.h"
#include "src/core/SkValidationUtils.h"
namespace {
class SkCropImageFilter final : public SkImageFilter_Base {
public:
SkCropImageFilter(const SkRect& cropRect, sk_sp<SkImageFilter> input)
: SkImageFilter_Base(&input, 1, /*cropRect=*/nullptr)
, fCropRect(cropRect) {
SkASSERT(cropRect.isFinite());
SkASSERT(cropRect.isSorted());
SkASSERT(!cropRect.isEmpty());
}
SkRect computeFastBounds(const SkRect& bounds) const override;
protected:
void flatten(SkWriteBuffer&) const override;
private:
friend void ::SkRegisterCropImageFilterFlattenable();
SK_FLATTENABLE_HOOKS(SkCropImageFilter)
skif::FilterResult onFilterImage(const skif::Context& context) const override;
skif::LayerSpace<SkIRect> onGetInputLayerBounds(
const skif::Mapping& mapping,
const skif::LayerSpace<SkIRect>& desiredOutput,
const skif::LayerSpace<SkIRect>& contentBounds,
VisitChildren recurse) const override;
skif::LayerSpace<SkIRect> onGetOutputLayerBounds(
const skif::Mapping& mapping,
const skif::LayerSpace<SkIRect>& contentBounds) const override;
// The crop rect is specified in floating point to allow cropping to partial local pixels,
// that could become whole pixels in the layer-space image if the canvas is scaled.
// For now it's always rounded to integer pixels as if it were non-AA.
skif::LayerSpace<SkIRect> cropRect(const skif::Mapping& mapping) const {
// TODO(michaelludwig) legacy code used roundOut() in applyCropRect(). If image diffs are
// incorrect when migrating to this filter, this may need to be adjusted.
return mapping.paramToLayer(fCropRect).round();
}
skif::ParameterSpace<SkRect> fCropRect;
};
} // end namespace
sk_sp<SkImageFilter> SkMakeCropImageFilter(const SkRect& rect, sk_sp<SkImageFilter> input) {
if (rect.isEmpty() || !rect.isFinite()) {
return nullptr;
}
return sk_sp<SkImageFilter>(new SkCropImageFilter(rect, std::move(input)));
}
void SkRegisterCropImageFilterFlattenable() {
SK_REGISTER_FLATTENABLE(SkCropImageFilter);
}
sk_sp<SkFlattenable> SkCropImageFilter::CreateProc(SkReadBuffer& buffer) {
SK_IMAGEFILTER_UNFLATTEN_COMMON(common, 1);
SkRect cropRect = buffer.readRect();
if (!buffer.isValid() || !buffer.validate(SkIsValidRect(cropRect))) {
return nullptr;
}
return SkMakeCropImageFilter(cropRect, common.getInput(0));
}
void SkCropImageFilter::flatten(SkWriteBuffer& buffer) const {
this->SkImageFilter_Base::flatten(buffer);
buffer.writeRect(SkRect(fCropRect));
}
///////////////////////////////////////////////////////////////////////////////////////////////////
skif::FilterResult SkCropImageFilter::onFilterImage(const skif::Context& context) const {
skif::LayerSpace<SkIRect> cropBounds = this->cropRect(context.mapping());
// Limit our crop to just what is necessary for the next stage in the filter pipeline.
if (!cropBounds.intersect(context.desiredOutput())) {
// The output is fully transparent so skip evaluating the child, although in most cases this
// is detected earlier based on getInputLayerBounds() and the entire DAG can be skipped.
// That's not always possible when a parent filter combines a dynamic layer with image
// filters that produce fixed outputs (i.e. source filters).
return {};
}
skif::FilterResult childOutput = this->filterInput(0, context);
// While filterInput() adjusts the context passed to our child filter to account for the
// crop rect and desired output, 'childOutput' does not necessarily fit that exactly. An
// explicit resolve to these bounds ensures the crop is applied and the result is as small as
// possible, and in most cases does not require rendering a new image.
// NOTE - for now, with decal-only tiling, it actually NEVER requires rendering a new image.
return childOutput.resolveToBounds(cropBounds);
}
// TODO(michaelludwig) - onGetInputLayerBounds() and onGetOutputLayerBounds() are tightly coupled
// to both each other's behavior and to onFilterImage(). If onFilterImage() had a concept of a
// dry-run (e.g. FilterResult had null images but tracked the bounds the images would be) then
// onGetInputLayerBounds() is the union of all requested inputs at the leaf nodes of the DAG, and
// onGetOutputLayerBounds() is the bounds of the dry-run result. This might have more overhead, but
// would reduce the complexity of implementations by quite a bit.
skif::LayerSpace<SkIRect> SkCropImageFilter::onGetInputLayerBounds(
const skif::Mapping& mapping,
const skif::LayerSpace<SkIRect>& desiredOutput,
const skif::LayerSpace<SkIRect>& contentBounds,
VisitChildren recurse) const {
// Assuming unbounded desired output, this filter only needs to process an image that's at most
// sized to our crop rect.
skif::LayerSpace<SkIRect> requiredInput = this->cropRect(mapping);
// But we can restrict the crop rect to just what's requested, since anything beyond that won't
// be rendered.
if (!requiredInput.intersect(desiredOutput)) {
// We wouldn't draw anything when filtering, so return empty bounds now to skip a layer.
return skif::LayerSpace<SkIRect>::Empty();
}
if (recurse == VisitChildren::kNo) {
return requiredInput;
} else {
// Our required input is the desired output for our child image filter.
return this->visitInputLayerBounds(mapping, requiredInput, contentBounds);
}
}
skif::LayerSpace<SkIRect> SkCropImageFilter::onGetOutputLayerBounds(
const skif::Mapping& mapping,
const skif::LayerSpace<SkIRect>& contentBounds) const {
// Assuming unbounded child content, our output is a decal-tiled image sized to our crop rect.
skif::LayerSpace<SkIRect> output = this->cropRect(mapping);
// But the child output image is drawn into our output surface with its own decal tiling, which
// may allow the output dimensions to be reduced.
skif::LayerSpace<SkIRect> childOutput = this->visitOutputLayerBounds(mapping, contentBounds);
if (output.intersect(childOutput)) {
return output;
} else {
// Nothing would be drawn into our crop rect, so nothing would be output.
return skif::LayerSpace<SkIRect>::Empty();
}
}
SkRect SkCropImageFilter::computeFastBounds(const SkRect& bounds) const {
// TODO(michaelludwig) - This is conceptually very similar to calling onGetOutputLayerBounds()
// with an identity skif::Mapping (hence why fCropRect can be used directly), but it also does
// not involve any rounding to pixels for both the content bounds or the output.
// FIXME(michaelludwig) - There is a limitation in the current system for "fast bounds", since
// there's no way for the crop image filter to hide the fact that a child affects transparent
// black, so the entire DAG still is treated as if it cannot compute fast bounds. If we migrate
// getOutputLayerBounds() to operate on float rects, and to report infinite bounds for
// nodes that affect transparent black, then fastBounds() and onAffectsTransparentBlack() impls
// can go away entirely. That's not feasible until everything else is migrated onto the new crop
// rect filter and the new APIs.
if (this->getInput(0) && !this->getInput(0)->canComputeFastBounds()) {
// The input bounds to the crop are effectively infinite so the output fills the crop rect.
return SkRect(fCropRect);
}
SkRect inputBounds = this->getInput(0) ? this->getInput(0)->computeFastBounds(bounds) : bounds;
if (!inputBounds.intersect(SkRect(fCropRect))) {
return SkRect::MakeEmpty();
}
return inputBounds;
}

View File

@ -0,0 +1,19 @@
/*
* Copyright 2021 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#ifndef SkCropImageFilter_DEFINED
#define SkCropImageFilter_DEFINED
#include "include/core/SkRefCnt.h"
class SkImageFilter;
struct SkRect;
// TODO (michaelludwig): Move to SkImageFilters::Crop when ready to expose to the public
SK_API sk_sp<SkImageFilter> SkMakeCropImageFilter(const SkRect& rect, sk_sp<SkImageFilter> input);
#endif

View File

@ -129,6 +129,7 @@
SkRegisterBlurImageFilterFlattenable();
SkRegisterColorFilterImageFilterFlattenable();
SkRegisterComposeImageFilterFlattenable();
SkRegisterCropImageFilterFlattenable();
SkRegisterDisplacementMapImageFilterFlattenable();
SkRegisterDropShadowImageFilterFlattenable();
SkRegisterImageImageFilterFlattenable();