xref: /aosp_15_r20/external/skia/src/effects/imagefilters/SkMagnifierImageFilter.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1 /*
2  * Copyright 2012 The Android Open Source Project
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 "include/effects/SkImageFilters.h"
9 
10 #include "include/core/SkFlattenable.h"
11 #include "include/core/SkImageFilter.h"
12 #include "include/core/SkM44.h"
13 #include "include/core/SkMatrix.h"
14 #include "include/core/SkPoint.h"
15 #include "include/core/SkRect.h"
16 #include "include/core/SkRefCnt.h"
17 #include "include/core/SkSamplingOptions.h"
18 #include "include/core/SkScalar.h"
19 #include "include/core/SkShader.h"
20 #include "include/core/SkSize.h"
21 #include "include/core/SkSpan.h"
22 #include "include/core/SkTypes.h"
23 #include "include/effects/SkRuntimeEffect.h"
24 #include "include/private/base/SkFloatingPoint.h"
25 #include "src/core/SkImageFilterTypes.h"
26 #include "src/core/SkImageFilter_Base.h"
27 #include "src/core/SkKnownRuntimeEffects.h"
28 #include "src/core/SkPicturePriv.h"
29 #include "src/core/SkReadBuffer.h"
30 #include "src/core/SkWriteBuffer.h"
31 
32 #include <algorithm>
33 #include <optional>
34 #include <utility>
35 
36 namespace {
37 
38 class SkMagnifierImageFilter final : public SkImageFilter_Base {
39 public:
SkMagnifierImageFilter(const SkRect & lensBounds,float zoomAmount,float inset,const SkSamplingOptions & sampling,sk_sp<SkImageFilter> input)40     SkMagnifierImageFilter(const SkRect& lensBounds,
41                            float zoomAmount,
42                            float inset,
43                            const SkSamplingOptions& sampling,
44                            sk_sp<SkImageFilter> input)
45         : SkImageFilter_Base(&input, 1)
46         , fLensBounds(lensBounds)
47         , fZoomAmount(zoomAmount)
48         , fInset(inset)
49         , fSampling(sampling) {}
50 
51     SkRect computeFastBounds(const SkRect&) const override;
52 
53 protected:
54     void flatten(SkWriteBuffer&) const override;
55 
56 private:
57     friend void ::SkRegisterMagnifierImageFilterFlattenable();
58     SK_FLATTENABLE_HOOKS(SkMagnifierImageFilter)
59 
60     skif::FilterResult onFilterImage(const skif::Context& context) const override;
61 
62     skif::LayerSpace<SkIRect> onGetInputLayerBounds(
63             const skif::Mapping& mapping,
64             const skif::LayerSpace<SkIRect>& desiredOutput,
65             std::optional<skif::LayerSpace<SkIRect>> contentBounds) const override;
66 
67     std::optional<skif::LayerSpace<SkIRect>> onGetOutputLayerBounds(
68             const skif::Mapping& mapping,
69             std::optional<skif::LayerSpace<SkIRect>> contentBounds) const override;
70 
71     skif::ParameterSpace<SkRect> fLensBounds;
72     // Zoom is relative so does not belong to a coordinate space, see note in onFilterImage().
73     float fZoomAmount;
74     // Inset is really a ParameterSpace<SkSize> where width = height = fInset, but we store just the
75     // float here for easier serialization and convert to a size in onFilterImage().
76     float fInset;
77     SkSamplingOptions fSampling;
78 };
79 
80 } // end namespace
81 
Magnifier(const SkRect & lensBounds,SkScalar zoomAmount,SkScalar inset,const SkSamplingOptions & sampling,sk_sp<SkImageFilter> input,const CropRect & cropRect)82 sk_sp<SkImageFilter> SkImageFilters::Magnifier(const SkRect& lensBounds,
83                                                SkScalar zoomAmount,
84                                                SkScalar inset,
85                                                const SkSamplingOptions& sampling,
86                                                sk_sp<SkImageFilter> input,
87                                                const CropRect& cropRect) {
88     if (lensBounds.isEmpty() || !lensBounds.isFinite() ||
89         zoomAmount <= 0.f || inset < 0.f ||
90         !SkIsFinite(zoomAmount, inset)) {
91         return nullptr; // invalid
92     }
93     // The magnifier automatically restricts its output based on the size of the image it receives
94     // as input, so 'cropRect' only applies to its input.
95     if (cropRect) {
96         input = SkImageFilters::Crop(*cropRect, std::move(input));
97     }
98 
99     if (zoomAmount > 1.f) {
100         return sk_sp<SkImageFilter>(new SkMagnifierImageFilter(lensBounds, zoomAmount, inset,
101                                                                sampling, std::move(input)));
102     } else {
103         // Zooming with a value less than 1 is technically a downscaling, which "works" but the
104         // non-linear distortion behaves unintuitively. At zoomAmount = 1, this filter is an
105         // expensive identity function so treat zoomAmount <= 1 as a no-op.
106         return input;
107     }
108 }
109 
SkRegisterMagnifierImageFilterFlattenable()110 void SkRegisterMagnifierImageFilterFlattenable() {
111     SK_REGISTER_FLATTENABLE(SkMagnifierImageFilter);
112 }
113 
CreateProc(SkReadBuffer & buffer)114 sk_sp<SkFlattenable> SkMagnifierImageFilter::CreateProc(SkReadBuffer& buffer) {
115     if (buffer.isVersionLT(SkPicturePriv::kRevampMagnifierFilter)) {
116         // This was actually a legacy magnifier image filter that was serialized. Chrome is the
117         // only known client of the magnifier and its not used on webpages, so there shouldn't be
118         // SKPs that actually contain a flattened magnifier filter (legacy or new).
119         return nullptr;
120     }
121 
122     SK_IMAGEFILTER_UNFLATTEN_COMMON(common, 1);
123 
124     SkRect lensBounds;
125     buffer.readRect(&lensBounds);
126     SkScalar zoomAmount = buffer.readScalar();
127     SkScalar inset = buffer.readScalar();
128     SkSamplingOptions sampling = buffer.readSampling();
129     return SkImageFilters::Magnifier(lensBounds, zoomAmount, inset, sampling, common.getInput(0));
130 }
131 
flatten(SkWriteBuffer & buffer) const132 void SkMagnifierImageFilter::flatten(SkWriteBuffer& buffer) const {
133     this->SkImageFilter_Base::flatten(buffer);
134     buffer.writeRect(SkRect(fLensBounds));
135     buffer.writeScalar(fZoomAmount);
136     buffer.writeScalar(fInset);
137     buffer.writeSampling(fSampling);
138 }
139 
140 ////////////////////////////////////////////////////////////////////////////////
141 
make_magnifier_shader(sk_sp<SkShader> input,const skif::LayerSpace<SkRect> & lensBounds,const skif::LayerSpace<SkMatrix> & zoomXform,const skif::LayerSpace<SkSize> & inset)142 static sk_sp<SkShader> make_magnifier_shader(
143         sk_sp<SkShader> input,
144         const skif::LayerSpace<SkRect>& lensBounds,
145         const skif::LayerSpace<SkMatrix>& zoomXform,
146         const skif::LayerSpace<SkSize>& inset) {
147     const SkRuntimeEffect* magEffect =
148             GetKnownRuntimeEffect(SkKnownRuntimeEffects::StableKey::kMagnifier);
149 
150     SkRuntimeShaderBuilder builder(sk_ref_sp(magEffect));
151     builder.child("src") = std::move(input);
152 
153     SkASSERT(inset.width() > 0.f && inset.height() > 0.f);
154     builder.uniform("lensBounds") = SkRect(lensBounds);
155     builder.uniform("zoomXform") = SkV4{/*Tx*/zoomXform.rc(0, 2), /*Ty*/zoomXform.rc(1, 2),
156                                         /*Sx*/zoomXform.rc(0, 0), /*Sy*/zoomXform.rc(1, 1)};
157     builder.uniform("invInset") = SkV2{1.f / inset.width(),
158                                        1.f / inset.height()};
159 
160     return builder.makeShader();
161 }
162 
163 ////////////////////////////////////////////////////////////////////////////////
164 
onFilterImage(const skif::Context & context) const165 skif::FilterResult SkMagnifierImageFilter::onFilterImage(const skif::Context& context) const {
166     // These represent the full lens bounds and the ideal zoom center if everything is visible.
167     skif::LayerSpace<SkRect> lensBounds = context.mapping().paramToLayer(fLensBounds);
168     skif::LayerSpace<SkPoint> zoomCenter = lensBounds.center();
169 
170     // When magnifying near the edge of the screen, it's common for part of the lens bounds to be
171     // offscreen, which also means its input filter cannot provide the full required input.
172     // The magnifier's auto-sizing's goal is to cover the visible portion of the lens bounds.
173     skif::LayerSpace<SkRect> visibleLensBounds = lensBounds;
174     if (!visibleLensBounds.intersect(skif::LayerSpace<SkRect>(context.desiredOutput()))) {
175         return {};
176     }
177 
178     // We pre-emptively fit the zoomed-in src rect to what we expect the child input filter to
179     // produce. This should be correct in all cases except for failure to create an offscreen image,
180     // at which point there's nothing to be done anyway.
181     skif::LayerSpace<SkRect> expectedChildOutput = lensBounds;
182     if (std::optional<skif::LayerSpace<SkIRect>> output =
183             this->getChildOutputLayerBounds(0, context.mapping(), context.source().layerBounds())) {
184         expectedChildOutput = skif::LayerSpace<SkRect>(*output);
185     }
186 
187     // Clamp the zoom center to be within the childOutput image
188     zoomCenter = expectedChildOutput.clamp(zoomCenter);
189 
190     // The zoom we want to apply in layer-space is equal to
191     // mapping.paramToLayer(SkMatrix::Scale(fZoomAmount)).decomposeScale(&layerZoom).
192     // Because this filter only supports scale+translate matrices, the paramToLayer transform of
193     // the parameter-space scale matrix is a no-op. Thus layerZoom == fZoomAmount and we can avoid
194     // all of that math. This assumption is invalid if the matrix complexity is more than S+T.
195     SkASSERT(this->getCTMCapability() == MatrixCapability::kScaleTranslate);
196     // But also clamp the maximum amount of zoom to scale half of a layer pixel to the entire lens.
197     const float maxLensSize = std::max(1.f, std::max(lensBounds.width(), lensBounds.height()));
198     const float invZoom = 1.f /  std::min(fZoomAmount, 2.f * maxLensSize);
199 
200     // The srcRect is the bounding box of the pixels that are linearly scaled up, about zoomCenter.
201     // This is not the visual bounds of this upscaled region, but the bounds of the source pixels
202     // that will fill the main magnified region (which is simply the inset of lensBounds). When
203     // lensBounds has not been cropped by the actual input image, these equations are identical to
204     // the more intuitive L/R = center.x -/+ width/(2*zoom) and T/B = center.y -/+ height/(2*zoom).
205     // However, when lensBounds is cropped this automatically shifts the source rectangle away from
206     // the original zoom center such that the upscaled area is contained within the input image.
207     skif::LayerSpace<SkRect> srcRect{{
208             lensBounds.left()  * invZoom + zoomCenter.x()*(1.f - invZoom),
209             lensBounds.top()   * invZoom + zoomCenter.y()*(1.f - invZoom),
210             lensBounds.right() * invZoom + zoomCenter.x()*(1.f - invZoom),
211             lensBounds.bottom()* invZoom + zoomCenter.y()*(1.f - invZoom)}};
212 
213     // The above adjustment helps to account for offscreen, but when the magnifier is combined with
214     // backdrop offsets, more significant fitting needs to be performed to pin the visible src
215     // rect to what's available.
216     auto zoomXform = skif::LayerSpace<SkMatrix>::RectToRect(lensBounds, srcRect);
217     if (!expectedChildOutput.contains(visibleLensBounds)) {
218         // We need to pick a new srcRect such that srcRect is contained within fitRect and fills
219         // visibleLens, while maintaining the aspect ratio of the original srcRect -> lensBounds.
220         srcRect = zoomXform.mapRect(visibleLensBounds);
221 
222         if (expectedChildOutput.width() >= srcRect.width() &&
223             expectedChildOutput.height() >= srcRect.height()) {
224             float left = srcRect.left() < expectedChildOutput.left() ?
225                     expectedChildOutput.left() :
226                     std::min(srcRect.right(), expectedChildOutput.right()) - srcRect.width();
227             float top = srcRect.top() < expectedChildOutput.top() ?
228                     expectedChildOutput.top() :
229                     std::min(srcRect.bottom(), expectedChildOutput.bottom()) - srcRect.height();
230 
231             // Update transform to reflect fitted src
232             srcRect = skif::LayerSpace<SkRect>(
233                     SkRect::MakeXYWH(left, top, srcRect.width(), srcRect.height()));
234             zoomXform = skif::LayerSpace<SkMatrix>::RectToRect(visibleLensBounds, srcRect);
235         } // Else not enough of the target is available to cover, so don't try adjusting
236     }
237 
238     // When there is no SkSL support, or there's a 0 inset, the magnifier is equivalent to a
239     // rect->rect transform and crop.
240     skif::LayerSpace<SkSize> inset = context.mapping().paramToLayer(
241             skif::ParameterSpace<SkSize>({fInset, fInset}));
242     if (inset.width() <= 0.f || inset.height() <= 0.f)
243     {
244         // When applying the zoom as a direct transform, we only require the visibleSrcRect as
245         // input from the child filter, and transform it by the inverse of zoomXform (to go from
246         // src to lens bounds, since it was constructed to go from lens to src).
247         skif::LayerSpace<SkMatrix> invZoomXform;
248         if (!zoomXform.invert(&invZoomXform)) {
249             return {}; // pathological input
250         }
251         skif::FilterResult childOutput =
252                 this->getChildOutput(0, context.withNewDesiredOutput(srcRect.roundOut()));
253         return childOutput.applyTransform(context, invZoomXform, fSampling)
254                           .applyCrop(context, lensBounds.roundOut());
255     }
256 
257     using ShaderFlags = skif::FilterResult::ShaderFlags;
258     skif::FilterResult::Builder builder{context};
259     builder.add(this->getChildOutput(0, context.withNewDesiredOutput(visibleLensBounds.roundOut())),
260                 {}, ShaderFlags::kNonTrivialSampling, fSampling);
261     return builder.eval([&](SkSpan<sk_sp<SkShader>> inputs) {
262             // If the input resolved to a null shader, the magnified output will be transparent too
263             return inputs[0] ? make_magnifier_shader(inputs[0], lensBounds, zoomXform, inset)
264                              : nullptr;
265         }, lensBounds.roundOut());
266 }
267 
onGetInputLayerBounds(const skif::Mapping & mapping,const skif::LayerSpace<SkIRect> & desiredOutput,std::optional<skif::LayerSpace<SkIRect>> contentBounds) const268 skif::LayerSpace<SkIRect> SkMagnifierImageFilter::onGetInputLayerBounds(
269         const skif::Mapping& mapping,
270         const skif::LayerSpace<SkIRect>& desiredOutput,
271         std::optional<skif::LayerSpace<SkIRect>> contentBounds) const {
272     // The required input is always the lens bounds. The filter distorts the pixels contained within
273     // these bounds to zoom in on a portion of it, depending on the inset and zoom amount. However,
274     // it adjusts the region based on cropping that occurs between what's requested and what's
275     // provided. Theoretically it's possible that we could restrict the required input by the
276     // desired output, but that cropping should not adjust the zoom region or inset. This is non
277     // trivial to separate and is an unlikely use case so for now just require fLensBounds.
278     skif::LayerSpace<SkIRect> requiredInput = mapping.paramToLayer(fLensBounds).roundOut();
279     // Our required input is the desired output for our child image filter.
280     return this->getChildInputLayerBounds(0, mapping, requiredInput, contentBounds);
281 }
282 
onGetOutputLayerBounds(const skif::Mapping & mapping,std::optional<skif::LayerSpace<SkIRect>> contentBounds) const283 std::optional<skif::LayerSpace<SkIRect>> SkMagnifierImageFilter::onGetOutputLayerBounds(
284         const skif::Mapping& mapping,
285         std::optional<skif::LayerSpace<SkIRect>> contentBounds) const {
286     // The output of this filter is fLensBounds intersected with its child's output.
287     auto output = this->getChildOutputLayerBounds(0, mapping, contentBounds);
288     skif::LayerSpace<SkIRect> lensBounds = mapping.paramToLayer(fLensBounds).roundOut();
289     if (!output || lensBounds.intersect(*output)) {
290         return lensBounds;
291     } else {
292         // Nothing to magnify
293         return skif::LayerSpace<SkIRect>::Empty();
294     }
295 }
296 
computeFastBounds(const SkRect & src) const297 SkRect SkMagnifierImageFilter::computeFastBounds(const SkRect& src) const {
298     SkRect bounds = this->getInput(0) ? this->getInput(0)->computeFastBounds(src) : src;
299     if (bounds.intersect(SkRect(fLensBounds))) {
300         return bounds;
301     } else {
302         return SkRect::MakeEmpty();
303     }
304 }
305