diff options
Diffstat (limited to 'src/core')
-rw-r--r-- | src/core/SkBlurMask.cpp | 537 | ||||
-rw-r--r-- | src/core/SkBlurMask.h | 88 |
2 files changed, 625 insertions, 0 deletions
diff --git a/src/core/SkBlurMask.cpp b/src/core/SkBlurMask.cpp new file mode 100644 index 0000000000..086977528b --- /dev/null +++ b/src/core/SkBlurMask.cpp @@ -0,0 +1,537 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + + +#include "SkBlurMask.h" +#include "SkColorPriv.h" +#include "SkMaskBlurFilter.h" +#include "SkMath.h" +#include "SkTemplates.h" +#include "SkEndian.h" + + +// This constant approximates the scaling done in the software path's +// "high quality" mode, in SkBlurMask::Blur() (1 / sqrt(3)). +// IMHO, it actually should be 1: we blur "less" than we should do +// according to the CSS and canvas specs, simply because Safari does the same. +// Firefox used to do the same too, until 4.0 where they fixed it. So at some +// point we should probably get rid of these scaling constants and rebaseline +// all the blur tests. +static const SkScalar kBLUR_SIGMA_SCALE = 0.57735f; + +SkScalar SkBlurMask::ConvertRadiusToSigma(SkScalar radius) { + return radius > 0 ? kBLUR_SIGMA_SCALE * radius + 0.5f : 0.0f; +} + +SkScalar SkBlurMask::ConvertSigmaToRadius(SkScalar sigma) { + return sigma > 0.5f ? (sigma - 0.5f) / kBLUR_SIGMA_SCALE : 0.0f; +} + + +static void merge_src_with_blur(uint8_t dst[], int dstRB, + const uint8_t src[], int srcRB, + const uint8_t blur[], int blurRB, + int sw, int sh) { + dstRB -= sw; + srcRB -= sw; + blurRB -= sw; + while (--sh >= 0) { + for (int x = sw - 1; x >= 0; --x) { + *dst = SkToU8(SkAlphaMul(*blur, SkAlpha255To256(*src))); + dst += 1; + src += 1; + blur += 1; + } + dst += dstRB; + src += srcRB; + blur += blurRB; + } +} + +static void clamp_with_orig(uint8_t dst[], int dstRowBytes, + const uint8_t src[], int srcRowBytes, + int sw, int sh, + SkBlurStyle style) { + int x; + while (--sh >= 0) { + switch (style) { + case kSolid_SkBlurStyle: + for (x = sw - 1; x >= 0; --x) { + int s = *src; + int d = *dst; + *dst = SkToU8(s + d - SkMulDiv255Round(s, d)); + dst += 1; + src += 1; + } + break; + case kOuter_SkBlurStyle: + for (x = sw - 1; x >= 0; --x) { + if (*src) { + *dst = SkToU8(SkAlphaMul(*dst, SkAlpha255To256(255 - *src))); + } + dst += 1; + src += 1; + } + break; + default: + SkDEBUGFAIL("Unexpected blur style here"); + break; + } + dst += dstRowBytes - sw; + src += srcRowBytes - sw; + } +} + +/////////////////////////////////////////////////////////////////////////////// + +// we use a local function to wrap the class static method to work around +// a bug in gcc98 +void SkMask_FreeImage(uint8_t* image); +void SkMask_FreeImage(uint8_t* image) { + SkMask::FreeImage(image); +} + +bool SkBlurMask::BoxBlur(SkMask* dst, const SkMask& src, + SkScalar sigma, SkBlurStyle style, SkBlurQuality quality, + SkIPoint* margin, bool force_quality) { + + if (src.fFormat != SkMask::kA8_Format) { + return false; + } + + SkIPoint border; + + SkMaskBlurFilter blurFilter{sigma, sigma}; + if (blurFilter.hasNoBlur()) { + return false; + } + border = blurFilter.blur(src, dst); + // If src.fImage is null, then this call is only to calculate the border. + if (src.fImage != nullptr && dst->fImage == nullptr) { + return false; + } + + if (src.fImage != nullptr) { + // if need be, alloc the "real" dst (same size as src) and copy/merge + // the blur into it (applying the src) + if (style == kInner_SkBlurStyle) { + // now we allocate the "real" dst, mirror the size of src + size_t srcSize = src.computeImageSize(); + if (0 == srcSize) { + return false; // too big to allocate, abort + } + auto blur = dst->fImage; + dst->fImage = SkMask::AllocImage(srcSize); + auto blurStart = &blur[border.x() + border.y() * dst->fRowBytes]; + merge_src_with_blur(dst->fImage, src.fRowBytes, + src.fImage, src.fRowBytes, + blurStart, + dst->fRowBytes, + src.fBounds.width(), src.fBounds.height()); + SkMask::FreeImage(blur); + } else if (style != kNormal_SkBlurStyle) { + auto dstStart = &dst->fImage[border.x() + border.y() * dst->fRowBytes]; + clamp_with_orig(dstStart, + dst->fRowBytes, src.fImage, src.fRowBytes, + src.fBounds.width(), src.fBounds.height(), style); + } + } + + if (style == kInner_SkBlurStyle) { + dst->fBounds = src.fBounds; // restore trimmed bounds + dst->fRowBytes = src.fRowBytes; + } + + if (margin != nullptr) { + *margin = border; + } + + return true; +} + +/* Convolving a box with itself three times results in a piecewise + quadratic function: + + 0 x <= -1.5 + 9/8 + 3/2 x + 1/2 x^2 -1.5 < x <= -.5 + 3/4 - x^2 -.5 < x <= .5 + 9/8 - 3/2 x + 1/2 x^2 0.5 < x <= 1.5 + 0 1.5 < x + + Mathematica: + + g[x_] := Piecewise [ { + {9/8 + 3/2 x + 1/2 x^2 , -1.5 < x <= -.5}, + {3/4 - x^2 , -.5 < x <= .5}, + {9/8 - 3/2 x + 1/2 x^2 , 0.5 < x <= 1.5} + }, 0] + + To get the profile curve of the blurred step function at the rectangle + edge, we evaluate the indefinite integral, which is piecewise cubic: + + 0 x <= -1.5 + 9/16 + 9/8 x + 3/4 x^2 + 1/6 x^3 -1.5 < x <= -0.5 + 1/2 + 3/4 x - 1/3 x^3 -.5 < x <= .5 + 7/16 + 9/8 x - 3/4 x^2 + 1/6 x^3 .5 < x <= 1.5 + 1 1.5 < x + + in Mathematica code: + + gi[x_] := Piecewise[ { + { 0 , x <= -1.5 }, + { 9/16 + 9/8 x + 3/4 x^2 + 1/6 x^3, -1.5 < x <= -0.5 }, + { 1/2 + 3/4 x - 1/3 x^3 , -.5 < x <= .5}, + { 7/16 + 9/8 x - 3/4 x^2 + 1/6 x^3, .5 < x <= 1.5} + },1] +*/ + +static float gaussianIntegral(float x) { + if (x > 1.5f) { + return 0.0f; + } + if (x < -1.5f) { + return 1.0f; + } + + float x2 = x*x; + float x3 = x2*x; + + if ( x > 0.5f ) { + return 0.5625f - (x3 / 6.0f - 3.0f * x2 * 0.25f + 1.125f * x); + } + if ( x > -0.5f ) { + return 0.5f - (0.75f * x - x3 / 3.0f); + } + return 0.4375f + (-x3 / 6.0f - 3.0f * x2 * 0.25f - 1.125f * x); +} + +/* ComputeBlurProfile fills in an array of floating + point values between 0 and 255 for the profile signature of + a blurred half-plane with the given blur radius. Since we're + going to be doing screened multiplications (i.e., 1 - (1-x)(1-y)) + all the time, we actually fill in the profile pre-inverted + (already done 255-x). +*/ + +void SkBlurMask::ComputeBlurProfile(uint8_t* profile, int size, SkScalar sigma) { + SkASSERT(SkScalarCeilToInt(6*sigma) == size); + + int center = size >> 1; + + float invr = 1.f/(2*sigma); + + profile[0] = 255; + for (int x = 1 ; x < size ; ++x) { + float scaled_x = (center - x - .5f) * invr; + float gi = gaussianIntegral(scaled_x); + profile[x] = 255 - (uint8_t) (255.f * gi); + } +} + +// TODO MAYBE: Maintain a profile cache to avoid recomputing this for +// commonly used radii. Consider baking some of the most common blur radii +// directly in as static data? + +// Implementation adapted from Michael Herf's approach: +// http://stereopsis.com/shadowrect/ + +uint8_t SkBlurMask::ProfileLookup(const uint8_t *profile, int loc, + int blurredWidth, int sharpWidth) { + // how far are we from the original edge? + int dx = SkAbs32(((loc << 1) + 1) - blurredWidth) - sharpWidth; + int ox = dx >> 1; + if (ox < 0) { + ox = 0; + } + + return profile[ox]; +} + +void SkBlurMask::ComputeBlurredScanline(uint8_t *pixels, const uint8_t *profile, + unsigned int width, SkScalar sigma) { + + unsigned int profile_size = SkScalarCeilToInt(6*sigma); + SkAutoTMalloc<uint8_t> horizontalScanline(width); + + unsigned int sw = width - profile_size; + // nearest odd number less than the profile size represents the center + // of the (2x scaled) profile + int center = ( profile_size & ~1 ) - 1; + + int w = sw - center; + + for (unsigned int x = 0 ; x < width ; ++x) { + if (profile_size <= sw) { + pixels[x] = ProfileLookup(profile, x, width, w); + } else { + float span = float(sw)/(2*sigma); + float giX = 1.5f - (x+.5f)/(2*sigma); + pixels[x] = (uint8_t) (255 * (gaussianIntegral(giX) - gaussianIntegral(giX + span))); + } + } +} + +bool SkBlurMask::BlurRect(SkScalar sigma, SkMask *dst, + const SkRect &src, SkBlurStyle style, + SkIPoint *margin, SkMask::CreateMode createMode) { + int profileSize = SkScalarCeilToInt(6*sigma); + if (profileSize <= 0) { + return false; // no blur to compute + } + + int pad = profileSize/2; + if (margin) { + margin->set( pad, pad ); + } + + dst->fBounds.set(SkScalarRoundToInt(src.fLeft - pad), + SkScalarRoundToInt(src.fTop - pad), + SkScalarRoundToInt(src.fRight + pad), + SkScalarRoundToInt(src.fBottom + pad)); + + dst->fRowBytes = dst->fBounds.width(); + dst->fFormat = SkMask::kA8_Format; + dst->fImage = nullptr; + + int sw = SkScalarFloorToInt(src.width()); + int sh = SkScalarFloorToInt(src.height()); + + if (createMode == SkMask::kJustComputeBounds_CreateMode) { + if (style == kInner_SkBlurStyle) { + dst->fBounds.set(SkScalarRoundToInt(src.fLeft), + SkScalarRoundToInt(src.fTop), + SkScalarRoundToInt(src.fRight), + SkScalarRoundToInt(src.fBottom)); // restore trimmed bounds + dst->fRowBytes = sw; + } + return true; + } + + SkAutoTMalloc<uint8_t> profile(profileSize); + + ComputeBlurProfile(profile, profileSize, sigma); + + size_t dstSize = dst->computeImageSize(); + if (0 == dstSize) { + return false; // too big to allocate, abort + } + + uint8_t* dp = SkMask::AllocImage(dstSize); + + dst->fImage = dp; + + int dstHeight = dst->fBounds.height(); + int dstWidth = dst->fBounds.width(); + + uint8_t *outptr = dp; + + SkAutoTMalloc<uint8_t> horizontalScanline(dstWidth); + SkAutoTMalloc<uint8_t> verticalScanline(dstHeight); + + ComputeBlurredScanline(horizontalScanline, profile, dstWidth, sigma); + ComputeBlurredScanline(verticalScanline, profile, dstHeight, sigma); + + for (int y = 0 ; y < dstHeight ; ++y) { + for (int x = 0 ; x < dstWidth ; x++) { + unsigned int maskval = SkMulDiv255Round(horizontalScanline[x], verticalScanline[y]); + *(outptr++) = maskval; + } + } + + if (style == kInner_SkBlurStyle) { + // now we allocate the "real" dst, mirror the size of src + size_t srcSize = (size_t)(src.width() * src.height()); + if (0 == srcSize) { + return false; // too big to allocate, abort + } + dst->fImage = SkMask::AllocImage(srcSize); + for (int y = 0 ; y < sh ; y++) { + uint8_t *blur_scanline = dp + (y+pad)*dstWidth + pad; + uint8_t *inner_scanline = dst->fImage + y*sw; + memcpy(inner_scanline, blur_scanline, sw); + } + SkMask::FreeImage(dp); + + dst->fBounds.set(SkScalarRoundToInt(src.fLeft), + SkScalarRoundToInt(src.fTop), + SkScalarRoundToInt(src.fRight), + SkScalarRoundToInt(src.fBottom)); // restore trimmed bounds + dst->fRowBytes = sw; + + } else if (style == kOuter_SkBlurStyle) { + for (int y = pad ; y < dstHeight-pad ; y++) { + uint8_t *dst_scanline = dp + y*dstWidth + pad; + memset(dst_scanline, 0, sw); + } + } else if (style == kSolid_SkBlurStyle) { + for (int y = pad ; y < dstHeight-pad ; y++) { + uint8_t *dst_scanline = dp + y*dstWidth + pad; + memset(dst_scanline, 0xff, sw); + } + } + // normal and solid styles are the same for analytic rect blurs, so don't + // need to handle solid specially. + + return true; +} + +bool SkBlurMask::BlurRRect(SkScalar sigma, SkMask *dst, + const SkRRect &src, SkBlurStyle style, + SkIPoint *margin, SkMask::CreateMode createMode) { + // Temporary for now -- always fail, should cause caller to fall back + // to old path. Plumbing just to land API and parallelize effort. + + return false; +} + +// The "simple" blur is a direct implementation of separable convolution with a discrete +// gaussian kernel. It's "ground truth" in a sense; too slow to be used, but very +// useful for correctness comparisons. + +bool SkBlurMask::BlurGroundTruth(SkScalar sigma, SkMask* dst, const SkMask& src, + SkBlurStyle style, SkIPoint* margin) { + + if (src.fFormat != SkMask::kA8_Format) { + return false; + } + + float variance = sigma * sigma; + + int windowSize = SkScalarCeilToInt(sigma*6); + // round window size up to nearest odd number + windowSize |= 1; + + SkAutoTMalloc<float> gaussWindow(windowSize); + + int halfWindow = windowSize >> 1; + + gaussWindow[halfWindow] = 1; + + float windowSum = 1; + for (int x = 1 ; x <= halfWindow ; ++x) { + float gaussian = expf(-x*x / (2*variance)); + gaussWindow[halfWindow + x] = gaussWindow[halfWindow-x] = gaussian; + windowSum += 2*gaussian; + } + + // leave the filter un-normalized for now; we will divide by the normalization + // sum later; + + int pad = halfWindow; + if (margin) { + margin->set( pad, pad ); + } + + dst->fBounds = src.fBounds; + dst->fBounds.outset(pad, pad); + + dst->fRowBytes = dst->fBounds.width(); + dst->fFormat = SkMask::kA8_Format; + dst->fImage = nullptr; + + if (src.fImage) { + + size_t dstSize = dst->computeImageSize(); + if (0 == dstSize) { + return false; // too big to allocate, abort + } + + int srcWidth = src.fBounds.width(); + int srcHeight = src.fBounds.height(); + int dstWidth = dst->fBounds.width(); + + const uint8_t* srcPixels = src.fImage; + uint8_t* dstPixels = SkMask::AllocImage(dstSize); + SkAutoTCallVProc<uint8_t, SkMask_FreeImage> autoCall(dstPixels); + + // do the actual blur. First, make a padded copy of the source. + // use double pad so we never have to check if we're outside anything + + int padWidth = srcWidth + 4*pad; + int padHeight = srcHeight; + int padSize = padWidth * padHeight; + + SkAutoTMalloc<uint8_t> padPixels(padSize); + memset(padPixels, 0, padSize); + + for (int y = 0 ; y < srcHeight; ++y) { + uint8_t* padptr = padPixels + y * padWidth + 2*pad; + const uint8_t* srcptr = srcPixels + y * srcWidth; + memcpy(padptr, srcptr, srcWidth); + } + + // blur in X, transposing the result into a temporary floating point buffer. + // also double-pad the intermediate result so that the second blur doesn't + // have to do extra conditionals. + + int tmpWidth = padHeight + 4*pad; + int tmpHeight = padWidth - 2*pad; + int tmpSize = tmpWidth * tmpHeight; + + SkAutoTMalloc<float> tmpImage(tmpSize); + memset(tmpImage, 0, tmpSize*sizeof(tmpImage[0])); + + for (int y = 0 ; y < padHeight ; ++y) { + uint8_t *srcScanline = padPixels + y*padWidth; + for (int x = pad ; x < padWidth - pad ; ++x) { + float *outPixel = tmpImage + (x-pad)*tmpWidth + y + 2*pad; // transposed output + uint8_t *windowCenter = srcScanline + x; + for (int i = -pad ; i <= pad ; ++i) { + *outPixel += gaussWindow[pad+i]*windowCenter[i]; + } + *outPixel /= windowSum; + } + } + + // blur in Y; now filling in the actual desired destination. We have to do + // the transpose again; these transposes guarantee that we read memory in + // linear order. + + for (int y = 0 ; y < tmpHeight ; ++y) { + float *srcScanline = tmpImage + y*tmpWidth; + for (int x = pad ; x < tmpWidth - pad ; ++x) { + float *windowCenter = srcScanline + x; + float finalValue = 0; + for (int i = -pad ; i <= pad ; ++i) { + finalValue += gaussWindow[pad+i]*windowCenter[i]; + } + finalValue /= windowSum; + uint8_t *outPixel = dstPixels + (x-pad)*dstWidth + y; // transposed output + int integerPixel = int(finalValue + 0.5f); + *outPixel = SkClampMax( SkClampPos(integerPixel), 255 ); + } + } + + dst->fImage = dstPixels; + // if need be, alloc the "real" dst (same size as src) and copy/merge + // the blur into it (applying the src) + if (style == kInner_SkBlurStyle) { + // now we allocate the "real" dst, mirror the size of src + size_t srcSize = src.computeImageSize(); + if (0 == srcSize) { + return false; // too big to allocate, abort + } + dst->fImage = SkMask::AllocImage(srcSize); + merge_src_with_blur(dst->fImage, src.fRowBytes, + srcPixels, src.fRowBytes, + dstPixels + pad*dst->fRowBytes + pad, + dst->fRowBytes, srcWidth, srcHeight); + SkMask::FreeImage(dstPixels); + } else if (style != kNormal_SkBlurStyle) { + clamp_with_orig(dstPixels + pad*dst->fRowBytes + pad, + dst->fRowBytes, srcPixels, src.fRowBytes, srcWidth, srcHeight, style); + } + (void)autoCall.release(); + } + + if (style == kInner_SkBlurStyle) { + dst->fBounds = src.fBounds; // restore trimmed bounds + dst->fRowBytes = src.fRowBytes; + } + + return true; +} diff --git a/src/core/SkBlurMask.h b/src/core/SkBlurMask.h new file mode 100644 index 0000000000..67009b0bb5 --- /dev/null +++ b/src/core/SkBlurMask.h @@ -0,0 +1,88 @@ +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkBlurMask_DEFINED +#define SkBlurMask_DEFINED + +#include "SkBlurTypes.h" +#include "SkShader.h" +#include "SkMask.h" +#include "SkRRect.h" + +class SkBlurMask { +public: + static bool SK_WARN_UNUSED_RESULT BlurRect(SkScalar sigma, SkMask *dst, const SkRect &src, + SkBlurStyle, SkIPoint *margin = nullptr, + SkMask::CreateMode createMode = + SkMask::kComputeBoundsAndRenderImage_CreateMode); + static bool SK_WARN_UNUSED_RESULT BlurRRect(SkScalar sigma, SkMask *dst, const SkRRect &src, + SkBlurStyle, SkIPoint *margin = nullptr, + SkMask::CreateMode createMode = + SkMask::kComputeBoundsAndRenderImage_CreateMode); + + // forceQuality will prevent BoxBlur from falling back to the low quality approach when sigma + // is very small -- this can be used predict the margin bump ahead of time without completely + // replicating the internal logic. This permits not only simpler caching of blurred results, + // but also being able to predict precisely at what pixels the blurred profile of e.g. a + // rectangle will lie. + // + // Calling details: + // * calculate margin - if src.fImage is null, then this call only calculates the border. + // * failure - if src.fImage is not null, failure is signal with dst->fImage being + // null. + + static bool SK_WARN_UNUSED_RESULT BoxBlur(SkMask* dst, const SkMask& src, + SkScalar sigma, SkBlurStyle style, SkBlurQuality, + SkIPoint* margin = nullptr, + bool forceQuality = false); + + // the "ground truth" blur does a gaussian convolution; it's slow + // but useful for comparison purposes. + static bool SK_WARN_UNUSED_RESULT BlurGroundTruth(SkScalar sigma, SkMask* dst, + const SkMask& src, + SkBlurStyle, SkIPoint* margin = nullptr); + + // If radius > 0, return the corresponding sigma, else return 0 + static SkScalar SK_API ConvertRadiusToSigma(SkScalar radius); + // If sigma > 0.5, return the corresponding radius, else return 0 + static SkScalar SK_API ConvertSigmaToRadius(SkScalar sigma); + + /* Helper functions for analytic rectangle blurs */ + + /** Look up the intensity of the (one dimnensional) blurred half-plane. + @param profile The precomputed 1D blur profile; initialized by ComputeBlurProfile below. + @param loc the location to look up; The lookup will clamp invalid inputs, but + meaningful data are available between 0 and blurred_width + @param blurred_width The width of the final, blurred rectangle + @param sharp_width The width of the original, unblurred rectangle. + */ + static uint8_t ProfileLookup(const uint8_t* profile, int loc, int blurredWidth, int sharpWidth); + + /** Populate the profile of a 1D blurred halfplane. + @param profile The 1D table to fill in + @param size Should be 6*sigma bytes + @param sigma The standard deviation of the gaussian blur kernel + */ + static void ComputeBlurProfile(uint8_t* profile, int size, SkScalar sigma); + + /** Compute an entire scanline of a blurred step function. This is a 1D helper that + will produce both the horizontal and vertical profiles of the blurry rectangle. + @param pixels Location to store the resulting pixel data; allocated and managed by caller + @param profile Precomputed blur profile computed by ComputeBlurProfile above. + @param width Size of the pixels array. + @param sigma Standard deviation of the gaussian blur kernel used to compute the profile; + this implicitly gives the size of the pixels array. + */ + + static void ComputeBlurredScanline(uint8_t* pixels, const uint8_t* profile, + unsigned int width, SkScalar sigma); + + + +}; + +#endif |