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