diff --git a/modules/juce_graphics/effects/juce_DropShadowEffect.cpp b/modules/juce_graphics/effects/juce_DropShadowEffect.cpp index a2d964e384..73a1b31b5c 100644 --- a/modules/juce_graphics/effects/juce_DropShadowEffect.cpp +++ b/modules/juce_graphics/effects/juce_DropShadowEffect.cpp @@ -49,8 +49,9 @@ void DropShadow::drawForImage (Graphics& g, const Image& srcImage) const return; Image blurred; - srcImage.convertedToFormat (Image::SingleChannel) - .applySingleChannelBoxBlurEffect (radius, blurred); + ImageEffects::applySingleChannelBoxBlurEffect (radius, + srcImage.convertedToFormat (Image::SingleChannel), + blurred); g.setColour (colour); g.drawImageAt (blurred, offset.x, offset.y, true); @@ -76,7 +77,7 @@ void DropShadow::drawForPath (Graphics& g, const Path& path) const } Image blurred; - pathImage.applySingleChannelBoxBlurEffect (radius, blurred); + ImageEffects::applySingleChannelBoxBlurEffect (radius, pathImage, blurred); g.setColour (colour); g.drawImageAt (blurred, area.getX(), area.getY(), true); diff --git a/modules/juce_graphics/effects/juce_GlowEffect.cpp b/modules/juce_graphics/effects/juce_GlowEffect.cpp index 9c4ca4cc2d..347241c66c 100644 --- a/modules/juce_graphics/effects/juce_GlowEffect.cpp +++ b/modules/juce_graphics/effects/juce_GlowEffect.cpp @@ -47,7 +47,7 @@ void GlowEffect::setGlowProperties (float newRadius, Colour newColour, PointgetSharedCount(); } -void Image::applyGaussianBlurEffect (float radius, Image& result) const -{ - if (image == nullptr) - { - result = {}; - return; - } - - auto copy = result; - image->applyGaussianBlurEffect (radius, copy); - - if (copy.isValid()) - { - result = std::move (copy); - return; - } - - const auto tie = [] (const auto& x) { return std::tuple (x.getFormat(), x.getWidth(), x.getHeight()); }; - - if (tie (*this) != tie (result)) - result = Image { getFormat(), getWidth(), getHeight(), false }; - - ImageConvolutionKernel blurKernel (roundToInt (radius * 2.0f)); - - blurKernel.createGaussianBlur (radius); - - blurKernel.applyToImage (result, *this, result.getBounds()); -} - -static void blurDataTriplets (uint8* d, int num, const int delta) noexcept -{ - uint32 last = d[0]; - d[0] = (uint8) ((d[0] + d[delta] + 1) / 3); - d += delta; - - num -= 2; - - do - { - const uint32 newLast = d[0]; - d[0] = (uint8) ((last + d[0] + d[delta] + 1) / 3); - d += delta; - last = newLast; - } - while (--num > 0); - - d[0] = (uint8) ((last + d[0] + 1) / 3); -} - -static void blurSingleChannelImage (uint8* const data, const int width, const int height, - const int lineStride, const int repetitions) noexcept -{ - jassert (width > 2 && height > 2); - - for (int y = 0; y < height; ++y) - for (int i = repetitions; --i >= 0;) - blurDataTriplets (data + lineStride * y, width, 1); - - for (int x = 0; x < width; ++x) - for (int i = repetitions; --i >= 0;) - blurDataTriplets (data + x, height, lineStride); -} - -static void blurSingleChannelImage (Image& image, int radius) -{ - const Image::BitmapData bm (image, Image::BitmapData::readWrite); - blurSingleChannelImage (bm.data, bm.width, bm.height, bm.lineStride, 2 * radius); -} - -void Image::applySingleChannelBoxBlurEffect (int radius, Image& result) const -{ - if (image == nullptr) - { - result = {}; - return; - } - - auto copy = result; - image->applySingleChannelBoxBlurEffect (radius, copy); - - if (copy.isValid()) - { - result = std::move (copy); - return; - } - - if (std::tuple (SingleChannel, getWidth(), getHeight()) != std::tuple (result.getFormat(), result.getWidth(), result.getHeight())) - result = Image { SingleChannel, getWidth(), getHeight(), false }; - - { - BitmapData source { *this, BitmapData::readOnly }; - BitmapData dest { result, BitmapData::writeOnly }; - BitmapDataDetail::convert (source, dest); - } - - blurSingleChannelImage (result, radius); -} - bool Image::isValid() const noexcept { return image != nullptr; @@ -828,6 +730,111 @@ void Image::moveImageSection (int dx, int dy, } } +void ImageEffects::applyGaussianBlurEffect (float radius, const Image& input, Image& result) +{ + auto* image = input.getPixelData(); + + if (image == nullptr) + { + result = {}; + return; + } + + auto copy = result; + image->applyGaussianBlurEffect (radius, copy); + + if (copy.isValid()) + { + result = std::move (copy); + return; + } + + const auto tie = [] (const auto& x) { return std::tuple (x.getFormat(), x.getWidth(), x.getHeight()); }; + + if (tie (input) != tie (result)) + result = Image { input.getFormat(), input.getWidth(), input.getHeight(), false }; + + ImageConvolutionKernel blurKernel (roundToInt (radius * 2.0f)); + + blurKernel.createGaussianBlur (radius); + + blurKernel.applyToImage (result, input, result.getBounds()); +} + +static void blurDataTriplets (uint8* d, int num, const int delta) noexcept +{ + uint32 last = d[0]; + d[0] = (uint8) ((d[0] + d[delta] + 1) / 3); + d += delta; + + num -= 2; + + do + { + const uint32 newLast = d[0]; + d[0] = (uint8) ((last + d[0] + d[delta] + 1) / 3); + d += delta; + last = newLast; + } + while (--num > 0); + + d[0] = (uint8) ((last + d[0] + 1) / 3); +} + +static void blurSingleChannelImage (uint8* const data, const int width, const int height, + const int lineStride, const int repetitions) noexcept +{ + jassert (width > 2 && height > 2); + + for (int y = 0; y < height; ++y) + for (int i = repetitions; --i >= 0;) + blurDataTriplets (data + lineStride * y, width, 1); + + for (int x = 0; x < width; ++x) + for (int i = repetitions; --i >= 0;) + blurDataTriplets (data + x, height, lineStride); +} + +static void blurSingleChannelImage (Image& image, int radius) +{ + const Image::BitmapData bm (image, Image::BitmapData::readWrite); + blurSingleChannelImage (bm.data, bm.width, bm.height, bm.lineStride, 2 * radius); +} + +void ImageEffects::applySingleChannelBoxBlurEffect (int radius, const Image& input, Image& result) +{ + auto* image = input.getPixelData(); + + if (image == nullptr) + { + result = {}; + return; + } + + auto copy = result; + image->applySingleChannelBoxBlurEffect (radius, copy); + + if (copy.isValid()) + { + result = std::move (copy); + return; + } + + const auto inputConfig = std::tuple (Image::SingleChannel, input.getWidth(), input.getHeight()); + const auto outputConfig = std::tuple (result.getFormat(), result.getWidth(), result.getHeight()); + + if (inputConfig != outputConfig) + result = Image { Image::SingleChannel, input.getWidth(), input.getHeight(), false }; + + { + Image::BitmapData source { input, Image::BitmapData::readOnly }; + Image::BitmapData dest { result, Image::BitmapData::writeOnly }; + BitmapDataDetail::convert (source, dest); + } + + blurSingleChannelImage (result, radius); +} + //============================================================================== #if JUCE_ALLOW_STATIC_NULL_VARIABLES diff --git a/modules/juce_graphics/images/juce_Image.h b/modules/juce_graphics/images/juce_Image.h index 627dabc132..b0d322a558 100644 --- a/modules/juce_graphics/images/juce_Image.h +++ b/modules/juce_graphics/images/juce_Image.h @@ -416,20 +416,6 @@ public: */ int getReferenceCount() const noexcept; - /** Applies a blur to this image, placing the blurred image in the result out-parameter. - - If result is already the correct size, then its storage will be reused directly. - Otherwise, new storage may be allocated for the blurred image. - */ - void applyGaussianBlurEffect (float radius, Image& result) const; - - /** Applies a blur to this image, placing the blurred image in the result out-parameter. - - If result is already the correct size, then its storage will be reused directly. - Otherwise, new storage may be allocated for the blurred image. - */ - void applySingleChannelBoxBlurEffect (int radius, Image& result) const; - //============================================================================== /** @internal */ ImagePixelData* getPixelData() const noexcept { return image.get(); } @@ -488,6 +474,8 @@ public: virtual int getSharedCount() const noexcept; /** Applies a native blur effect to this image, if available. + This blur applies to all channels of the input image. It may be more expensive to + calculate than a box blur, but should produce higher-quality results. Implementations should attempt to re-use the storage provided in the result out-parameter when possible. @@ -498,6 +486,9 @@ public: virtual void applyGaussianBlurEffect (float radius, Image& result); /** Applies a native blur effect to this image, if available. + This is intended for blurring single-channel images, which is useful when rendering drop + shadows. This is implemented as several box-blurs in series. The results should be visually + similar to a Gaussian blur, but less accurate. Implementations should attempt to re-use the storage provided in the result out-parameter when possible. @@ -505,7 +496,7 @@ public: If native blurs are unsupported, or if creating a blur fails for any other reason, the result out-parameter will be reset to an invalid image. */ - virtual void applySingleChannelBoxBlurEffect (float radius, Image& result); + virtual void applySingleChannelBoxBlurEffect (int radius, Image& result); /** The pixel format of the image data. */ const Image::PixelFormat pixelFormat; @@ -597,4 +588,43 @@ public: int getTypeID() const override; }; +//============================================================================== +/** + Utility functions for applying effects to images. These effects may or may not + be hardware-accelerated. + + @tags{Graphics} +*/ +struct ImageEffects +{ + ImageEffects() = delete; + + /** Applies a blur to this image, placing the blurred image in the result out-parameter. + This will attempt to call the applyGaussianBlurEffect() member of the input image's + underlying ImagePixelData, which will use hardware acceleration if available. If this + fails, then a software blur will be applied instead. + + This blur applies to all channels of the input image. It may be more expensive to + calculate than a box blur, but should produce higher-quality results. + + If result is already the correct size, then its storage will be reused directly. + Otherwise, new storage may be allocated for the blurred image. + */ + static void applyGaussianBlurEffect (float radius, const Image& input, Image& result); + + /** Applies a blur to this image, placing the blurred image in the result out-parameter. + This will attempt to call the applySingleChannelBoxBlurEffect() member of the input image's + underlying ImagePixelData, which will use hardware acceleration if available. If this + fails, then a software blur will be applied instead. + + This kind of blur is only capable of blurring single-channel images, which is useful when + rendering drop shadows. The blur is implemented as several box-blurs in series. The results + should be visually similar to a Gaussian blur, but less accurate. + + If result is already the correct size, then its storage will be reused directly. + Otherwise, new storage may be allocated for the blurred image. + */ + static void applySingleChannelBoxBlurEffect (int radius, const Image& input, Image& result); +}; + } // namespace juce diff --git a/modules/juce_graphics/native/juce_Direct2DImage_windows.cpp b/modules/juce_graphics/native/juce_Direct2DImage_windows.cpp index c4cb8438e9..cb95e6fb79 100644 --- a/modules/juce_graphics/native/juce_Direct2DImage_windows.cpp +++ b/modules/juce_graphics/native/juce_Direct2DImage_windows.cpp @@ -364,6 +364,88 @@ void Direct2DPixelData::applyGaussianBlurEffect (float radius, Image& result) outputDataContext->SetTarget (nullptr); } +void Direct2DPixelData::applySingleChannelBoxBlurEffect (int radius, Image& result) +{ + // The result must be a separate image! + jassert (result.getPixelData() != this); + + if (context == nullptr) + { + result = {}; + return; + } + + constexpr FLOAT kernel[] { 1.0f / 9.0f, 2.0f / 9.0f, 3.0f / 9.0f, 2.0f / 9.0f, 1.0f / 9.0f }; + + ComSmartPtr begin, end; + + for (auto horizontal : { false, true }) + { + for (auto i = 0; i < radius; ++i) + { + ComSmartPtr effect; + if (const auto hr = context->CreateEffect (CLSID_D2D1ConvolveMatrix, effect.resetAndGetPointerAddress()); + FAILED (hr) || effect == nullptr) + { + result = {}; + return; + } + + effect->SetValue (D2D1_CONVOLVEMATRIX_PROP_KERNEL_SIZE_X, (UINT32) (horizontal ? std::size (kernel) : 1)); + effect->SetValue (D2D1_CONVOLVEMATRIX_PROP_KERNEL_SIZE_Y, (UINT32) (horizontal ? 1 : std::size (kernel))); + effect->SetValue (D2D1_CONVOLVEMATRIX_PROP_KERNEL_MATRIX, kernel); + + if (begin == nullptr) + { + begin = effect; + end = effect; + } + else + { + effect->SetInputEffect (0, end); + end = effect; + } + } + } + + if (begin == nullptr) + { + result = {}; + return; + } + + begin->SetInput (0, getAdapterD2D1Bitmap()); + + const auto originalPixelData = dynamic_cast (result.getPixelData()); + + if (originalPixelData == nullptr || std::tuple (Image::SingleChannel, width, height) != std::tuple (originalPixelData->pixelFormat, originalPixelData->width, originalPixelData->height)) + result = Image { make (Image::SingleChannel, width, height, false, adapter) }; + + const auto outputPixelData = dynamic_cast (result.getPixelData()); + + if (outputPixelData == nullptr) + { + result = {}; + return; + } + + outputPixelData->createDeviceResources(); + auto outputDataContext = outputPixelData->context; + + if (outputDataContext == nullptr) + { + result = {}; + return; + } + + outputDataContext->SetTarget (outputPixelData->getAdapterD2D1Bitmap()); + outputDataContext->BeginDraw(); + outputDataContext->Clear(); + outputDataContext->DrawImage (end); + outputDataContext->EndDraw(); + outputDataContext->SetTarget (nullptr); +} + std::unique_ptr Direct2DPixelData::createType() const { return std::make_unique(); diff --git a/modules/juce_graphics/native/juce_Direct2DImage_windows.h b/modules/juce_graphics/native/juce_Direct2DImage_windows.h index cad495de55..54218426b3 100644 --- a/modules/juce_graphics/native/juce_Direct2DImage_windows.h +++ b/modules/juce_graphics/native/juce_Direct2DImage_windows.h @@ -58,6 +58,7 @@ public: ImagePixelData::Ptr clone() override; void applyGaussianBlurEffect (float radius, Image& result) override; + void applySingleChannelBoxBlurEffect (int radius, Image& result) override; std::unique_ptr createType() const override;