blob: e84533afb149d2765cfb6a8f4e0fbe049611a18d [file] [log] [blame]
/*
* Copyright 2012 The Android Open Source Project
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "include/core/SkAlphaType.h"
#include "include/core/SkBitmap.h"
#include "include/core/SkBlendMode.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkColor.h"
#include "include/core/SkColorType.h"
#include "include/core/SkFlattenable.h"
#include "include/core/SkImageFilter.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkPaint.h"
#include "include/core/SkPoint.h"
#include "include/core/SkRect.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkSamplingOptions.h"
#include "include/core/SkScalar.h"
#include "include/core/SkShader.h"
#include "include/core/SkSize.h"
#include "include/core/SkTypes.h"
#include "include/effects/SkImageFilters.h"
#include "include/private/base/SkTPin.h"
#include "src/core/SkImageFilterTypes.h"
#include "src/core/SkImageFilter_Base.h"
#include "src/core/SkPicturePriv.h"
#include "src/core/SkReadBuffer.h"
#include "src/core/SkSpecialImage.h"
#include "src/core/SkSpecialSurface.h"
#include "src/core/SkValidationUtils.h"
#include "src/core/SkWriteBuffer.h"
#include "src/effects/imagefilters/SkCropImageFilter.h"
#include <algorithm>
#include <memory>
#include <utility>
#ifdef SK_ENABLE_SKSL
#include "include/core/SkM44.h"
#include "include/core/SkMatrix.h"
#include "include/core/SkTileMode.h"
#include "include/effects/SkRuntimeEffect.h"
#include "src/core/SkRuntimeEffectPriv.h"
#endif
#if defined(SK_GANESH)
#include "src/gpu/ganesh/GrColorSpaceXform.h"
#include "src/gpu/ganesh/GrFragmentProcessor.h"
#include "src/gpu/ganesh/GrSurfaceProxy.h"
#include "src/gpu/ganesh/GrSurfaceProxyView.h"
#include "src/gpu/ganesh/effects/GrSkSLFP.h"
#include "src/gpu/ganesh/effects/GrTextureEffect.h"
#endif
namespace {
// DEPRECATED: This implementation does not perform any bounds calculations, or respect the CTM,
// and only stores part of the state needed to correctly produce the magnifying lens effect. The
// rest of the state is calculated in Chromium and it relies on the fact that this implementation
// breaks the rules for everything to work out in the compositor.
// TODO: Delete this once Chromium has been updated to use the new magnifier factory and impl.
class SkLegacyMagnifierImageFilter final : public SkImageFilter_Base {
public:
SkLegacyMagnifierImageFilter(const SkRect& srcRect, SkScalar inset, sk_sp<SkImageFilter> input,
const SkRect* cropRect)
: INHERITED(&input, 1, cropRect)
, fSrcRect(srcRect)
, fInset(inset) {
SkASSERT(srcRect.left() >= 0 && srcRect.top() >= 0 && inset >= 0);
}
protected:
void flatten(SkWriteBuffer&) const override;
sk_sp<SkSpecialImage> onFilterImage(const Context&, SkIPoint* offset) const override;
private:
friend class SkMagnifierImageFilter; // For CreateProc on out-of-date SKPs
friend void ::SkRegisterMagnifierImageFilterFlattenable();
SK_FLATTENABLE_HOOKS(SkLegacyMagnifierImageFilter)
SkRect fSrcRect;
SkScalar fInset;
using INHERITED = SkImageFilter_Base;
};
class SkMagnifierImageFilter final : public SkImageFilter_Base {
public:
SkMagnifierImageFilter(const SkRect& lensBounds,
float zoomAmount,
float inset,
const SkSamplingOptions& sampling,
sk_sp<SkImageFilter> input)
: SkImageFilter_Base(&input, 1, nullptr)
, fLensBounds(lensBounds)
, fZoomAmount(zoomAmount)
, fInset(inset)
, fSampling(sampling) {}
SkRect computeFastBounds(const SkRect&) const override;
protected:
void flatten(SkWriteBuffer&) const override;
private:
friend void ::SkRegisterMagnifierImageFilterFlattenable();
SK_FLATTENABLE_HOOKS(SkMagnifierImageFilter)
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) const override;
skif::LayerSpace<SkIRect> onGetOutputLayerBounds(
const skif::Mapping& mapping,
const skif::LayerSpace<SkIRect>& contentBounds) const override;
skif::ParameterSpace<SkRect> fLensBounds;
// Zoom is relative so does not belong to a coordinate space, see note in onFilterImage().
float fZoomAmount;
// Inset is really a ParameterSpace<SkSize> where width = height = fInset, but we store just the
// float here for easier serialization and convert to a size in onFilterImage().
float fInset;
SkSamplingOptions fSampling;
};
} // end namespace
sk_sp<SkImageFilter> SkImageFilters::Magnifier(
const SkRect& srcRect, SkScalar inset, sk_sp<SkImageFilter> input,
const CropRect& cropRect) {
if (!SkScalarIsFinite(inset) || !SkIsValidRect(srcRect)) {
return nullptr;
}
if (inset < 0) {
return nullptr;
}
// Negative numbers in src rect are not supported
if (srcRect.fLeft < 0 || srcRect.fTop < 0) {
return nullptr;
}
return sk_sp<SkImageFilter>(new SkLegacyMagnifierImageFilter(srcRect, inset, std::move(input),
cropRect));
}
sk_sp<SkImageFilter> SkImageFilters::Magnifier(const SkRect& lensBounds,
SkScalar zoomAmount,
SkScalar inset,
const SkSamplingOptions& sampling,
sk_sp<SkImageFilter> input,
const CropRect& cropRect) {
if (lensBounds.isEmpty() || !lensBounds.isFinite() ||
zoomAmount <= 0.f || !SkScalarIsFinite(zoomAmount) ||
inset < 0.f || !SkScalarIsFinite(inset)) {
return nullptr; // invalid
}
// The magnifier automatically restricts its output based on the size of the image it receives
// as input, so 'cropRect' only applies to its input.
if (cropRect) {
input = SkMakeCropImageFilter(*cropRect, std::move(input));
}
if (zoomAmount > 1.f) {
return sk_sp<SkImageFilter>(new SkMagnifierImageFilter(lensBounds, zoomAmount, inset,
sampling, std::move(input)));
} else {
// Zooming with a value less than 1 is technically a downscaling, which "works" but the
// non-linear distortion behaves unintuitively. At zoomAmount = 1, this filter is an
// expensive identity function so treat zoomAmount <= 1 as a no-op.
return input;
}
}
void SkRegisterMagnifierImageFilterFlattenable() {
SK_REGISTER_FLATTENABLE(SkMagnifierImageFilter);
SK_REGISTER_FLATTENABLE(SkLegacyMagnifierImageFilter);
// TODO (michaelludwig) - Remove after grace period for SKPs to stop using old name
SkFlattenable::Register("SkMagnifierImageFilterImpl", SkLegacyMagnifierImageFilter::CreateProc);
}
sk_sp<SkFlattenable> SkLegacyMagnifierImageFilter::CreateProc(SkReadBuffer& buffer) {
SK_IMAGEFILTER_UNFLATTEN_COMMON(common, 1);
SkRect src;
buffer.readRect(&src);
return SkImageFilters::Magnifier(src, buffer.readScalar(), common.getInput(0),
common.cropRect());
}
void SkLegacyMagnifierImageFilter::flatten(SkWriteBuffer& buffer) const {
this->INHERITED::flatten(buffer);
buffer.writeRect(fSrcRect);
buffer.writeScalar(fInset);
}
sk_sp<SkFlattenable> SkMagnifierImageFilter::CreateProc(SkReadBuffer& buffer) {
if (buffer.isVersionLT(SkPicturePriv::kRevampMagnifierFilter)) {
// This was actually a legacy magnifier image filter that was serialized.
return SkLegacyMagnifierImageFilter::CreateProc(buffer);
}
SK_IMAGEFILTER_UNFLATTEN_COMMON(common, 1);
SkRect lensBounds;
buffer.readRect(&lensBounds);
SkScalar zoomAmount = buffer.readScalar();
SkScalar inset = buffer.readScalar();
SkSamplingOptions sampling = buffer.readSampling();
return SkImageFilters::Magnifier(lensBounds, zoomAmount, inset, sampling, common.getInput(0));
}
void SkMagnifierImageFilter::flatten(SkWriteBuffer& buffer) const {
this->SkImageFilter_Base::flatten(buffer);
buffer.writeRect(SkRect(fLensBounds));
buffer.writeScalar(fZoomAmount);
buffer.writeScalar(fInset);
buffer.writeSampling(fSampling);
}
////////////////////////////////////////////////////////////////////////////////
#ifdef SK_ENABLE_SKSL
static sk_sp<SkShader> make_magnifier_shader(
const skif::Context& context,
const skif::FilterResult& input,
const SkSamplingOptions& sampling,
const skif::LayerSpace<SkRect>& lensBounds,
const skif::LayerSpace<SkRect>& srcRect,
const skif::LayerSpace<SkSize>& inset) {
static const SkRuntimeEffect* effect = SkMakeRuntimeEffect(SkRuntimeEffect::MakeForShader,
"uniform shader src;"
"uniform float4 lensBounds;"
"uniform float4 zoomXform;"
"uniform float2 invInset;"
"half4 main(float2 coord) {"
"float2 zoomCoord = zoomXform.xy + zoomXform.zw*coord;"
// edgeInset is the smallest distance to the lens bounds edges,
// in units of "insets".
"float2 edgeInset = min(coord - lensBounds.xy, lensBounds.zw - coord) * invInset;"
// The equations for 'weight' ensure that it is 0 along the outside of lensBounds so
// it seams with any un-zoomed, un-filtered content. The zoomed content fills a rounded
// rectangle that is 1 "inset" in from lensBounds with circular corners with radii
// equal to the inset distance. Outside of this region, there is a non-linear weighting
// to compress the un-zoomed content to the zoomed content. The critical zone about
// each corner is limited to 2x"inset" square.
"float weight = (edgeInset.x < 2.0 && edgeInset.y < 2.0)"
// Circular distortion weighted by distance to inset corner
"? (2.0 - length(2.0 - edgeInset))"
// Linear zoom, or single-axis compression outside of the inset area (if delta < 1)
": min(edgeInset.x, edgeInset.y);"
// Saturate before squaring so that negative weights are clamped to 0 before squaring
"weight = saturate(weight);"
"return src.eval(mix(coord, zoomCoord, weight*weight));"
"}"
);
// TODO: FilterResult or FilterBuilder should hide the details of turning a FilterResult into
// an SkShader (and possibly wrap binding the input for an SkRuntimeEffect, too).
SkIPoint inputOrigin;
sk_sp<SkSpecialImage> inputImage = input.imageAndOffset(context, &inputOrigin);
if (!inputImage) {
return nullptr;
}
sk_sp<SkShader> inputShader = inputImage->asShader(
SkTileMode::kDecal, sampling, SkMatrix::Translate(inputOrigin.fX, inputOrigin.fY));
if (!inputShader) {
return nullptr;
}
SkRuntimeShaderBuilder builder(sk_ref_sp(effect));
builder.child("src") = std::move(inputShader);
SkASSERT(inset.width() > 0.f && inset.height() > 0.f);
auto zoomXform = skif::LayerSpace<SkMatrix>::RectToRect(lensBounds, srcRect);
builder.uniform("lensBounds") = SkRect(lensBounds);
builder.uniform("zoomXform") = SkV4{/*Tx*/zoomXform.rc(0, 2), /*Ty*/zoomXform.rc(1, 2),
/*Sx*/zoomXform.rc(0, 0), /*Sy*/zoomXform.rc(1, 1)};
builder.uniform("invInset") = SkV2{1.f / inset.width(),
1.f / inset.height()};
return builder.makeShader();
}
#endif // SK_ENABLE_SKSL
#if defined(SK_GANESH)
static std::unique_ptr<GrFragmentProcessor> make_magnifier_fp(
std::unique_ptr<GrFragmentProcessor> input,
SkIRect bounds,
SkRect srcRect,
float xInvZoom,
float yInvZoom,
float xInvInset,
float yInvInset) {
static const SkRuntimeEffect* effect = SkMakeRuntimeEffect(SkRuntimeEffect::MakeForShader,
"uniform shader src;"
"uniform float4 boundsUniform;"
"uniform float xInvZoom;"
"uniform float yInvZoom;"
"uniform float xInvInset;"
"uniform float yInvInset;"
"uniform half2 offset;"
"half4 main(float2 coord) {"
"float2 zoom_coord = offset + coord * float2(xInvZoom, yInvZoom);"
"float2 delta = (coord - boundsUniform.xy) * boundsUniform.zw;"
"delta = min(delta, float2(1.0) - delta);"
"delta *= float2(xInvInset, yInvInset);"
"float weight = 0.0;"
"if (delta.s < 2.0 && delta.t < 2.0) {"
"delta = float2(2.0) - delta;"
"float dist = length(delta);"
"dist = max(2.0 - dist, 0.0);"
"weight = min(dist * dist, 1.0);"
"} else {"
"float2 delta_squared = delta * delta;"
"weight = min(min(delta_squared.x, delta_squared.y), 1.0);"
"}"
"return src.eval(mix(coord, zoom_coord, weight));"
"}"
);
SkV4 boundsUniform = {static_cast<float>(bounds.x()),
static_cast<float>(bounds.y()),
1.f / bounds.width(),
1.f / bounds.height()};
return GrSkSLFP::Make(effect, "magnifier_fp", /*inputFP=*/nullptr, GrSkSLFP::OptFlags::kNone,
"src", std::move(input),
"boundsUniform", boundsUniform,
"xInvZoom", xInvZoom,
"yInvZoom", yInvZoom,
"xInvInset", xInvInset,
"yInvInset", yInvInset,
"offset", SkV2{srcRect.x(), srcRect.y()});
}
#endif
sk_sp<SkSpecialImage> SkLegacyMagnifierImageFilter::onFilterImage(const Context& ctx,
SkIPoint* offset) const {
SkIPoint inputOffset = SkIPoint::Make(0, 0);
sk_sp<SkSpecialImage> input(this->filterInput(0, ctx, &inputOffset));
if (!input) {
return nullptr;
}
const SkIRect inputBounds = SkIRect::MakeXYWH(inputOffset.x(), inputOffset.y(),
input->width(), input->height());
SkIRect bounds;
if (!this->applyCropRect(ctx, inputBounds, &bounds)) {
return nullptr;
}
SkScalar invInset = fInset > 0 ? SkScalarInvert(fInset) : SK_Scalar1;
SkScalar invXZoom = fSrcRect.width() / bounds.width();
SkScalar invYZoom = fSrcRect.height() / bounds.height();
#if defined(SK_GANESH)
if (ctx.gpuBacked()) {
auto context = ctx.getContext();
GrSurfaceProxyView inputView = input->view(context);
SkASSERT(inputView.asTextureProxy());
const auto isProtected = inputView.proxy()->isProtected();
const auto origin = inputView.origin();
offset->fX = bounds.left();
offset->fY = bounds.top();
bounds.offset(-inputOffset);
// Map bounds and srcRect into the proxy space. Due to the zoom effect,
// it's not just an offset for fSrcRect.
bounds.offset(input->subset().x(), input->subset().y());
SkRect srcRect = fSrcRect.makeOffset((1.f - invXZoom) * input->subset().x(),
(1.f - invYZoom) * input->subset().y());
auto inputFP = GrTextureEffect::Make(std::move(inputView), kPremul_SkAlphaType);
auto fp = make_magnifier_fp(std::move(inputFP),
bounds,
srcRect,
invXZoom,
invYZoom,
bounds.width() * invInset,
bounds.height() * invInset);
fp = GrColorSpaceXformEffect::Make(std::move(fp),
input->getColorSpace(), input->alphaType(),
ctx.colorSpace(), kPremul_SkAlphaType);
if (!fp) {
return nullptr;
}
return DrawWithFP(context, std::move(fp), bounds, ctx.colorType(), ctx.colorSpace(),
ctx.surfaceProps(), origin, isProtected);
}
#endif
SkBitmap inputBM;
if (!input->getROPixels(&inputBM)) {
return nullptr;
}
if ((inputBM.colorType() != kN32_SkColorType) ||
(fSrcRect.width() >= inputBM.width()) || (fSrcRect.height() >= inputBM.height())) {
return nullptr;
}
SkASSERT(inputBM.getPixels());
if (!inputBM.getPixels() || inputBM.width() <= 0 || inputBM.height() <= 0) {
return nullptr;
}
const SkImageInfo info = SkImageInfo::MakeN32Premul(bounds.width(), bounds.height());
SkBitmap dst;
if (!dst.tryAllocPixels(info)) {
return nullptr;
}
SkColor* dptr = dst.getAddr32(0, 0);
int dstWidth = dst.width(), dstHeight = dst.height();
for (int y = 0; y < dstHeight; ++y) {
for (int x = 0; x < dstWidth; ++x) {
SkScalar x_dist = std::min(x, dstWidth - x - 1) * invInset;
SkScalar y_dist = std::min(y, dstHeight - y - 1) * invInset;
SkScalar weight = 0;
static const SkScalar kScalar2 = SkScalar(2);
// To create a smooth curve at the corners, we need to work on
// a square twice the size of the inset.
if (x_dist < kScalar2 && y_dist < kScalar2) {
x_dist = kScalar2 - x_dist;
y_dist = kScalar2 - y_dist;
SkScalar dist = SkScalarSqrt(SkScalarSquare(x_dist) +
SkScalarSquare(y_dist));
dist = std::max(kScalar2 - dist, 0.0f);
// SkTPin rather than std::max to handle potential NaN
weight = SkTPin(SkScalarSquare(dist), 0.0f, SK_Scalar1);
} else {
SkScalar sqDist = std::min(SkScalarSquare(x_dist),
SkScalarSquare(y_dist));
// SkTPin rather than std::max to handle potential NaN
weight = SkTPin(sqDist, 0.0f, SK_Scalar1);
}
SkScalar x_interp = weight * (fSrcRect.x() + x * invXZoom) + (1 - weight) * x;
SkScalar y_interp = weight * (fSrcRect.y() + y * invYZoom) + (1 - weight) * y;
int x_val = SkTPin(bounds.x() + SkScalarFloorToInt(x_interp), 0, inputBM.width() - 1);
int y_val = SkTPin(bounds.y() + SkScalarFloorToInt(y_interp), 0, inputBM.height() - 1);
*dptr = *inputBM.getAddr32(x_val, y_val);
dptr++;
}
}
offset->fX = bounds.left();
offset->fY = bounds.top();
return SkSpecialImage::MakeFromRaster(SkIRect::MakeWH(bounds.width(), bounds.height()),
dst, ctx.surfaceProps());
}
////////////////////////////////////////////////////////////////////////////////
skif::FilterResult SkMagnifierImageFilter::onFilterImage(const skif::Context& context) const {
skif::LayerSpace<SkRect> lensBounds = context.mapping().paramToLayer(fLensBounds);
skif::LayerSpace<SkPoint> zoomCenter = lensBounds.center();
skif::FilterResult childOutput =
this->getChildOutput(0, context.withNewDesiredOutput(lensBounds.roundOut()));
// If lensBounds is not partially off screen, 'childOutput' should exactly match the layer-space
// lens bounds. However, when this is used as a backdrop filter, or if there was a crop on the
// input, this may not be the case. Stylistically, this filter adjusts the lens bounds and
// zoomed-in content such that the non-linear inset does not extend beyond what was provided.
// This avoids zooming in on a clamped texture boundary.
if (!lensBounds.intersect(skif::LayerSpace<SkRect>(childOutput.layerBounds()))) {
return {};
}
// Clamp the zoom center to be within the childOutput image
zoomCenter = lensBounds.clamp(zoomCenter);
// The zoom we want to apply in layer-space is equal to
// mapping.paramToLayer(SkMatrix::Scale(fZoomAmount)).decomposeScale(&layerZoom).
// Because this filter only supports scale+translate matrices, the paramToLayer transform of
// the parameter-space scale matrix is a no-op. Thus layerZoom == fZoomAmount and we can avoid
// all of that math. This assumption is invalid if the matrix complexity is more than S+T.
SkASSERT(this->getCTMCapability() == MatrixCapability::kScaleTranslate);
float invZoom = 1.f / fZoomAmount;
// The srcRect is the bounding box of the pixels that are linearly scaled up, about zoomCenter.
// This is not the visual bounds of this upscaled region, but the bounds of the source pixels
// that will fill the main magnified region (which is simply the inset of lensBounds). When
// lensBounds has not been cropped by the actual input image, these equations are identical to
// the more intuitive L/R = center.x -/+ width/(2*zoom) and T/B = center.y -/+ height/(2*zoom).
// However, when lensBounds is cropped this automatically shifts the source rectangle away from
// the original zoom center such that the upscaled area is contained within the input image.
skif::LayerSpace<SkRect> srcRect{{
lensBounds.left() * invZoom + zoomCenter.x()*(1.f - invZoom),
lensBounds.top() * invZoom + zoomCenter.y()*(1.f - invZoom),
lensBounds.right() * invZoom + zoomCenter.x()*(1.f - invZoom),
lensBounds.bottom()* invZoom + zoomCenter.y()*(1.f - invZoom)}};
// When there is no SkSL support, or there's a 0 inset, the magnifier is equivalent to a
// rect->rect transform and crop.
#ifdef SK_ENABLE_SKSL
skif::LayerSpace<SkSize> inset = context.mapping().paramToLayer(
skif::ParameterSpace<SkSize>({fInset, fInset}));
if (inset.width() <= 0.f || inset.height() <= 0.f)
#endif
{
// NOTE: We crop back down to srcRect because we requested an unclipped lensBounds from the
// child filter. Since srcRect is dependent on the clipped lensBounds from what the child
// actually produced, we can't just request an unclipped srcRect initially.
auto zoomXform = skif::LayerSpace<SkMatrix>::RectToRect(srcRect, lensBounds);
return childOutput.applyCrop(context, srcRect.roundOut())
.applyTransform(context, zoomXform, fSampling);
}
#ifdef SK_ENABLE_SKSL
// TODO: FilterResult will eventually have a builder API to hide a lot of this boilerplate,
// since it will likely be the same for many other image filter implementations. The magnifier
// filter is just the first port to FilterResult that doesn't rely on applying meta transforms.
skif::LayerSpace<SkIRect> outputBounds = lensBounds.roundOut();
sk_sp<SkSpecialSurface> surf = context.makeSurface(SkISize(outputBounds.size()));
if (!surf) {
return {};
}
SkCanvas* canvas = surf->getCanvas();
canvas->translate(-outputBounds.left(), -outputBounds.top());
SkPaint paint;
paint.setBlendMode(SkBlendMode::kSrc);
paint.setShader(make_magnifier_shader(context, childOutput, fSampling,
lensBounds, srcRect, inset));
canvas->drawPaint(paint);
return {surf->makeImageSnapshot(), outputBounds.topLeft()};
#endif
}
skif::LayerSpace<SkIRect> SkMagnifierImageFilter::onGetInputLayerBounds(
const skif::Mapping& mapping,
const skif::LayerSpace<SkIRect>& desiredOutput,
const skif::LayerSpace<SkIRect>& contentBounds) const {
// The required input is always the lens bounds. The filter distorts the pixels contained within
// these bounds to zoom in on a portion of it, depending on the inset and zoom amount. However,
// it adjusts the region based on cropping that occurs between what's requested and what's
// provided. Theoretically it's possible that we could restrict the required input by the
// desired output, but that cropping should not adjust the zoom region or inset. This is non
// trivial to separate and is an unlikely use case so for now just require fLensBounds.
skif::LayerSpace<SkIRect> requiredInput = mapping.paramToLayer(fLensBounds).roundOut();
// Our required input is the desired output for our child image filter.
return this->getChildInputLayerBounds(0, mapping, requiredInput, contentBounds);
}
skif::LayerSpace<SkIRect> SkMagnifierImageFilter::onGetOutputLayerBounds(
const skif::Mapping& mapping,
const skif::LayerSpace<SkIRect>& contentBounds) const {
// The output of this filter is fLensBounds intersected with its child's output.
skif::LayerSpace<SkIRect> output = this->getChildOutputLayerBounds(0, mapping, contentBounds);
if (output.intersect(mapping.paramToLayer(fLensBounds).roundOut())) {
return output;
} else {
// Nothing to magnify
return skif::LayerSpace<SkIRect>::Empty();
}
}
SkRect SkMagnifierImageFilter::computeFastBounds(const SkRect& src) const {
SkRect bounds = this->getInput(0) ? this->getInput(0)->computeFastBounds(src) : src;
if (bounds.intersect(SkRect(fLensBounds))) {
return bounds;
} else {
return SkRect::MakeEmpty();
}
}