1 /*
2 * Copyright 2021 Google LLC
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8 #include "src/effects/imagefilters/SkCropImageFilter.h"
9
10 #include "include/core/SkFlattenable.h"
11 #include "include/core/SkImageFilter.h"
12 #include "include/core/SkRect.h"
13 #include "src/core/SkImageFilterTypes.h"
14 #include "src/core/SkImageFilter_Base.h"
15 #include "src/core/SkReadBuffer.h"
16 #include "src/core/SkValidationUtils.h"
17 #include "src/core/SkWriteBuffer.h"
18
19 #include <utility>
20
21 namespace {
22
23 class SkCropImageFilter final : public SkImageFilter_Base {
24 public:
SkCropImageFilter(const SkRect & cropRect,sk_sp<SkImageFilter> input)25 SkCropImageFilter(const SkRect& cropRect, sk_sp<SkImageFilter> input)
26 : SkImageFilter_Base(&input, 1, /*cropRect=*/nullptr)
27 , fCropRect(cropRect) {
28 SkASSERT(cropRect.isFinite());
29 SkASSERT(cropRect.isSorted());
30 }
31
32 SkRect computeFastBounds(const SkRect& bounds) const override;
33
34 protected:
35 void flatten(SkWriteBuffer&) const override;
36
37 private:
38 friend void ::SkRegisterCropImageFilterFlattenable();
39 SK_FLATTENABLE_HOOKS(SkCropImageFilter)
40
41 skif::FilterResult onFilterImage(const skif::Context& context) const override;
42
43 skif::LayerSpace<SkIRect> onGetInputLayerBounds(
44 const skif::Mapping& mapping,
45 const skif::LayerSpace<SkIRect>& desiredOutput,
46 const skif::LayerSpace<SkIRect>& contentBounds,
47 VisitChildren recurse) const override;
48
49 skif::LayerSpace<SkIRect> onGetOutputLayerBounds(
50 const skif::Mapping& mapping,
51 const skif::LayerSpace<SkIRect>& contentBounds) const override;
52
53 // The crop rect is specified in floating point to allow cropping to partial local pixels,
54 // that could become whole pixels in the layer-space image if the canvas is scaled.
55 // For now it's always rounded to integer pixels as if it were non-AA.
cropRect(const skif::Mapping & mapping) const56 skif::LayerSpace<SkIRect> cropRect(const skif::Mapping& mapping) const {
57 return mapping.paramToLayer(fCropRect).roundOut();
58 }
59
60 skif::ParameterSpace<SkRect> fCropRect;
61 };
62
63 } // end namespace
64
SkMakeCropImageFilter(const SkRect & rect,sk_sp<SkImageFilter> input)65 sk_sp<SkImageFilter> SkMakeCropImageFilter(const SkRect& rect, sk_sp<SkImageFilter> input) {
66 if (!rect.isFinite()) {
67 return nullptr;
68 }
69 return sk_sp<SkImageFilter>(new SkCropImageFilter(rect, std::move(input)));
70 }
71
SkRegisterCropImageFilterFlattenable()72 void SkRegisterCropImageFilterFlattenable() {
73 SK_REGISTER_FLATTENABLE(SkCropImageFilter);
74 }
75
CreateProc(SkReadBuffer & buffer)76 sk_sp<SkFlattenable> SkCropImageFilter::CreateProc(SkReadBuffer& buffer) {
77 SK_IMAGEFILTER_UNFLATTEN_COMMON(common, 1);
78 SkRect cropRect = buffer.readRect();
79 if (!buffer.isValid() || !buffer.validate(SkIsValidRect(cropRect))) {
80 return nullptr;
81 }
82 return SkMakeCropImageFilter(cropRect, common.getInput(0));
83 }
84
flatten(SkWriteBuffer & buffer) const85 void SkCropImageFilter::flatten(SkWriteBuffer& buffer) const {
86 this->SkImageFilter_Base::flatten(buffer);
87 buffer.writeRect(SkRect(fCropRect));
88 }
89
90 ///////////////////////////////////////////////////////////////////////////////////////////////////
91
onFilterImage(const skif::Context & context) const92 skif::FilterResult SkCropImageFilter::onFilterImage(const skif::Context& context) const {
93 skif::LayerSpace<SkIRect> cropBounds = this->cropRect(context.mapping());
94 if (cropBounds.isEmpty()) {
95 // Don't bother evaluating the input filter if the crop wouldn't show anything
96 return {};
97 }
98
99 skif::FilterResult childOutput = this->filterInput(0, context);
100 // While filterInput() adjusts the context passed to our child filter to account for the
101 // crop rect and desired output, 'childOutput' does not necessarily fit that exactly. Calling
102 // applyCrop() ensures this is true, optimally avoiding rendering a new image if possible.
103 return childOutput.applyCrop(context, cropBounds);
104 }
105
106 // TODO(michaelludwig) - onGetInputLayerBounds() and onGetOutputLayerBounds() are tightly coupled
107 // to both each other's behavior and to onFilterImage(). If onFilterImage() had a concept of a
108 // dry-run (e.g. FilterResult had null images but tracked the bounds the images would be) then
109 // onGetInputLayerBounds() is the union of all requested inputs at the leaf nodes of the DAG, and
110 // onGetOutputLayerBounds() is the bounds of the dry-run result. This might have more overhead, but
111 // would reduce the complexity of implementations by quite a bit.
onGetInputLayerBounds(const skif::Mapping & mapping,const skif::LayerSpace<SkIRect> & desiredOutput,const skif::LayerSpace<SkIRect> & contentBounds,VisitChildren recurse) const112 skif::LayerSpace<SkIRect> SkCropImageFilter::onGetInputLayerBounds(
113 const skif::Mapping& mapping,
114 const skif::LayerSpace<SkIRect>& desiredOutput,
115 const skif::LayerSpace<SkIRect>& contentBounds,
116 VisitChildren recurse) const {
117 // Assuming unbounded desired output, this filter only needs to process an image that's at most
118 // sized to our crop rect.
119 skif::LayerSpace<SkIRect> requiredInput = this->cropRect(mapping);
120 // But we can restrict the crop rect to just what's requested, since anything beyond that won't
121 // be rendered.
122 if (!requiredInput.intersect(desiredOutput)) {
123 // We wouldn't draw anything when filtering, so return empty bounds now to skip a layer.
124 return skif::LayerSpace<SkIRect>::Empty();
125 }
126
127 if (recurse == VisitChildren::kNo) {
128 return requiredInput;
129 } else {
130 // Our required input is the desired output for our child image filter.
131 return this->visitInputLayerBounds(mapping, requiredInput, contentBounds);
132 }
133 }
134
onGetOutputLayerBounds(const skif::Mapping & mapping,const skif::LayerSpace<SkIRect> & contentBounds) const135 skif::LayerSpace<SkIRect> SkCropImageFilter::onGetOutputLayerBounds(
136 const skif::Mapping& mapping,
137 const skif::LayerSpace<SkIRect>& contentBounds) const {
138 // Assuming unbounded child content, our output is a decal-tiled image sized to our crop rect.
139 skif::LayerSpace<SkIRect> output = this->cropRect(mapping);
140 // But the child output image is drawn into our output surface with its own decal tiling, which
141 // may allow the output dimensions to be reduced.
142 skif::LayerSpace<SkIRect> childOutput = this->visitOutputLayerBounds(mapping, contentBounds);
143
144 if (output.intersect(childOutput)) {
145 return output;
146 } else {
147 // Nothing would be drawn into our crop rect, so nothing would be output.
148 return skif::LayerSpace<SkIRect>::Empty();
149 }
150 }
151
computeFastBounds(const SkRect & bounds) const152 SkRect SkCropImageFilter::computeFastBounds(const SkRect& bounds) const {
153 // TODO(michaelludwig) - This is conceptually very similar to calling onGetOutputLayerBounds()
154 // with an identity skif::Mapping (hence why fCropRect can be used directly), but it also does
155 // not involve any rounding to pixels for both the content bounds or the output.
156 // FIXME(michaelludwig) - There is a limitation in the current system for "fast bounds", since
157 // there's no way for the crop image filter to hide the fact that a child affects transparent
158 // black, so the entire DAG still is treated as if it cannot compute fast bounds. If we migrate
159 // getOutputLayerBounds() to operate on float rects, and to report infinite bounds for
160 // nodes that affect transparent black, then fastBounds() and onAffectsTransparentBlack() impls
161 // can go away entirely. That's not feasible until everything else is migrated onto the new crop
162 // rect filter and the new APIs.
163 if (this->getInput(0) && !this->getInput(0)->canComputeFastBounds()) {
164 // The input bounds to the crop are effectively infinite so the output fills the crop rect.
165 return SkRect(fCropRect);
166 }
167
168 SkRect inputBounds = this->getInput(0) ? this->getInput(0)->computeFastBounds(bounds) : bounds;
169 if (!inputBounds.intersect(SkRect(fCropRect))) {
170 return SkRect::MakeEmpty();
171 }
172 return inputBounds;
173 }
174