tdf#101181: improve glow effect
The shadow of objects must not be scaled: this displaces any internal
areas that need blur, e.g. holes. Instead, it needs to dilate the
shadow using kernel with radius equal to blur radius: this allows the
borders of dilated objects to be in the middle of the blur area. The
following blur makes those new margin points to have 50% intensity,
and full glow intensity at the point of old object margins. This also
removed artifacts when moving objects with glow effect caused by
mismatch between scaling and D2D range calculation.
The D2D range therefore is not calculated by scaling, but using grow.
Blur filter's "extend bitmap by blur radius" option got obsoleted and
removed.
There's no need to blur the glow color (24-bit RGB). Instead, glow
bitmap must be filled by glow color, and have an alpha mask that is
blurred accordingly. This makes the glow properly transparent, and
also reduces the blur complexity which now only needs to process 8
bits of alpha channel.
The object shadow is created using basegfx::BColorModifier_replace
inserted into the 2d decomposition of the effect, as before. To make
sure that any non-fully-transparent pixel will become black pixel in
the shadow, black color is used, and the result is further processed
in VclPixelProcessor2D::processGlowPrimitive2D with monochrome filter
using threshold 255.
Glow transparency attribute is taken into account: the initial value
at the margins of the objects. Color replacement filter is used to
replace the object shadow with the attribute value before blur pass.
Correct blur radius is used, calculated from glow effect radius,
instead of hardcoded value of 5 pixels. This makes the glow to fade
gradually along the full width of the effect, instead of only fading
in narrow outer border previously.
Since blur filter is only implemented for radius up to 254 pixels,
and since downsampling the shadow before blur increases performance
without noticeable quality loss, the image is downsampled before
filtering.
It should be noted that the glow effect is almost identical to soft
shadow effect, likely with the only difference of using dilation in
the former, but not in the latter. The code might be reused later to
implement soft shadow as well.
Change-Id: I728c532f9df7ccf85f353c23c6c7d8352d7b2086
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/93235
Tested-by: Jenkins
Reviewed-by: Tomaž Vajngerl <quikee@gmail.com>
Reviewed-by: Mike Kaganski <mike.kaganski@collabora.com>
diff --git a/drawinglayer/source/attribute/sdrglowattribute.cxx b/drawinglayer/source/attribute/sdrglowattribute.cxx
index 4295aef..90367ff 100644
--- a/drawinglayer/source/attribute/sdrglowattribute.cxx
+++ b/drawinglayer/source/attribute/sdrglowattribute.cxx
@@ -21,7 +21,7 @@ namespace drawinglayer
{
namespace attribute
{
SdrGlowAttribute::SdrGlowAttribute(sal_Int32 nRadius, const basegfx::BColor& rColor)
SdrGlowAttribute::SdrGlowAttribute(sal_Int32 nRadius, const Color& rColor)
: m_nRadius(nRadius)
, m_color(rColor)
{
@@ -44,31 +44,6 @@ bool SdrGlowAttribute::operator==(const SdrGlowAttribute& rCandidate) const
return m_nRadius == rCandidate.m_nRadius && m_color == rCandidate.m_color;
}
const basegfx::B2DHomMatrix& SdrGlowAttribute::GetTransfMatrix(basegfx::B2DRange nRange) const
{
if (!m_oTransfCache)
{
double dRadius100mm = static_cast<double>(m_nRadius) / 360.0;
// Apply a scaling with the center point of the shape as origin.
// 1) translate shape to the origin
basegfx::B2DHomMatrix matrix = basegfx::utils::createCoordinateSystemTransform(
nRange.getCenter(), basegfx::B2DVector(-1, 0), basegfx::B2DVector(0, -1));
basegfx::B2DHomMatrix inverse(matrix);
inverse.invert();
// 2) Scale up
double scale_x = (nRange.getWidth() + dRadius100mm) / nRange.getWidth();
double scale_y = (nRange.getHeight() + dRadius100mm) / nRange.getHeight();
matrix *= basegfx::utils::createScaleB2DHomMatrix(scale_x, scale_y);
// 3) Translate shape back to its place
matrix *= inverse;
m_oTransfCache = std::move(matrix);
}
return *m_oTransfCache;
}
} // end of namespace attribute
} // end of namespace drawinglayer
diff --git a/drawinglayer/source/primitive2d/glowprimitive2d.cxx b/drawinglayer/source/primitive2d/glowprimitive2d.cxx
index 7d6c23a..bf49b8e 100644
--- a/drawinglayer/source/primitive2d/glowprimitive2d.cxx
+++ b/drawinglayer/source/primitive2d/glowprimitive2d.cxx
@@ -30,12 +30,11 @@ using namespace com::sun::star;
namespace drawinglayer::primitive2d
{
GlowPrimitive2D::GlowPrimitive2D(const basegfx::B2DHomMatrix& rGlowTransform,
const basegfx::BColor& rGlowColor,
GlowPrimitive2D::GlowPrimitive2D(const Color& rGlowColor, double fRadius,
const Primitive2DContainer& rChildren)
: GroupPrimitive2D(rChildren)
, maGlowTransform(rGlowTransform)
, maGlowColor(rGlowColor)
, mfGlowRadius(fRadius)
{
}
@@ -45,7 +44,7 @@ bool GlowPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
{
const GlowPrimitive2D& rCompare = static_cast<const GlowPrimitive2D&>(rPrimitive);
return (getGlowTransform() == rCompare.getGlowTransform()
return (getGlowRadius() == rCompare.getGlowRadius()
&& getGlowColor() == rCompare.getGlowColor());
}
@@ -55,8 +54,9 @@ bool GlowPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
basegfx::B2DRange
GlowPrimitive2D::getB2DRange(const geometry::ViewInformation2D& rViewInformation) const
{
basegfx::B2DRange aRetval(getChildren().getB2DRange(rViewInformation));
aRetval.transform(getGlowTransform());
basegfx::B2DRange aRetval(GroupPrimitive2D::getB2DRange(rViewInformation));
// We need additional space for the glow from all sides
aRetval.grow(getGlowRadius());
return aRetval;
}
@@ -67,15 +67,13 @@ void GlowPrimitive2D::get2DDecomposition(
if (getChildren().empty())
return;
// create a modifiedColorPrimitive containing the Glow color and the content
// create a modifiedColorPrimitive containing the *black* color and the content. Using black
// on white allows creating useful mask in VclPixelProcessor2D::processGlowPrimitive2D.
basegfx::BColorModifierSharedPtr aBColorModifier
= std::make_shared<basegfx::BColorModifier_replace>(getGlowColor());
= std::make_shared<basegfx::BColorModifier_replace>(basegfx::BColor());
const Primitive2DReference xRefA(new ModifiedColorPrimitive2D(getChildren(), aBColorModifier));
const Primitive2DContainer aSequenceB{ xRefA };
// build transformed primitiveVector with Glow offset and add to target
rVisitor.append(new TransformPrimitive2D(getGlowTransform(), aSequenceB));
const Primitive2DReference xRef(new ModifiedColorPrimitive2D(getChildren(), aBColorModifier));
rVisitor.append(xRef);
}
// provide unique ID
diff --git a/drawinglayer/source/processor2d/vclpixelprocessor2d.cxx b/drawinglayer/source/processor2d/vclpixelprocessor2d.cxx
index bba66e2..6c14c09 100644
--- a/drawinglayer/source/processor2d/vclpixelprocessor2d.cxx
+++ b/drawinglayer/source/processor2d/vclpixelprocessor2d.cxx
@@ -23,7 +23,10 @@
#include <sal/log.hxx>
#include <tools/stream.hxx>
#include <vcl/BitmapBasicMorphologyFilter.hxx>
#include <vcl/BitmapColorReplaceFilter.hxx>
#include <vcl/BitmapFilterStackBlur.hxx>
#include <vcl/BitmapMonochromeFilter.hxx>
#include <vcl/outdev.hxx>
#include <vcl/dibtools.hxx>
#include <vcl/hatch.hxx>
@@ -912,42 +915,73 @@ void VclPixelProcessor2D::processGlowPrimitive2D(const primitive2d::GlowPrimitiv
{
basegfx::B2DRange aRange(rCandidate.getB2DRange(getViewInformation2D()));
aRange.transform(maCurrentTransformation);
aRange.grow(10.0);
basegfx::B2DVector aGlowRadiusVector(rCandidate.getGlowRadius(), 0);
// Calculate the pixel size of glow radius in current transformation
aGlowRadiusVector *= maCurrentTransformation;
const double fGlowRadius = aGlowRadiusVector.getLength();
impBufferDevice aBufferDevice(*mpOutputDevice, aRange);
if (aBufferDevice.isVisible())
{
// remember last OutDev and set to content
OutputDevice* pLastOutputDevice = mpOutputDevice;
mpOutputDevice = &aBufferDevice.getTransparence();
// paint content to virtual device
mpOutputDevice = &aBufferDevice.getContent();
// Processing will draw whatever geometry on white background, applying *black*
// replacement color. The black color replacement is added in 2d decomposition of
// glow primitive.
mpOutputDevice->Erase();
process(rCandidate);
// obtain result as a bitmap
auto bitmap = mpOutputDevice->GetBitmapEx(Point(aRange.getMinX(), aRange.getMinY()),
Bitmap bitmap = mpOutputDevice->GetBitmap(Point(aRange.getMinX(), aRange.getMinY()),
Size(aRange.getWidth(), aRange.getHeight()));
constexpr double nRadius = 5.0;
bitmap.Scale(Size(aRange.getWidth() - nRadius, aRange.getHeight() - nRadius));
// use bitmap later as mask
auto mask = bitmap.GetBitmap();
mpOutputDevice = &aBufferDevice.getContent();
process(rCandidate);
bitmap = mpOutputDevice->GetBitmapEx(Point(aRange.getMinX(), aRange.getMinY()),
Size(aRange.getWidth(), aRange.getHeight()));
bitmap.Scale(Size(aRange.getWidth() - nRadius, aRange.getHeight() - nRadius));
BitmapEx mask(bitmap); // copy the bitmap to mask
// Only completely transparent parts will be completely white; only those must be
// considered white on the initial B&W alpha mask. Any other color must be treated
// as black.
BitmapFilter::Filter(mask, BitmapMonochromeFilter(255));
// Scaling down increases performance without noticeable quality loss. Additionally,
// current blur implementation can only handle blur radius between 2 and 254.
Size aSize = mask.GetSizePixel();
double fScale = 1.0;
// Glow radius is the size of the halo from each side of the object. The halo is the
// border of glow color that fades from glow transparency level to fully transparent
// When blurring a sharp boundary (our case), it gets 50% of original intensity, and
// fades to both sides by the blur radius; thus blur radius is half of glow radius.
double fBlurRadius = fGlowRadius / 2;
while (fBlurRadius > 254 || aSize.Height() > 1000 || aSize.Width() > 1000)
{
fScale /= 2;
fBlurRadius /= 2;
aSize.setHeight(aSize.Height() / 2);
aSize.setWidth(aSize.Width() / 2);
}
// BmpScaleFlag::Fast is important for following color replacement
mask.Scale(fScale, fScale, BmpScaleFlag::Fast);
// Dilate the black pixels using blur radius, to make blur start at actual object margins.
// This differentiates glow from blurry shadow; so potentially extend this function to also
// handle blurry shadow, and conditionally skip this step
BitmapFilter::Filter(mask, BitmapDilateFilter(fBlurRadius));
// We need 8-bit grey mask for blurring
mask.Convert(BmpConversion::N8BitGreys);
// Consider glow transparency (initial transparency near the object edge)
const sal_uInt8 nTransparency = rCandidate.getGlowColor().GetTransparency();
const Color aTransparency(nTransparency, nTransparency, nTransparency);
BitmapFilter::Filter(mask, BitmapColorReplaceFilter(COL_BLACK, aTransparency));
// calculate blurry effect
BitmapFilterStackBlur glowFilter(nRadius);
BitmapFilter::Filter(bitmap, glowFilter);
BitmapFilter::Filter(mask, BitmapFilterStackBlur(fBlurRadius));
// The end result is the bitmap filled with glow color and blurred 8-bit alpha mask
bitmap.Erase(rCandidate.getGlowColor());
// alpha mask will be scaled up automatically to match bitmap
BitmapEx result(bitmap, AlphaMask(mask.GetBitmap()));
// back to old OutDev
mpOutputDevice = pLastOutputDevice;
mpOutputDevice->DrawBitmapEx(
Point(aRange.getMinX() - nRadius / 2, aRange.getMinY() - nRadius / 2),
BitmapEx(bitmap.GetBitmap(), mask));
// paint result
//aBufferDevice.paint();
mpOutputDevice->DrawBitmapEx(Point(aRange.getMinX(), aRange.getMinY()), result);
}
else
SAL_WARN("drawinglayer", "Temporary buffered virtual device is not visible");
diff --git a/include/drawinglayer/attribute/sdrglowattribute.hxx b/include/drawinglayer/attribute/sdrglowattribute.hxx
index f5120c1..b17560b 100644
--- a/include/drawinglayer/attribute/sdrglowattribute.hxx
+++ b/include/drawinglayer/attribute/sdrglowattribute.hxx
@@ -15,8 +15,7 @@
#include <basegfx/matrix/b2dhommatrix.hxx>
#include <basegfx/color/bcolor.hxx>
#include <basegfx/range/b2drange.hxx>
#include <optional>
#include <tools/color.hxx>
namespace drawinglayer
{
@@ -26,11 +25,10 @@ class DRAWINGLAYER_DLLPUBLIC SdrGlowAttribute
{
private:
sal_Int32 m_nRadius = 0;
mutable std::optional<basegfx::B2DHomMatrix> m_oTransfCache;
basegfx::BColor m_color;
Color m_color; // Includes alpha!
public:
SdrGlowAttribute(sal_Int32 nRadius, const basegfx::BColor& rColor);
SdrGlowAttribute(sal_Int32 nRadius, const Color& rColor);
SdrGlowAttribute();
SdrGlowAttribute(const SdrGlowAttribute&);
SdrGlowAttribute(SdrGlowAttribute&&);
@@ -41,10 +39,9 @@ public:
SdrGlowAttribute& operator=(SdrGlowAttribute&&);
// data access
const basegfx::B2DHomMatrix& GetTransfMatrix(basegfx::B2DRange nCenter) const;
const basegfx::BColor& getColor() const { return m_color; };
sal_Int32 getRadius() const { return m_nRadius; };
bool isDefault() const { return m_nRadius == 0; };
const Color& getColor() const { return m_color; }
sal_Int32 getRadius() const { return m_nRadius; }
bool isDefault() const { return m_nRadius == 0; }
};
} // end of namespace attribute
} // end of namespace drawinglayer
diff --git a/include/drawinglayer/primitive2d/glowprimitive2d.hxx b/include/drawinglayer/primitive2d/glowprimitive2d.hxx
index 0c77a9a..1aacdf0 100644
--- a/include/drawinglayer/primitive2d/glowprimitive2d.hxx
+++ b/include/drawinglayer/primitive2d/glowprimitive2d.hxx
@@ -25,6 +25,7 @@
#include <drawinglayer/primitive2d/groupprimitive2d.hxx>
#include <basegfx/matrix/b2dhommatrix.hxx>
#include <basegfx/color/bcolor.hxx>
#include <tools/color.hxx>
namespace drawinglayer
{
@@ -33,20 +34,19 @@ namespace primitive2d
class DRAWINGLAYER_DLLPUBLIC GlowPrimitive2D final : public GroupPrimitive2D
{
private:
/// the Glow transformation, normally just an offset
basegfx::B2DHomMatrix maGlowTransform;
/// the Glow color to which all geometry is to be forced; includes alpha
Color maGlowColor;
/// the Glow color to which all geometry is to be forced
basegfx::BColor maGlowColor;
/// the Glow size, in logical units (100ths of mm)
double mfGlowRadius;
public:
/// constructor
GlowPrimitive2D(const basegfx::B2DHomMatrix& rGlowTransform, const basegfx::BColor& rGlowColor,
const Primitive2DContainer& rChildren);
GlowPrimitive2D(const Color& rGlowColor, double fRadius, const Primitive2DContainer& rChildren);
/// data read access
const basegfx::B2DHomMatrix& getGlowTransform() const { return maGlowTransform; }
const basegfx::BColor& getGlowColor() const { return maGlowColor; }
const Color& getGlowColor() const { return maGlowColor; }
double getGlowRadius() const { return mfGlowRadius; }
/// compare operator
virtual bool operator==(const BasePrimitive2D& rPrimitive) const override;
diff --git a/include/vcl/BitmapFilterStackBlur.hxx b/include/vcl/BitmapFilterStackBlur.hxx
index 425420d..8ac6a47 100644
--- a/include/vcl/BitmapFilterStackBlur.hxx
+++ b/include/vcl/BitmapFilterStackBlur.hxx
@@ -18,10 +18,9 @@
class VCL_DLLPUBLIC BitmapFilterStackBlur : public BitmapFilter
{
sal_Int32 mnRadius;
bool mbExtend;
public:
BitmapFilterStackBlur(sal_Int32 nRadius, bool bExtend = true);
BitmapFilterStackBlur(sal_Int32 nRadius);
virtual ~BitmapFilterStackBlur();
virtual BitmapEx execute(BitmapEx const& rBitmap) const override;
diff --git a/svx/source/sdr/primitive2d/sdrattributecreator.cxx b/svx/source/sdr/primitive2d/sdrattributecreator.cxx
index ddfddd5..bb43198 100644
--- a/svx/source/sdr/primitive2d/sdrattributecreator.cxx
+++ b/svx/source/sdr/primitive2d/sdrattributecreator.cxx
@@ -341,9 +341,12 @@ namespace drawinglayer::primitive2d
if(!bGlow)
return attribute::SdrGlowAttribute();
sal_Int32 nRadius = rSet.Get(SDRATTR_GLOW_RAD).GetValue();
const Color aColor(rSet.Get(SDRATTR_GLOW_COLOR).GetColorValue());
Color aColor(rSet.Get(SDRATTR_GLOW_COLOR).GetColorValue());
sal_uInt16 nTransparency(rSet.Get(SDRATTR_GLOW_TRANSPARENCY).GetValue());
if (nTransparency)
aColor.SetTransparency(std::round(nTransparency / 100.0 * 255.0));
attribute::SdrGlowAttribute glowAttr{ nRadius, aColor.getBColor() };
attribute::SdrGlowAttribute glowAttr{ nRadius, aColor };
return glowAttr;
}
diff --git a/svx/source/sdr/primitive2d/sdrdecompositiontools.cxx b/svx/source/sdr/primitive2d/sdrdecompositiontools.cxx
index 5ce394f..193d944 100644
--- a/svx/source/sdr/primitive2d/sdrdecompositiontools.cxx
+++ b/svx/source/sdr/primitive2d/sdrdecompositiontools.cxx
@@ -540,10 +540,7 @@ namespace drawinglayer::primitive2d
const uno::Sequence< beans::PropertyValue > xViewParameters;
geometry::ViewInformation2D aViewInformation2D(xViewParameters);
aRetval[0] = Primitive2DReference(
new GlowPrimitive2D(
rGlow.GetTransfMatrix(rContent.getB2DRange(aViewInformation2D)),
rGlow.getColor(),
rContent));
new GlowPrimitive2D(rGlow.getColor(), rGlow.getRadius() / 360.0, rContent));
aRetval[1] = Primitive2DReference(new GroupPrimitive2D(rContent));
return aRetval;
}
diff --git a/vcl/qa/cppunit/BitmapFilterTest.cxx b/vcl/qa/cppunit/BitmapFilterTest.cxx
index fec21fa..dddfaf5 100644
--- a/vcl/qa/cppunit/BitmapFilterTest.cxx
+++ b/vcl/qa/cppunit/BitmapFilterTest.cxx
@@ -119,8 +119,8 @@ void BitmapFilterTest::testBlurCorrectness()
}
// Check blurred bitmap parameters
CPPUNIT_ASSERT_EQUAL(static_cast<long>(45), aBitmap24Bit.GetSizePixel().Width());
CPPUNIT_ASSERT_EQUAL(static_cast<long>(35), aBitmap24Bit.GetSizePixel().Height());
CPPUNIT_ASSERT_EQUAL(static_cast<long>(41), aBitmap24Bit.GetSizePixel().Width());
CPPUNIT_ASSERT_EQUAL(static_cast<long>(31), aBitmap24Bit.GetSizePixel().Height());
CPPUNIT_ASSERT_EQUAL(nBPP, aBitmap24Bit.GetBitCount());
@@ -188,7 +188,7 @@ void BitmapFilterTest::testPerformance()
Bitmap aResult;
for (int i = 0; i < nIterations; i++)
{
BitmapFilterStackBlur aBlurFilter(250, false); // don't extend the image
BitmapFilterStackBlur aBlurFilter(250);
aResult = aBlurFilter.filter(aBigBitmap);
}
auto end = std::chrono::high_resolution_clock::now();
diff --git a/vcl/source/bitmap/BitmapFilterStackBlur.cxx b/vcl/source/bitmap/BitmapFilterStackBlur.cxx
index 9b3c371..da51dae 100644
--- a/vcl/source/bitmap/BitmapFilterStackBlur.cxx
+++ b/vcl/source/bitmap/BitmapFilterStackBlur.cxx
@@ -558,39 +558,6 @@ void stackBlur8(Bitmap& rBitmap, sal_Int32 nRadius, sal_Int32 nComponentWidth)
pBlurVerticalFn, bParallel);
}
void centerExtendBitmap(Bitmap& rBitmap, sal_Int32 nExtendSize, Color aColor)
{
const Size& rSize = rBitmap.GetSizePixel();
const Size aNewSize(rSize.Width() + nExtendSize * 2, rSize.Height() + nExtendSize * 2);
Bitmap aNewBitmap(aNewSize, rBitmap.GetBitCount());
{
Bitmap::ScopedReadAccess pReadAccess(rBitmap);
BitmapScopedWriteAccess pWriteAccess(aNewBitmap);
long nWidthBorder = nExtendSize + rSize.Width();
long nHeightBorder = nExtendSize + rSize.Height();
for (long y = 0; y < aNewSize.Height(); y++)
{
for (long x = 0; x < aNewSize.Width(); x++)
{
if (y < nExtendSize || y >= nHeightBorder || x < nExtendSize || x >= nWidthBorder)
{
pWriteAccess->SetPixel(y, x, aColor);
}
else
{
pWriteAccess->SetPixel(y, x,
pReadAccess->GetPixel(y - nExtendSize, x - nExtendSize));
}
}
}
}
rBitmap = aNewBitmap;
}
} // end anonymous namespace
/**
@@ -614,9 +581,8 @@ void centerExtendBitmap(Bitmap& rBitmap, sal_Int32 nExtendSize, Color aColor)
* (https://code.google.com/p/fog/)
*
*/
BitmapFilterStackBlur::BitmapFilterStackBlur(sal_Int32 nRadius, bool bExtend)
BitmapFilterStackBlur::BitmapFilterStackBlur(sal_Int32 nRadius)
: mnRadius(nRadius)
, mbExtend(bExtend)
{
}
@@ -648,22 +614,12 @@ Bitmap BitmapFilterStackBlur::filter(Bitmap const& rBitmap) const
? 4
: 3;
if (mbExtend)
{
centerExtendBitmap(bitmapCopy, mnRadius, COL_WHITE);
}
stackBlur24(bitmapCopy, mnRadius, nComponentWidth);
}
else if (nScanlineFormat == ScanlineFormat::N8BitPal)
{
int nComponentWidth = 1;
if (mbExtend)
{
centerExtendBitmap(bitmapCopy, mnRadius, COL_WHITE);
}
stackBlur8(bitmapCopy, mnRadius, nComponentWidth);
}