summaryrefslogtreecommitdiffstats
path: root/gfx/skia/skia/src/core/SkImageFilterTypes.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'gfx/skia/skia/src/core/SkImageFilterTypes.cpp')
-rw-r--r--gfx/skia/skia/src/core/SkImageFilterTypes.cpp430
1 files changed, 430 insertions, 0 deletions
diff --git a/gfx/skia/skia/src/core/SkImageFilterTypes.cpp b/gfx/skia/skia/src/core/SkImageFilterTypes.cpp
new file mode 100644
index 0000000000..aa8cddb9cc
--- /dev/null
+++ b/gfx/skia/skia/src/core/SkImageFilterTypes.cpp
@@ -0,0 +1,430 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "src/core/SkImageFilterTypes.h"
+
+#include "src/core/SkImageFilter_Base.h"
+#include "src/core/SkMatrixPriv.h"
+
+// This exists to cover up issues where infinite precision would produce integers but float
+// math produces values just larger/smaller than an int and roundOut/In on bounds would produce
+// nearly a full pixel error. One such case is crbug.com/1313579 where the caller has produced
+// near integer CTM and uses integer crop rects that would grab an extra row/column of the
+// input image when using a strict roundOut.
+static constexpr float kRoundEpsilon = 1e-3f;
+
+// Both [I]Vectors and Sk[I]Sizes are transformed as non-positioned values, i.e. go through
+// mapVectors() not mapPoints().
+static SkIVector map_as_vector(int32_t x, int32_t y, const SkMatrix& matrix) {
+ SkVector v = SkVector::Make(SkIntToScalar(x), SkIntToScalar(y));
+ matrix.mapVectors(&v, 1);
+ return SkIVector::Make(SkScalarRoundToInt(v.fX), SkScalarRoundToInt(v.fY));
+}
+
+static SkVector map_as_vector(SkScalar x, SkScalar y, const SkMatrix& matrix) {
+ SkVector v = SkVector::Make(x, y);
+ matrix.mapVectors(&v, 1);
+ return v;
+}
+
+// If m is epsilon within the form [1 0 tx], this returns true and sets out to [tx, ty]
+// [0 1 ty]
+// [0 0 1 ]
+// TODO: Use this in decomposeCTM() (and possibly extend it to support is_nearly_scale_translate)
+// to be a little more forgiving on matrix types during layer configuration.
+static bool is_nearly_integer_translation(const skif::LayerSpace<SkMatrix>& m,
+ skif::LayerSpace<SkIPoint>* out=nullptr) {
+ float tx = SkScalarRoundToScalar(sk_ieee_float_divide(m.rc(0,2), m.rc(2,2)));
+ float ty = SkScalarRoundToScalar(sk_ieee_float_divide(m.rc(1,2), m.rc(2,2)));
+ SkMatrix expected = SkMatrix::MakeAll(1.f, 0.f, tx,
+ 0.f, 1.f, ty,
+ 0.f, 0.f, 1.f);
+ for (int i = 0; i < 9; ++i) {
+ if (!SkScalarNearlyEqual(expected.get(i), m.get(i), kRoundEpsilon)) {
+ return false;
+ }
+ }
+
+ if (out) {
+ *out = skif::LayerSpace<SkIPoint>({(int) tx, (int) ty});
+ }
+ return true;
+}
+
+static SkRect map_rect(const SkMatrix& matrix, const SkRect& rect) {
+ if (rect.isEmpty()) {
+ return SkRect::MakeEmpty();
+ }
+ return matrix.mapRect(rect);
+}
+
+static SkIRect map_rect(const SkMatrix& matrix, const SkIRect& rect) {
+ if (rect.isEmpty()) {
+ return SkIRect::MakeEmpty();
+ }
+ // Unfortunately, there is a range of integer values such that we have 1px precision as an int,
+ // but less precision as a float. This can lead to non-empty SkIRects becoming empty simply
+ // because of float casting. If we're already dealing with a float rect or having a float
+ // output, that's what we're stuck with; but if we are starting form an irect and desiring an
+ // SkIRect output, we go through efforts to preserve the 1px precision for simple transforms.
+ if (matrix.isScaleTranslate()) {
+ double l = (double)matrix.getScaleX()*rect.fLeft + (double)matrix.getTranslateX();
+ double r = (double)matrix.getScaleX()*rect.fRight + (double)matrix.getTranslateX();
+ double t = (double)matrix.getScaleY()*rect.fTop + (double)matrix.getTranslateY();
+ double b = (double)matrix.getScaleY()*rect.fBottom + (double)matrix.getTranslateY();
+
+ return {sk_double_saturate2int(sk_double_floor(std::min(l, r) + kRoundEpsilon)),
+ sk_double_saturate2int(sk_double_floor(std::min(t, b) + kRoundEpsilon)),
+ sk_double_saturate2int(sk_double_ceil(std::max(l, r) - kRoundEpsilon)),
+ sk_double_saturate2int(sk_double_ceil(std::max(t, b) - kRoundEpsilon))};
+ } else {
+ return skif::RoundOut(matrix.mapRect(SkRect::Make(rect)));
+ }
+}
+
+namespace skif {
+
+SkIRect RoundOut(SkRect r) { return r.makeInset(kRoundEpsilon, kRoundEpsilon).roundOut(); }
+
+SkIRect RoundIn(SkRect r) { return r.makeOutset(kRoundEpsilon, kRoundEpsilon).roundIn(); }
+
+bool Mapping::decomposeCTM(const SkMatrix& ctm, const SkImageFilter* filter,
+ const skif::ParameterSpace<SkPoint>& representativePt) {
+ SkMatrix remainder, layer;
+ SkSize decomposed;
+ using MatrixCapability = SkImageFilter_Base::MatrixCapability;
+ MatrixCapability capability =
+ filter ? as_IFB(filter)->getCTMCapability() : MatrixCapability::kComplex;
+ if (capability == MatrixCapability::kTranslate) {
+ // Apply the entire CTM post-filtering
+ remainder = ctm;
+ layer = SkMatrix::I();
+ } else if (ctm.isScaleTranslate() || capability == MatrixCapability::kComplex) {
+ // Either layer space can be anything (kComplex) - or - it can be scale+translate, and the
+ // ctm is. In both cases, the layer space can be equivalent to device space.
+ remainder = SkMatrix::I();
+ layer = ctm;
+ } else if (ctm.decomposeScale(&decomposed, &remainder)) {
+ // This case implies some amount of sampling post-filtering, either due to skew or rotation
+ // in the original matrix. As such, keep the layer matrix as simple as possible.
+ layer = SkMatrix::Scale(decomposed.fWidth, decomposed.fHeight);
+ } else {
+ // Perspective, which has a non-uniform scaling effect on the filter. Pick a single scale
+ // factor that best matches where the filter will be evaluated.
+ SkScalar scale = SkMatrixPriv::DifferentialAreaScale(ctm, SkPoint(representativePt));
+ if (SkScalarIsFinite(scale) && !SkScalarNearlyZero(scale)) {
+ // Now take the sqrt to go from an area scale factor to a scaling per X and Y
+ // FIXME: It would be nice to be able to choose a non-uniform scale.
+ scale = SkScalarSqrt(scale);
+ } else {
+ // The representative point was behind the W = 0 plane, so don't factor out any scale.
+ // NOTE: This makes remainder and layer the same as the MatrixCapability::Translate case
+ scale = 1.f;
+ }
+
+ remainder = ctm;
+ remainder.preScale(SkScalarInvert(scale), SkScalarInvert(scale));
+ layer = SkMatrix::Scale(scale, scale);
+ }
+
+ SkMatrix invRemainder;
+ if (!remainder.invert(&invRemainder)) {
+ // Under floating point arithmetic, it's possible to decompose an invertible matrix into
+ // a scaling matrix and a remainder and have the remainder be non-invertible. Generally
+ // when this happens the scale factors are so large and the matrix so ill-conditioned that
+ // it's unlikely that any drawing would be reasonable, so failing to make a layer is okay.
+ return false;
+ } else {
+ fParamToLayerMatrix = layer;
+ fLayerToDevMatrix = remainder;
+ fDevToLayerMatrix = invRemainder;
+ return true;
+ }
+}
+
+bool Mapping::adjustLayerSpace(const SkMatrix& layer) {
+ SkMatrix invLayer;
+ if (!layer.invert(&invLayer)) {
+ return false;
+ }
+ fParamToLayerMatrix.postConcat(layer);
+ fDevToLayerMatrix.postConcat(layer);
+ fLayerToDevMatrix.preConcat(invLayer);
+ return true;
+}
+
+// Instantiate map specializations for the 6 geometric types used during filtering
+template<>
+SkRect Mapping::map<SkRect>(const SkRect& geom, const SkMatrix& matrix) {
+ return map_rect(matrix, geom);
+}
+
+template<>
+SkIRect Mapping::map<SkIRect>(const SkIRect& geom, const SkMatrix& matrix) {
+ return map_rect(matrix, geom);
+}
+
+template<>
+SkIPoint Mapping::map<SkIPoint>(const SkIPoint& geom, const SkMatrix& matrix) {
+ SkPoint p = SkPoint::Make(SkIntToScalar(geom.fX), SkIntToScalar(geom.fY));
+ matrix.mapPoints(&p, 1);
+ return SkIPoint::Make(SkScalarRoundToInt(p.fX), SkScalarRoundToInt(p.fY));
+}
+
+template<>
+SkPoint Mapping::map<SkPoint>(const SkPoint& geom, const SkMatrix& matrix) {
+ SkPoint p;
+ matrix.mapPoints(&p, &geom, 1);
+ return p;
+}
+
+template<>
+IVector Mapping::map<IVector>(const IVector& geom, const SkMatrix& matrix) {
+ return IVector(map_as_vector(geom.fX, geom.fY, matrix));
+}
+
+template<>
+Vector Mapping::map<Vector>(const Vector& geom, const SkMatrix& matrix) {
+ return Vector(map_as_vector(geom.fX, geom.fY, matrix));
+}
+
+template<>
+SkISize Mapping::map<SkISize>(const SkISize& geom, const SkMatrix& matrix) {
+ SkIVector v = map_as_vector(geom.fWidth, geom.fHeight, matrix);
+ return SkISize::Make(v.fX, v.fY);
+}
+
+template<>
+SkSize Mapping::map<SkSize>(const SkSize& geom, const SkMatrix& matrix) {
+ SkVector v = map_as_vector(geom.fWidth, geom.fHeight, matrix);
+ return SkSize::Make(v.fX, v.fY);
+}
+
+template<>
+SkMatrix Mapping::map<SkMatrix>(const SkMatrix& m, const SkMatrix& matrix) {
+ // If 'matrix' maps from the C1 coord space to the C2 coord space, and 'm' is a transform that
+ // operates on, and outputs to, the C1 coord space, we want to return a new matrix that is
+ // equivalent to 'm' that operates on and outputs to C2. This is the same as mapping the input
+ // from C2 to C1 (matrix^-1), then transforming by 'm', and then mapping from C1 to C2 (matrix).
+ SkMatrix inv;
+ SkAssertResult(matrix.invert(&inv));
+ inv.postConcat(m);
+ inv.postConcat(matrix);
+ return inv;
+}
+
+LayerSpace<SkRect> LayerSpace<SkMatrix>::mapRect(const LayerSpace<SkRect>& r) const {
+ return LayerSpace<SkRect>(map_rect(fData, SkRect(r)));
+}
+
+LayerSpace<SkIRect> LayerSpace<SkMatrix>::mapRect(const LayerSpace<SkIRect>& r) const {
+ return LayerSpace<SkIRect>(map_rect(fData, SkIRect(r)));
+}
+
+sk_sp<SkSpecialImage> FilterResult::imageAndOffset(SkIPoint* offset) const {
+ auto [image, origin] = this->resolve(fLayerBounds);
+ *offset = SkIPoint(origin);
+ return image;
+}
+
+FilterResult FilterResult::applyCrop(const Context& ctx,
+ const LayerSpace<SkIRect>& crop) const {
+ LayerSpace<SkIRect> tightBounds = crop;
+ // TODO(michaelludwig): Intersecting to the target output is only valid when the crop has
+ // decal tiling (the only current option).
+ if (!fImage || !tightBounds.intersect(ctx.desiredOutput())) {
+ // The desired output would be filled with transparent black.
+ return {};
+ }
+
+ if (crop.contains(fLayerBounds)) {
+ // The original crop does not affect the image (although the context's desired output might)
+ // We can tighten fLayerBounds to the desired output without resolving the image, regardless
+ // of the transform type.
+ // TODO(michaelludwig): If the crop would use mirror or repeat, the above isn't true.
+ FilterResult restrictedOutput = *this;
+ SkAssertResult(restrictedOutput.fLayerBounds.intersect(ctx.desiredOutput()));
+ return restrictedOutput;
+ } else {
+ return this->resolve(tightBounds);
+ }
+}
+
+static bool compatible_sampling(const SkSamplingOptions& currentSampling,
+ bool currentXformWontAffectNearest,
+ SkSamplingOptions* nextSampling,
+ bool nextXformWontAffectNearest) {
+ // Both transforms could perform non-trivial sampling, but if they are similar enough we
+ // assume performing one non-trivial sampling operation with the concatenated transform will
+ // not be visually distinguishable from sampling twice.
+ // TODO(michaelludwig): For now ignore mipmap policy, SkSpecialImages are not supposed to be
+ // drawn with mipmapping, and the majority of filter steps produce images that are at the
+ // proper scale and do not define mip levels. The main exception is the ::Image() filter
+ // leaf but that doesn't use this system yet.
+ if (currentSampling.isAniso() && nextSampling->isAniso()) {
+ // Assume we can get away with one sampling at the highest anisotropy level
+ *nextSampling = SkSamplingOptions::Aniso(std::max(currentSampling.maxAniso,
+ nextSampling->maxAniso));
+ return true;
+ } else if (currentSampling.useCubic && (nextSampling->filter == SkFilterMode::kLinear ||
+ (nextSampling->useCubic &&
+ currentSampling.cubic.B == nextSampling->cubic.B &&
+ currentSampling.cubic.C == nextSampling->cubic.C))) {
+ // Assume we can get away with the current bicubic filter, since the next is the same
+ // or a bilerp that can be upgraded.
+ *nextSampling = currentSampling;
+ return true;
+ } else if (nextSampling->useCubic && currentSampling.filter == SkFilterMode::kLinear) {
+ // Mirror of the above, assume we can just get away with next's cubic resampler
+ return true;
+ } else if (currentSampling.filter == SkFilterMode::kLinear &&
+ nextSampling->filter == SkFilterMode::kLinear) {
+ // Assume we can get away with a single bilerp vs. the two
+ return true;
+ } else if (nextSampling->filter == SkFilterMode::kNearest && currentXformWontAffectNearest) {
+ // The next transform and nearest-neighbor filtering isn't impacted by the current transform
+ SkASSERT(currentSampling.filter == SkFilterMode::kLinear);
+ return true;
+ } else if (currentSampling.filter == SkFilterMode::kNearest && nextXformWontAffectNearest) {
+ // The next transform doesn't change the nearest-neighbor filtering of the current transform
+ SkASSERT(nextSampling->filter == SkFilterMode::kLinear);
+ *nextSampling = currentSampling;
+ return true;
+ } else {
+ // The current or next sampling is nearest neighbor, and will produce visible texels
+ // oriented with the current transform; assume this is a desired effect and preserve it.
+ return false;
+ }
+}
+
+FilterResult FilterResult::applyTransform(const Context& ctx,
+ const LayerSpace<SkMatrix> &transform,
+ const SkSamplingOptions &sampling) const {
+ if (!fImage) {
+ // Transformed transparent black remains transparent black.
+ return {};
+ }
+
+ // Extract the sampling options that matter based on the current and next transforms.
+ // We make sure the new sampling is bilerp (default) if the new transform doesn't matter
+ // (and assert that the current is bilerp if its transform didn't matter). Bilerp can be
+ // maximally combined, so simplifies the logic in compatible_sampling().
+ const bool currentXformIsInteger = is_nearly_integer_translation(fTransform);
+ const bool nextXformIsInteger = is_nearly_integer_translation(transform);
+
+ SkASSERT(!currentXformIsInteger || fSamplingOptions == kDefaultSampling);
+ SkSamplingOptions nextSampling = nextXformIsInteger ? kDefaultSampling : sampling;
+
+ FilterResult transformed;
+ if (compatible_sampling(fSamplingOptions, currentXformIsInteger,
+ &nextSampling, nextXformIsInteger)) {
+ // We can concat transforms and 'nextSampling' will be either fSamplingOptions,
+ // sampling, or a merged combination depending on the two transforms in play.
+ transformed = *this;
+ } else {
+ // We'll have to resolve this FilterResult first before 'transform' and 'sampling' can be
+ // correctly evaluated. 'nextSampling' will always be 'sampling'.
+ transformed = this->resolve(fLayerBounds);
+ }
+
+ transformed.concatTransform(transform, nextSampling, ctx.desiredOutput());
+ if (transformed.layerBounds().isEmpty()) {
+ return {};
+ } else {
+ return transformed;
+ }
+}
+
+void FilterResult::concatTransform(const LayerSpace<SkMatrix>& transform,
+ const SkSamplingOptions& newSampling,
+ const LayerSpace<SkIRect>& desiredOutput) {
+ if (!fImage) {
+ // Under normal circumstances, concatTransform() will only be called when we have an image,
+ // but if resolve() fails to make a special surface, we may end up here at which point
+ // doing nothing further is appropriate.
+ return;
+ }
+ fSamplingOptions = newSampling;
+ fTransform.postConcat(transform);
+ // Rebuild the layer bounds and then restrict to the current desired output. The original value
+ // of fLayerBounds includes the image mapped by the original fTransform as well as any
+ // accumulated soft crops from desired outputs of prior stages. To prevent discarding that info,
+ // we map fLayerBounds by the additional transform, instead of re-mapping the image bounds.
+ fLayerBounds = transform.mapRect(fLayerBounds);
+ if (!fLayerBounds.intersect(desiredOutput)) {
+ // The transformed output doesn't touch the desired, so it would just be transparent black.
+ // TODO: This intersection only applies when the tile mode is kDecal.
+ fLayerBounds = LayerSpace<SkIRect>::Empty();
+ }
+}
+
+std::pair<sk_sp<SkSpecialImage>, LayerSpace<SkIPoint>> FilterResult::resolve(
+ LayerSpace<SkIRect> dstBounds) const {
+ // TODO(michaelludwig): Only valid for kDecal, although kClamp would only need 1 extra
+ // pixel of padding so some restriction could happen. We also should skip the intersection if
+ // we need to include transparent black pixels.
+ if (!fImage || !dstBounds.intersect(fLayerBounds)) {
+ return {nullptr, {}};
+ }
+
+ // TODO: This logic to skip a draw will also need to account for the tile mode, but we can
+ // always restrict to the intersection of dstBounds and the image's subset since we are
+ // currently always decal sampling.
+ // TODO(michaelludwig): If we get to the point where all filter results track bounds in
+ // floating point, then we can extend this case to any S+T transform.
+ LayerSpace<SkIPoint> origin;
+ if (is_nearly_integer_translation(fTransform, &origin)) {
+ LayerSpace<SkIRect> imageBounds(SkIRect::MakeXYWH(origin.x(), origin.y(),
+ fImage->width(), fImage->height()));
+ if (!imageBounds.intersect(dstBounds)) {
+ return {nullptr, {}};
+ }
+
+ // Offset the image subset directly to avoid issues negating (origin). With the prior
+ // intersection (bounds - origin) will be >= 0, but (bounds + (-origin)) may not, (e.g.
+ // origin is INT_MIN).
+ SkIRect subset = { imageBounds.left() - origin.x(),
+ imageBounds.top() - origin.y(),
+ imageBounds.right() - origin.x(),
+ imageBounds.bottom() - origin.y() };
+ SkASSERT(subset.fLeft >= 0 && subset.fTop >= 0 &&
+ subset.fRight <= fImage->width() && subset.fBottom <= fImage->height());
+
+ return {fImage->makeSubset(subset), imageBounds.topLeft()};
+ } // else fall through and attempt a draw
+
+ sk_sp<SkSpecialSurface> surface = fImage->makeSurface(fImage->colorType(),
+ fImage->getColorSpace(),
+ SkISize(dstBounds.size()),
+ kPremul_SkAlphaType, {});
+ if (!surface) {
+ return {nullptr, {}};
+ }
+ SkCanvas* canvas = surface->getCanvas();
+ // skbug.com/5075: GPU-backed special surfaces don't reset their contents.
+ canvas->clear(SK_ColorTRANSPARENT);
+ canvas->translate(-dstBounds.left(), -dstBounds.top()); // dst's origin adjustment
+
+ SkPaint paint;
+ paint.setAntiAlias(true);
+ paint.setBlendMode(SkBlendMode::kSrc);
+
+ // TODO: When using a tile mode other than kDecal, we'll need to use SkSpecialImage::asShader()
+ // and use drawRect(fLayerBounds).
+ if (!fLayerBounds.contains(dstBounds)) {
+ // We're resolving to a larger than necessary image, so make sure transparency outside of
+ // fLayerBounds is preserved.
+ // NOTE: This should only happen when the next layer requires processing transparent black.
+ canvas->clipIRect(SkIRect(fLayerBounds));
+ }
+ canvas->concat(SkMatrix(fTransform)); // src's origin is embedded in fTransform
+ fImage->draw(canvas, 0.f, 0.f, fSamplingOptions, &paint);
+
+ return {surface->makeImageSnapshot(), dstBounds.topLeft()};
+}
+
+} // end namespace skif