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;
}