• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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/core/SkAlphaType.h"
9 #include "include/core/SkBitmap.h"
10 #include "include/core/SkColor.h"
11 #include "include/core/SkColorPriv.h"
12 #include "include/core/SkColorType.h"
13 #include "include/core/SkFlattenable.h"
14 #include "include/core/SkImageFilter.h"
15 #include "include/core/SkImageInfo.h"
16 #include "include/core/SkPoint.h"
17 #include "include/core/SkRect.h"
18 #include "include/core/SkRefCnt.h"
19 #include "include/core/SkScalar.h"
20 #include "include/core/SkSize.h"
21 #include "include/core/SkTileMode.h"
22 #include "include/core/SkTypes.h"
23 #include "include/effects/SkImageFilters.h"
24 #include "include/private/base/SkMath.h"
25 #include "include/private/base/SkTPin.h"
26 #include "include/private/base/SkTemplates.h"
27 #include "src/core/SkImageFilter_Base.h"
28 #include "src/core/SkReadBuffer.h"
29 #include "src/core/SkSpecialImage.h"
30 #include "src/core/SkWriteBuffer.h"
31 
32 #include <cstdint>
33 #include <cstring>
34 #include <memory>
35 #include <utility>
36 class SkMatrix;
37 
38 #if defined(SK_GANESH)
39 #include "include/gpu/GrRecordingContext.h"
40 #include "src/gpu/ganesh/GrFragmentProcessor.h"
41 #include "src/gpu/ganesh/GrRecordingContextPriv.h"
42 #include "src/gpu/ganesh/GrSurfaceProxy.h"
43 #include "src/gpu/ganesh/GrSurfaceProxyView.h"
44 #include "src/gpu/ganesh/SkGr.h"
45 #include "src/gpu/ganesh/effects/GrMatrixConvolutionEffect.h"
46 #endif
47 
48 using namespace skia_private;
49 
50 namespace {
51 
52 class SkMatrixConvolutionImageFilter final : public SkImageFilter_Base {
53 public:
SkMatrixConvolutionImageFilter(const SkISize & kernelSize,const SkScalar * kernel,SkScalar gain,SkScalar bias,const SkIPoint & kernelOffset,SkTileMode tileMode,bool convolveAlpha,sk_sp<SkImageFilter> input,const SkRect * cropRect)54     SkMatrixConvolutionImageFilter(const SkISize& kernelSize, const SkScalar* kernel,
55                                    SkScalar gain, SkScalar bias, const SkIPoint& kernelOffset,
56                                    SkTileMode tileMode, bool convolveAlpha,
57                                    sk_sp<SkImageFilter> input, const SkRect* cropRect)
58             : INHERITED(&input, 1, cropRect)
59             , fKernelSize(kernelSize)
60             , fGain(gain)
61             , fBias(bias)
62             , fKernelOffset(kernelOffset)
63             , fTileMode(tileMode)
64             , fConvolveAlpha(convolveAlpha) {
65         size_t size = (size_t) sk_64_mul(fKernelSize.width(), fKernelSize.height());
66         fKernel = new SkScalar[size];
67         memcpy(fKernel, kernel, size * sizeof(SkScalar));
68         SkASSERT(kernelSize.fWidth >= 1 && kernelSize.fHeight >= 1);
69         SkASSERT(kernelOffset.fX >= 0 && kernelOffset.fX < kernelSize.fWidth);
70         SkASSERT(kernelOffset.fY >= 0 && kernelOffset.fY < kernelSize.fHeight);
71     }
72 
~SkMatrixConvolutionImageFilter()73     ~SkMatrixConvolutionImageFilter() override {
74         delete[] fKernel;
75     }
76 
77 protected:
78 
79     void flatten(SkWriteBuffer&) const override;
80 
81     sk_sp<SkSpecialImage> onFilterImage(const Context&, SkIPoint* offset) const override;
82     SkIRect onFilterNodeBounds(const SkIRect&, const SkMatrix& ctm,
83                                MapDirection, const SkIRect* inputRect) const override;
84     bool onAffectsTransparentBlack() const override;
85 
86 private:
87     friend void ::SkRegisterMatrixConvolutionImageFilterFlattenable();
88     SK_FLATTENABLE_HOOKS(SkMatrixConvolutionImageFilter)
89 
90     SkISize     fKernelSize;
91     SkScalar*   fKernel;
92     SkScalar    fGain;
93     SkScalar    fBias;
94     SkIPoint    fKernelOffset;
95     SkTileMode  fTileMode;
96     bool        fConvolveAlpha;
97 
98     template <class PixelFetcher, bool convolveAlpha>
99     void filterPixels(const SkBitmap& src,
100                       SkBitmap* result,
101                       SkIVector& offset,
102                       SkIRect rect,
103                       const SkIRect& bounds) const;
104     template <class PixelFetcher>
105     void filterPixels(const SkBitmap& src,
106                       SkBitmap* result,
107                       SkIVector& offset,
108                       const SkIRect& rect,
109                       const SkIRect& bounds) const;
110     void filterInteriorPixels(const SkBitmap& src,
111                               SkBitmap* result,
112                               SkIVector& offset,
113                               const SkIRect& rect,
114                               const SkIRect& bounds) const;
115     void filterBorderPixels(const SkBitmap& src,
116                             SkBitmap* result,
117                             SkIVector& offset,
118                             const SkIRect& rect,
119                             const SkIRect& bounds) const;
120 
121     using INHERITED = SkImageFilter_Base;
122 };
123 
124 class UncheckedPixelFetcher {
125 public:
fetch(const SkBitmap & src,int x,int y,const SkIRect & bounds)126     static inline SkPMColor fetch(const SkBitmap& src, int x, int y, const SkIRect& bounds) {
127         return *src.getAddr32(x, y);
128     }
129 };
130 
131 class ClampPixelFetcher {
132 public:
fetch(const SkBitmap & src,int x,int y,const SkIRect & bounds)133     static inline SkPMColor fetch(const SkBitmap& src, int x, int y, const SkIRect& bounds) {
134         x = SkTPin(x, bounds.fLeft, bounds.fRight - 1);
135         y = SkTPin(y, bounds.fTop, bounds.fBottom - 1);
136         return *src.getAddr32(x, y);
137     }
138 };
139 
140 class RepeatPixelFetcher {
141 public:
fetch(const SkBitmap & src,int x,int y,const SkIRect & bounds)142     static inline SkPMColor fetch(const SkBitmap& src, int x, int y, const SkIRect& bounds) {
143         x = (x - bounds.left()) % bounds.width() + bounds.left();
144         y = (y - bounds.top()) % bounds.height() + bounds.top();
145         if (x < bounds.left()) {
146             x += bounds.width();
147         }
148         if (y < bounds.top()) {
149             y += bounds.height();
150         }
151         return *src.getAddr32(x, y);
152     }
153 };
154 
155 class ClampToBlackPixelFetcher {
156 public:
fetch(const SkBitmap & src,int x,int y,const SkIRect & bounds)157     static inline SkPMColor fetch(const SkBitmap& src, int x, int y, const SkIRect& bounds) {
158         if (x < bounds.fLeft || x >= bounds.fRight || y < bounds.fTop || y >= bounds.fBottom) {
159             return 0;
160         } else {
161             return *src.getAddr32(x, y);
162         }
163     }
164 };
165 
166 } // end namespace
167 
MatrixConvolution(const SkISize & kernelSize,const SkScalar kernel[],SkScalar gain,SkScalar bias,const SkIPoint & kernelOffset,SkTileMode tileMode,bool convolveAlpha,sk_sp<SkImageFilter> input,const CropRect & cropRect)168 sk_sp<SkImageFilter> SkImageFilters::MatrixConvolution(const SkISize& kernelSize,
169                                                        const SkScalar kernel[],
170                                                        SkScalar gain,
171                                                        SkScalar bias,
172                                                        const SkIPoint& kernelOffset,
173                                                        SkTileMode tileMode,
174                                                        bool convolveAlpha,
175                                                        sk_sp<SkImageFilter> input,
176                                                        const CropRect& cropRect) {
177     // We need to be able to read at most SK_MaxS32 bytes, so divide that
178     // by the size of a scalar to know how many scalars we can read.
179     static constexpr int32_t kMaxKernelSize = SK_MaxS32 / sizeof(SkScalar);
180 
181     if (kernelSize.width() < 1 || kernelSize.height() < 1) {
182         return nullptr;
183     }
184     if (kMaxKernelSize / kernelSize.fWidth < kernelSize.fHeight) {
185         return nullptr;
186     }
187     if (!kernel) {
188         return nullptr;
189     }
190     if ((kernelOffset.fX < 0) || (kernelOffset.fX >= kernelSize.fWidth) ||
191         (kernelOffset.fY < 0) || (kernelOffset.fY >= kernelSize.fHeight)) {
192         return nullptr;
193     }
194     return sk_sp<SkImageFilter>(new SkMatrixConvolutionImageFilter(
195             kernelSize, kernel, gain, bias, kernelOffset, tileMode, convolveAlpha,
196             std::move(input), cropRect));
197 }
198 
SkRegisterMatrixConvolutionImageFilterFlattenable()199 void SkRegisterMatrixConvolutionImageFilterFlattenable() {
200     SK_REGISTER_FLATTENABLE(SkMatrixConvolutionImageFilter);
201     // TODO (michaelludwig) - Remove after grace period for SKPs to stop using old name
202     SkFlattenable::Register("SkMatrixConvolutionImageFilterImpl",
203                             SkMatrixConvolutionImageFilter::CreateProc);
204 }
205 
CreateProc(SkReadBuffer & buffer)206 sk_sp<SkFlattenable> SkMatrixConvolutionImageFilter::CreateProc(SkReadBuffer& buffer) {
207     SK_IMAGEFILTER_UNFLATTEN_COMMON(common, 1);
208 
209     SkISize kernelSize;
210     kernelSize.fWidth = buffer.readInt();
211     kernelSize.fHeight = buffer.readInt();
212     const int count = buffer.getArrayCount();
213 
214     const int64_t kernelArea = sk_64_mul(kernelSize.width(), kernelSize.height());
215     if (!buffer.validate(kernelArea == count)) {
216         return nullptr;
217     }
218     if (!buffer.validateCanReadN<SkScalar>(count)) {
219         return nullptr;
220     }
221     AutoSTArray<16, SkScalar> kernel(count);
222     if (!buffer.readScalarArray(kernel.get(), count)) {
223         return nullptr;
224     }
225     SkScalar gain = buffer.readScalar();
226     SkScalar bias = buffer.readScalar();
227     SkIPoint kernelOffset;
228     kernelOffset.fX = buffer.readInt();
229     kernelOffset.fY = buffer.readInt();
230 
231     SkTileMode tileMode = buffer.read32LE(SkTileMode::kLastTileMode);
232     bool convolveAlpha = buffer.readBool();
233 
234     if (!buffer.isValid()) {
235         return nullptr;
236     }
237     return SkImageFilters::MatrixConvolution(
238                 kernelSize, kernel.get(), gain, bias, kernelOffset, tileMode,
239                 convolveAlpha, common.getInput(0), common.cropRect());
240 }
241 
flatten(SkWriteBuffer & buffer) const242 void SkMatrixConvolutionImageFilter::flatten(SkWriteBuffer& buffer) const {
243     this->INHERITED::flatten(buffer);
244     buffer.writeInt(fKernelSize.fWidth);
245     buffer.writeInt(fKernelSize.fHeight);
246     buffer.writeScalarArray(fKernel, fKernelSize.fWidth * fKernelSize.fHeight);
247     buffer.writeScalar(fGain);
248     buffer.writeScalar(fBias);
249     buffer.writeInt(fKernelOffset.fX);
250     buffer.writeInt(fKernelOffset.fY);
251     buffer.writeInt((int) fTileMode);
252     buffer.writeBool(fConvolveAlpha);
253 }
254 
255 ///////////////////////////////////////////////////////////////////////////////////////////////////
256 
257 template<class PixelFetcher, bool convolveAlpha>
filterPixels(const SkBitmap & src,SkBitmap * result,SkIVector & offset,SkIRect rect,const SkIRect & bounds) const258 void SkMatrixConvolutionImageFilter::filterPixels(const SkBitmap& src,
259                                                   SkBitmap* result,
260                                                   SkIVector& offset,
261                                                   SkIRect rect,
262                                                   const SkIRect& bounds) const {
263     if (!rect.intersect(bounds)) {
264         return;
265     }
266     for (int y = rect.fTop; y < rect.fBottom; ++y) {
267         SkPMColor* dptr = result->getAddr32(rect.fLeft - offset.fX, y - offset.fY);
268         for (int x = rect.fLeft; x < rect.fRight; ++x) {
269             SkScalar sumA = 0, sumR = 0, sumG = 0, sumB = 0;
270             for (int cy = 0; cy < fKernelSize.fHeight; cy++) {
271                 for (int cx = 0; cx < fKernelSize.fWidth; cx++) {
272                     SkPMColor s = PixelFetcher::fetch(src,
273                                                       x + cx - fKernelOffset.fX,
274                                                       y + cy - fKernelOffset.fY,
275                                                       bounds);
276                     SkScalar k = fKernel[cy * fKernelSize.fWidth + cx];
277                     if (convolveAlpha) {
278                         sumA += SkGetPackedA32(s) * k;
279                     }
280                     sumR += SkGetPackedR32(s) * k;
281                     sumG += SkGetPackedG32(s) * k;
282                     sumB += SkGetPackedB32(s) * k;
283                 }
284             }
285             int a = convolveAlpha
286                   ? SkTPin(SkScalarFloorToInt(sumA * fGain + fBias), 0, 255)
287                   : 255;
288             int r = SkTPin(SkScalarFloorToInt(sumR * fGain + fBias), 0, a);
289             int g = SkTPin(SkScalarFloorToInt(sumG * fGain + fBias), 0, a);
290             int b = SkTPin(SkScalarFloorToInt(sumB * fGain + fBias), 0, a);
291             if (!convolveAlpha) {
292                 a = SkGetPackedA32(PixelFetcher::fetch(src, x, y, bounds));
293                 *dptr++ = SkPreMultiplyARGB(a, r, g, b);
294             } else {
295                 *dptr++ = SkPackARGB32(a, r, g, b);
296             }
297         }
298     }
299 }
300 
301 template<class PixelFetcher>
filterPixels(const SkBitmap & src,SkBitmap * result,SkIVector & offset,const SkIRect & rect,const SkIRect & bounds) const302 void SkMatrixConvolutionImageFilter::filterPixels(const SkBitmap& src,
303                                                   SkBitmap* result,
304                                                   SkIVector& offset,
305                                                   const SkIRect& rect,
306                                                   const SkIRect& bounds) const {
307     if (fConvolveAlpha) {
308         filterPixels<PixelFetcher, true>(src, result, offset, rect, bounds);
309     } else {
310         filterPixels<PixelFetcher, false>(src, result, offset, rect, bounds);
311     }
312 }
313 
filterInteriorPixels(const SkBitmap & src,SkBitmap * result,SkIVector & offset,const SkIRect & rect,const SkIRect & bounds) const314 void SkMatrixConvolutionImageFilter::filterInteriorPixels(const SkBitmap& src,
315                                                           SkBitmap* result,
316                                                           SkIVector& offset,
317                                                           const SkIRect& rect,
318                                                           const SkIRect& bounds) const {
319     switch (fTileMode) {
320         case SkTileMode::kMirror:
321             // TODO (michaelludwig) - Implement mirror tiling, treat as repeat for now.
322         case SkTileMode::kRepeat:
323             // In repeat mode, we still need to wrap the samples around the src
324             filterPixels<RepeatPixelFetcher>(src, result, offset, rect, bounds);
325             break;
326         case SkTileMode::kClamp:
327             // Fall through
328         case SkTileMode::kDecal:
329             filterPixels<UncheckedPixelFetcher>(src, result, offset, rect, bounds);
330             break;
331     }
332 }
333 
filterBorderPixels(const SkBitmap & src,SkBitmap * result,SkIVector & offset,const SkIRect & rect,const SkIRect & srcBounds) const334 void SkMatrixConvolutionImageFilter::filterBorderPixels(const SkBitmap& src,
335                                                         SkBitmap* result,
336                                                         SkIVector& offset,
337                                                         const SkIRect& rect,
338                                                         const SkIRect& srcBounds) const {
339     switch (fTileMode) {
340         case SkTileMode::kClamp:
341             filterPixels<ClampPixelFetcher>(src, result, offset, rect, srcBounds);
342             break;
343         case SkTileMode::kMirror:
344             // TODO (michaelludwig) - Implement mirror tiling, treat as repeat for now.
345         case SkTileMode::kRepeat:
346             filterPixels<RepeatPixelFetcher>(src, result, offset, rect, srcBounds);
347             break;
348         case SkTileMode::kDecal:
349             filterPixels<ClampToBlackPixelFetcher>(src, result, offset, rect, srcBounds);
350             break;
351     }
352 }
353 
onFilterImage(const Context & ctx,SkIPoint * offset) const354 sk_sp<SkSpecialImage> SkMatrixConvolutionImageFilter::onFilterImage(const Context& ctx,
355                                                                     SkIPoint* offset) const {
356     SkIPoint inputOffset = SkIPoint::Make(0, 0);
357     sk_sp<SkSpecialImage> input(this->filterInput(0, ctx, &inputOffset));
358     if (!input) {
359         return nullptr;
360     }
361 
362     SkIRect dstBounds;
363     input = this->applyCropRectAndPad(this->mapContext(ctx), input.get(), &inputOffset, &dstBounds);
364     if (!input) {
365         return nullptr;
366     }
367 
368     const SkIRect originalSrcBounds = SkIRect::MakeXYWH(inputOffset.fX, inputOffset.fY,
369                                                         input->width(), input->height());
370 
371     SkIRect srcBounds = this->onFilterNodeBounds(dstBounds, ctx.ctm(), kReverse_MapDirection,
372                                                  &originalSrcBounds);
373 
374     if (SkTileMode::kRepeat == fTileMode || SkTileMode::kMirror == fTileMode) {
375         srcBounds = DetermineRepeatedSrcBound(srcBounds, fKernelOffset,
376                                               fKernelSize, originalSrcBounds);
377     } else {
378         if (!srcBounds.intersect(dstBounds)) {
379             return nullptr;
380         }
381     }
382 
383 #if defined(SK_GANESH)
384     if (ctx.gpuBacked()) {
385         auto context = ctx.getContext();
386 
387         // Ensure the input is in the destination color space. Typically applyCropRect will have
388         // called pad_image to account for our dilation of bounds, so the result will already be
389         // moved to the destination color space. If a filter DAG avoids that, then we use this
390         // fall-back, which saves us from having to do the xform during the filter itself.
391         input = ImageToColorSpace(input.get(), ctx.colorType(), ctx.colorSpace(),
392                                   ctx.surfaceProps());
393 
394         GrSurfaceProxyView inputView = input->view(context);
395         SkASSERT(inputView.asTextureProxy());
396 
397         const auto isProtected = inputView.proxy()->isProtected();
398         const auto origin = inputView.origin();
399 
400         offset->fX = dstBounds.left();
401         offset->fY = dstBounds.top();
402         dstBounds.offset(-inputOffset);
403         srcBounds.offset(-inputOffset);
404         // Map srcBounds from input's logical image domain to that of the proxy
405         srcBounds.offset(input->subset().x(), input->subset().y());
406 
407         auto fp = GrMatrixConvolutionEffect::Make(context,
408                                                   std::move(inputView),
409                                                   srcBounds,
410                                                   fKernelSize,
411                                                   fKernel,
412                                                   fGain,
413                                                   fBias,
414                                                   fKernelOffset,
415                                                   SkTileModeToWrapMode(fTileMode),
416                                                   fConvolveAlpha,
417                                                   *ctx.getContext()->priv().caps());
418         if (!fp) {
419             return nullptr;
420         }
421 
422         // FIXME (michaelludwig) - Clean this up as part of the imagefilter refactor, some filters
423         // instead require a coord transform on the FP. At very least, be consistent, at best make
424         // it so that filter impls don't need to worry about the subset origin.
425 
426         // Must also map the dstBounds since it is used as the src rect in DrawWithFP when
427         // evaluating the FP, and the dst rect just uses the size of dstBounds.
428         dstBounds.offset(input->subset().x(), input->subset().y());
429         return DrawWithFP(context, std::move(fp), dstBounds, ctx.colorType(), ctx.colorSpace(),
430                           ctx.surfaceProps(), origin, isProtected);
431     }
432 #endif
433 
434     SkBitmap inputBM;
435     if (!input->getROPixels(&inputBM)) {
436         return nullptr;
437     }
438 
439     if (inputBM.colorType() != kN32_SkColorType) {
440         return nullptr;
441     }
442 
443     if (!fConvolveAlpha && !inputBM.isOpaque()) {
444         // This leaves the bitmap tagged as premul, which seems weird to me,
445         // but is consistent with old behavior.
446         inputBM.readPixels(inputBM.info().makeAlphaType(kUnpremul_SkAlphaType),
447                            inputBM.getPixels(), inputBM.rowBytes(), 0,0);
448     }
449 
450     if (!inputBM.getPixels()) {
451         return nullptr;
452     }
453 
454     const SkImageInfo info = SkImageInfo::MakeN32(dstBounds.width(), dstBounds.height(),
455                                                   inputBM.alphaType());
456 
457     SkBitmap dst;
458     if (!dst.tryAllocPixels(info)) {
459         return nullptr;
460     }
461 
462     offset->fX = dstBounds.fLeft;
463     offset->fY = dstBounds.fTop;
464     dstBounds.offset(-inputOffset);
465     srcBounds.offset(-inputOffset);
466 
467     SkIRect interior;
468     if (SkTileMode::kRepeat == fTileMode || SkTileMode::kMirror == fTileMode) {
469         // In repeat mode, the filterPixels calls will wrap around
470         // so we just need to render 'dstBounds'
471         interior = dstBounds;
472     } else {
473         interior = SkIRect::MakeXYWH(dstBounds.left() + fKernelOffset.fX,
474                                      dstBounds.top() + fKernelOffset.fY,
475                                      dstBounds.width() - fKernelSize.fWidth + 1,
476                                      dstBounds.height() - fKernelSize.fHeight + 1);
477     }
478 
479     SkIRect top = SkIRect::MakeLTRB(dstBounds.left(), dstBounds.top(),
480                                     dstBounds.right(), interior.top());
481     SkIRect bottom = SkIRect::MakeLTRB(dstBounds.left(), interior.bottom(),
482                                        dstBounds.right(), dstBounds.bottom());
483     SkIRect left = SkIRect::MakeLTRB(dstBounds.left(), interior.top(),
484                                      interior.left(), interior.bottom());
485     SkIRect right = SkIRect::MakeLTRB(interior.right(), interior.top(),
486                                       dstBounds.right(), interior.bottom());
487 
488     SkIVector dstContentOffset = { offset->fX - inputOffset.fX, offset->fY - inputOffset.fY };
489 
490     this->filterBorderPixels(inputBM, &dst, dstContentOffset, top, srcBounds);
491     this->filterBorderPixels(inputBM, &dst, dstContentOffset, left, srcBounds);
492     this->filterInteriorPixels(inputBM, &dst, dstContentOffset, interior, srcBounds);
493     this->filterBorderPixels(inputBM, &dst, dstContentOffset, right, srcBounds);
494     this->filterBorderPixels(inputBM, &dst, dstContentOffset, bottom, srcBounds);
495 
496     return SkSpecialImage::MakeFromRaster(SkIRect::MakeWH(dstBounds.width(), dstBounds.height()),
497                                           dst, ctx.surfaceProps());
498 }
499 
onFilterNodeBounds(const SkIRect & src,const SkMatrix & ctm,MapDirection dir,const SkIRect * inputRect) const500 SkIRect SkMatrixConvolutionImageFilter::onFilterNodeBounds(
501         const SkIRect& src, const SkMatrix& ctm, MapDirection dir, const SkIRect* inputRect) const {
502     if (kReverse_MapDirection == dir && inputRect &&
503         (SkTileMode::kRepeat == fTileMode || SkTileMode::kMirror == fTileMode)) {
504         SkASSERT(inputRect);
505         return DetermineRepeatedSrcBound(src, fKernelOffset, fKernelSize, *inputRect);
506     }
507 
508     SkIRect dst = src;
509     int w = fKernelSize.width() - 1, h = fKernelSize.height() - 1;
510 
511     if (kReverse_MapDirection == dir) {
512         dst.adjust(-fKernelOffset.fX, -fKernelOffset.fY,
513                    w - fKernelOffset.fX, h - fKernelOffset.fY);
514     } else {
515         dst.adjust(fKernelOffset.fX - w, fKernelOffset.fY - h, fKernelOffset.fX, fKernelOffset.fY);
516     }
517     return dst;
518 }
519 
onAffectsTransparentBlack() const520 bool SkMatrixConvolutionImageFilter::onAffectsTransparentBlack() const {
521     // It seems that the only rational way for repeat sample mode to work is if the caller
522     // explicitly restricts the input in which case the input range is explicitly known and
523     // specified.
524     // TODO: is seems that this should be true for clamp mode too.
525 
526     // For the other modes, because the kernel is applied in device-space, we have no idea what
527     // pixels it will affect in object-space.
528     return SkTileMode::kRepeat != fTileMode && SkTileMode::kMirror != fTileMode;
529 }
530