blob: c4ad58d6ee065d7304a4d1124e7c58ab04cdc902 [file] [log] [blame]
/*
* 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 "include/core/SkFlattenable.h"
#include "include/core/SkImageFilter.h"
#include "include/core/SkRect.h"
#include "src/core/SkImageFilterTypes.h"
#include "src/core/SkImageFilter_Base.h"
#include "src/core/SkReadBuffer.h"
#include "src/core/SkValidationUtils.h"
#include "src/core/SkWriteBuffer.h"
#include <utility>
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());
}
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 {
return mapping.paramToLayer(fCropRect).roundOut();
}
skif::ParameterSpace<SkRect> fCropRect;
};
} // end namespace
sk_sp<SkImageFilter> SkMakeCropImageFilter(const SkRect& rect, sk_sp<SkImageFilter> input) {
if (!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());
if (cropBounds.isEmpty()) {
// Don't bother evaluating the input filter if the crop wouldn't show anything
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. Calling
// applyCrop() ensures this is true, optimally avoiding rendering a new image if possible.
return childOutput.applyCrop(context, 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;
}