FilterResult FilterResult::rescale()

in src/core/SkImageFilterTypes.cpp [1637:1906]


FilterResult FilterResult::rescale(const Context& ctx,
                                   const LayerSpace<SkSize>& scale,
                                   bool enforceDecal) const {
    LayerSpace<SkIRect> visibleLayerBounds = fLayerBounds;
    if (!fImage || !visibleLayerBounds.intersect(ctx.desiredOutput()) ||
        scale.width() <= 0.f || scale.height() <= 0.f) {
        return {};
    }

    // NOTE: For the first pass, PixelSpace and LayerSpace are equivalent
    PixelSpace<SkIPoint> origin;
    const bool pixelAligned = is_nearly_integer_translation(fTransform, &origin);
    SkEnumBitMask<BoundsAnalysis> analysis = this->analyzeBounds(ctx.desiredOutput(),
                                                                 BoundsScope::kRescale);

    // If there's no actual scaling, and no other effects that have to be resolved for blur(),
    // then just extract the necessary subset. Otherwise fall through and apply the effects with
    // scale factor (possibly identity).
    const bool canDeferTiling =
            pixelAligned &&
            !(analysis & BoundsAnalysis::kRequiresLayerCrop) &&
            !(enforceDecal && (analysis & BoundsAnalysis::kHasLayerFillingEffect));

    // To match legacy color space conversion logic, treat a null src as sRGB and a null dst as
    // as the src CS.
    const SkColorSpace* srcCS = fImage->getColorSpace() ? fImage->getColorSpace()
                                                        : sk_srgb_singleton();
    const SkColorSpace* dstCS = ctx.colorSpace() ? ctx.colorSpace() : srcCS;
    const bool hasEffectsToApply =
            !canDeferTiling ||
            SkToBool(fColorFilter) ||
            fImage->colorType() != ctx.backend()->colorType() ||
            !SkColorSpace::Equals(srcCS, dstCS);

    int xSteps = downscale_step_count(scale.width());
    int ySteps = downscale_step_count(scale.height());
    if (xSteps == 0 && ySteps == 0 && !hasEffectsToApply) {
        if (analysis & BoundsAnalysis::kHasLayerFillingEffect) {
            // At this point, the only effects that could be visible is a non-decal mode, so just
            // return the image with adjusted layer bounds to match desired output.
            FilterResult noop = *this;
            noop.fLayerBounds = visibleLayerBounds;
            return noop;
        } else {
            // The visible layer bounds represents a tighter bounds than the image itself
            return this->subset(origin, visibleLayerBounds);
        }
    }

    PixelSpace<SkIRect> srcRect;
    SkTileMode tileMode;
    bool cfBorder = false;
    bool deferPeriodicTiling = false;
    if (canDeferTiling && (analysis & BoundsAnalysis::kHasLayerFillingEffect)) {
        // When we can defer tiling, and said tiling is visible, rescaling the original image
        // uses smaller textures.
        srcRect = LayerSpace<SkIRect>(SkIRect::MakeXYWH(origin.x(), origin.y(),
                                                        fImage->width(), fImage->height()));
        if (fTileMode == SkTileMode::kDecal &&
            (analysis & BoundsAnalysis::kHasLayerFillingEffect)) {
            // Like in applyColorFilter() evaluate the transparent CF'ed border and clamp to it.
            tileMode = SkTileMode::kClamp;
            cfBorder = true;
        } else {
            tileMode = fTileMode;
            deferPeriodicTiling = tileMode == SkTileMode::kRepeat ||
                                  tileMode == SkTileMode::kMirror;
        }
    } else {
        // Otherwise we either have to rescale the layer-bounds-sized image (!canDeferTiling)
        // or the tiling isn't visible so the layer bounds represents a smaller effective
        // image than the original image data.
        srcRect = visibleLayerBounds;
        tileMode = SkTileMode::kDecal;
    }

    srcRect = srcRect.relevantSubset(ctx.desiredOutput(), tileMode);
    // To avoid incurring error from rounding up the dimensions at every step, the logical size of
    // the image is tracked in floats through the whole process; rounding to integers is only done
    // to produce a conservative pixel buffer and clamp-tiling is used so that partially covered
    // pixels are filled with the un-weighted color.
    PixelSpace<SkRect> stepBoundsF{srcRect};
    if (stepBoundsF.isEmpty()) {
        return {};
    }
    // stepPixelBounds holds integer pixel values (as floats) and includes any padded outsetting
    // that was rendered by the previous step, while stepBoundsF does not have any padding.
    PixelSpace<SkRect> stepPixelBounds{srcRect};

    // If we made it here, at least one iteration is required, even if xSteps and ySteps are 0.
    FilterResult image = *this;
    if (!pixelAligned && (xSteps > 0 || ySteps > 0)) {
        // If the source image has a deferred transform with a downscaling factor, we don't want to
        // necessarily compose the first rescale step's transform with it because we will then be
        // missing pixels in the bilinear filtering and create sampling artifacts during animations.
        // NOTE: Force nextSteps counts to the max integer value when the accumulated scale factor
        // is not finite, to force the input image to be resolved.
        LayerSpace<SkSize> netScale = image.fTransform.mapSize(scale);
        int nextXSteps = std::isfinite(netScale.width()) ? downscale_step_count(netScale.width())
                                                         : std::numeric_limits<int>::max();
        int nextYSteps = std::isfinite(netScale.height()) ? downscale_step_count(netScale.height())
                                                          : std::numeric_limits<int>::max();
        // We only need to resolve the deferred transform if the rescaling along an axis is not
        // near identity (steps > 0). If it's near identity, there's no real difference in sampling
        // between resolving here and deferring it to the first rescale iteration.
        if ((xSteps > 0 && nextXSteps > xSteps) || (ySteps > 0 && nextYSteps > ySteps)) {
            // Resolve the deferred transform. We don't just fold the deferred scale factor into
            // the rescaling steps because, for better or worse, the deferred transform does not
            // otherwise participate in progressive scaling so we should be consistent.
            image = image.resolve(ctx, srcRect);
            if (!image) {
                // Early out if the resolve failed
                return {};
            }
            if (!cfBorder) {
                // This sets the resolved image to match either kDecal or the deferred tile mode.
                image.fTileMode = tileMode;
            } // else leave it as kDecal when cfBorder is true
        }
    }

    // For now, if we are deferring periodic tiling, we need to ensure that the low-res image bounds
    // are pixel aligned. This is because the tiling is applied at the pixel level in SkImageShader,
    // and we need the period of the low-res image to align with the original high-resolution period
    // If/when SkImageShader supports shader-tiling over fractional bounds, this can relax.
    float finalScaleX = xSteps > 0 ? scale.width() : 1.f;
    float finalScaleY = ySteps > 0 ? scale.height() : 1.f;
    if (deferPeriodicTiling) {
        PixelSpace<SkRect> dstBoundsF = scale_about_center(stepBoundsF, finalScaleX, finalScaleY);
        // Use a pixel bounds that's smaller than what was requested to ensure any post-blur amount
        // is lower than the max supported. In the event that roundIn() would collapse to an empty
        // rect, use a 1x1 bounds that contains the center point.
        PixelSpace<SkIRect> innerDstPixels = dstBoundsF.roundIn();
        int dstCenterX = sk_float_floor2int(0.5f * dstBoundsF.right()  + 0.5f * dstBoundsF.left());
        int dstCenterY = sk_float_floor2int(0.5f * dstBoundsF.bottom() + 0.5f * dstBoundsF.top());
        dstBoundsF = PixelSpace<SkRect>({(float) std::min(dstCenterX,   innerDstPixels.left()),
                                         (float) std::min(dstCenterY,   innerDstPixels.top()),
                                         (float) std::max(dstCenterX+1, innerDstPixels.right()),
                                         (float) std::max(dstCenterY+1, innerDstPixels.bottom())});

        finalScaleX = dstBoundsF.width() / srcRect.width();
        finalScaleY = dstBoundsF.height() / srcRect.height();

        // Recompute how many steps are needed, as we may need to do one more step from the round-in
        xSteps = downscale_step_count(finalScaleX);
        ySteps = downscale_step_count(finalScaleY);

        // The periodic tiling effect will be manually rendered into the lower resolution image so
        // that clamp tiling can be used at each decimation.
        image.fTileMode = SkTileMode::kClamp;
    }

    do {
        float sx = 1.f;
        if (xSteps > 0) {
            sx = xSteps > 1 ? 0.5f : srcRect.width()*finalScaleX / stepBoundsF.width();
            xSteps--;
        }

        float sy = 1.f;
        if (ySteps > 0) {
            sy = ySteps > 1 ? 0.5f : srcRect.height()*finalScaleY / stepBoundsF.height();
            ySteps--;
        }

        // Downscale relative to the center of the image, which better distributes any sort of
        // sampling errors across the image (vs. emphasizing the bottom right edges).
        PixelSpace<SkRect> dstBoundsF = scale_about_center(stepBoundsF, sx, sy);

        // NOTE: Rounding out is overly conservative when dstBoundsF has an odd integer width/height
        // but with coordinates at 1/2. In this case, we could create a pixel grid that has a
        // fractional translation in the final FilterResult but that will best be done when
        // FilterResult tracks floating bounds.
        PixelSpace<SkIRect> dstPixelBounds = dstBoundsF.roundOut();

        PixelBoundary boundary = PixelBoundary::kUnknown;
        PixelSpace<SkIRect> sampleBounds = dstPixelBounds;
        if (tileMode == SkTileMode::kDecal) {
            boundary = PixelBoundary::kTransparent;
        } else {
            // This is roughly equivalent to using PixelBoundary::kInitialized, but keeps some of
            // the later logic simpler.
            dstPixelBounds.outset(LayerSpace<SkISize>({1,1}));
        }

        AutoSurface surface{ctx, dstPixelBounds, boundary, /*renderInParameterSpace=*/false};
        if (surface) {
            const auto scaleXform = PixelSpace<SkMatrix>::RectToRect(stepBoundsF, dstBoundsF);

            // Redo analysis with the actual scale transform and padded low res bounds.
            // With the padding added to dstPixelBounds, intermediate steps should not require
            // shader tiling. Unfortunately, when the last step requires a scale factor other than
            // 1/2, shader based clamping may still be necessary with just a single pixel of padding
            // TODO: Given that the final step may often require shader-based tiling, it may make
            // sense to tile into a large enough texture that the subsequent blurs will not require
            // any shader-based tiling.
            analysis = image.analyzeBounds(SkMatrix(scaleXform),
                                           SkIRect(sampleBounds),
                                           BoundsScope::kRescale);

            // Primary fill that will cover all of 'sampleBounds'
            SkPaint paint;
            paint.setShader(image.getAnalyzedShaderView(ctx, image.sampling(), analysis));
#if !defined(SK_USE_SRCOVER_FOR_FILTERS)
            paint.setBlendMode(SkBlendMode::kSrc);
#endif

            PixelSpace<SkRect> srcSampled;
            SkAssertResult(scaleXform.inverseMapRect(PixelSpace<SkRect>(sampleBounds),
                                                     &srcSampled));

            surface->save();
                surface->concat(SkMatrix(scaleXform));
                surface->drawRect(SkRect(srcSampled), paint);
            surface->restore();

            if (cfBorder) {
                // Fill in the border with the transparency-affecting color filter, which is
                // what the image shader's tile mode would have produced anyways but this avoids
                // triggering shader-based tiling.
                SkASSERT(fColorFilter && as_CFB(fColorFilter)->affectsTransparentBlack());
                SkASSERT(tileMode == SkTileMode::kClamp);

                draw_color_filtered_border(surface.canvas(), dstPixelBounds, fColorFilter);
                // Clamping logic will preserve its values on subsequent rescale steps.
                cfBorder = false;
            } else if (tileMode != SkTileMode::kDecal) {
                // Draw the edges of the shader into the padded border, respecting the tile mode
                draw_tiled_border(surface.canvas(), tileMode, paint, scaleXform,
                                  stepPixelBounds, PixelSpace<SkRect>(dstPixelBounds));
            }
        } else {
            // Rescaling can't complete, no sense in downscaling non-existent data
            return {};
        }

        image = surface.snap();
        // If we are deferring periodic tiling, use kClamp on subsequent steps to preserve the
        // border pixels. The original tile mode will be restored at the end.
        image.fTileMode = deferPeriodicTiling ? SkTileMode::kClamp : tileMode;

        stepBoundsF = dstBoundsF;
        stepPixelBounds = PixelSpace<SkRect>(dstPixelBounds);
    } while(xSteps > 0 || ySteps > 0);


    // Rebuild the downscaled image, including a transform back to the original layer-space
    // resolution, restoring the layer bounds it should fill, and setting tile mode.
    if (deferPeriodicTiling) {
        // Inset the image to undo the manually added border of pixels, which will allow the result
        // to have the kInitialized boundary state.
        image = image.insetByPixel();
    } else {
        SkASSERT(tileMode == SkTileMode::kDecal || tileMode == SkTileMode::kClamp);
        // Leave the image as-is. If it's decal tiled, this preserves the known transparent
        // boundary. If it's clamp tiled, we want to clamp to the carefully maintained boundary
        // pixels that better preserved the original boundary. Taking a subset like we did for
        // periodic tiles would effectively clamp to the interior of the image.
    }
    image.fTileMode = tileMode;
    image.fTransform.postConcat(
            LayerSpace<SkMatrix>::RectToRect(stepBoundsF, LayerSpace<SkRect>{srcRect}));
    image.fLayerBounds = visibleLayerBounds;

    SkASSERT(!enforceDecal || image.fTileMode == SkTileMode::kDecal);
    SkASSERT(image.fTileMode != SkTileMode::kDecal ||
             image.fBoundary == PixelBoundary::kTransparent);
    SkASSERT(!deferPeriodicTiling || image.fBoundary == PixelBoundary::kInitialized);
    return image;
}