/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This file is part of the LibreOffice project. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This file incorporates work covered by the following license notice: * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed * with this work for additional information regarding copyright * ownership. The ASF licenses this file to you under the Apache * License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.apache.org/licenses/LICENSE-2.0 . */ #include #include #include #include #include #include #include #include #include #include "GlowSoftEgdeShadowTools.hxx" #ifdef DBG_UTIL #include #include #endif #include #include using namespace com::sun::star; namespace drawinglayer::primitive2d { ShadowPrimitive2D::ShadowPrimitive2D(basegfx::B2DHomMatrix aShadowTransform, const basegfx::BColor& rShadowColor, double fShadowBlur, Primitive2DContainer&& aChildren) : BufferedDecompositionGroupPrimitive2D(std::move(aChildren)) , maShadowTransform(std::move(aShadowTransform)) , maShadowColor(rShadowColor) , mfShadowBlur(fShadowBlur) , mfLastDiscreteBlurRadius(0.0) , maLastClippedRange() { } bool ShadowPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const { if (BufferedDecompositionGroupPrimitive2D::operator==(rPrimitive)) { const ShadowPrimitive2D& rCompare = static_cast(rPrimitive); return (getShadowTransform() == rCompare.getShadowTransform() && getShadowColor() == rCompare.getShadowColor() && getShadowBlur() == rCompare.getShadowBlur()); } return false; } // Helper to get the to-be-shadowed geometry completely embedded to // a ModifiedColorPrimitive2D (change to ShadowColor) and TransformPrimitive2D // (direction/offset/transformation of shadow). Since this is used pretty // often, pack into a helper void ShadowPrimitive2D::getFullyEmbeddedShadowPrimitives(Primitive2DContainer& rContainer) const { if (getChildren().empty()) return; // create a modifiedColorPrimitive containing the shadow color and the content const basegfx::BColorModifierSharedPtr aBColorModifier = std::make_shared(getShadowColor()); const Primitive2DReference xRefA( new ModifiedColorPrimitive2D(Primitive2DContainer(getChildren()), aBColorModifier)); Primitive2DContainer aSequenceB{ xRefA }; // build transformed primitiveVector with shadow offset and add to target rContainer.visit(new TransformPrimitive2D(getShadowTransform(), std::move(aSequenceB))); } bool ShadowPrimitive2D::prepareValuesAndcheckValidity( basegfx::B2DRange& rBlurRange, basegfx::B2DRange& rClippedRange, basegfx::B2DVector& rDiscreteBlurSize, double& rfDiscreteBlurRadius, const geometry::ViewInformation2D& rViewInformation) const { // no BlurRadius defined, done if (getShadowBlur() <= 0.0) return false; // no geometry, done if (getChildren().empty()) return false; // no pixel target, done if (rViewInformation.getObjectToViewTransformation().isIdentity()) return false; // get fully embedded ShadowPrimitive Primitive2DContainer aEmbedded; getFullyEmbeddedShadowPrimitives(aEmbedded); // get geometry range that defines area that needs to be pixelated rBlurRange = aEmbedded.getB2DRange(rViewInformation); // no range of geometry, done if (rBlurRange.isEmpty()) return false; // extend range by BlurRadius in all directions rBlurRange.grow(getShadowBlur()); // initialize ClippedRange to full BlurRange -> all is visible rClippedRange = rBlurRange; // get Viewport and check if used. If empty, all is visible (see // ViewInformation2D definition in viewinformation2d.hxx) if (!rViewInformation.getViewport().isEmpty()) { // if used, extend by BlurRadius to ensure needed parts are included basegfx::B2DRange aVisibleArea(rViewInformation.getViewport()); aVisibleArea.grow(getShadowBlur()); // calculate ClippedRange rClippedRange.intersect(aVisibleArea); // if BlurRange is completely outside of VisibleArea, ClippedRange // will be empty and we are done if (rClippedRange.isEmpty()) return false; } // calculate discrete pixel size of BlurRange. If it's too small to visualize, we are done rDiscreteBlurSize = rViewInformation.getObjectToViewTransformation() * rBlurRange.getRange(); if (ceil(rDiscreteBlurSize.getX()) < 2.0 || ceil(rDiscreteBlurSize.getY()) < 2.0) return false; // calculate discrete pixel size of BlurRadius. If it's too small to visualize, we are done rfDiscreteBlurRadius = ceil( (rViewInformation.getObjectToViewTransformation() * basegfx::B2DVector(getShadowBlur(), 0)) .getLength()); if (rfDiscreteBlurRadius < 1.0) return false; return true; } void ShadowPrimitive2D::create2DDecomposition( Primitive2DContainer& rContainer, const geometry::ViewInformation2D& rViewInformation) const { if (getShadowBlur() <= 0.0) { // Normal (non-blurred) shadow is already completely // handled by get2DDecomposition and not buffered. It // does not need to be since it's a simple embedding // to a ModifiedColorPrimitive2D and TransformPrimitive2D return; } // from here on we process a blurred shadow basegfx::B2DRange aBlurRange; basegfx::B2DRange aClippedRange; basegfx::B2DVector aDiscreteBlurSize; double fDiscreteBlurRadius(0.0); // Check various validity details and calculate/prepare values. If false, we are done if (!prepareValuesAndcheckValidity(aBlurRange, aClippedRange, aDiscreteBlurSize, fDiscreteBlurRadius, rViewInformation)) return; // Create embedding transformation from object to top-left zero-aligned // target pixel geometry (discrete form of ClippedRange) // First, move to top-left of BlurRange const sal_uInt32 nDiscreteBlurWidth(ceil(aDiscreteBlurSize.getX())); const sal_uInt32 nDiscreteBlurHeight(ceil(aDiscreteBlurSize.getY())); basegfx::B2DHomMatrix aEmbedding(basegfx::utils::createTranslateB2DHomMatrix( -aClippedRange.getMinX(), -aClippedRange.getMinY())); // Second, scale to discrete bitmap size // Even when using the offset from ClippedRange, we need to use the // scaling from the full representation, thus from BlurRange aEmbedding.scale(nDiscreteBlurWidth / aBlurRange.getWidth(), nDiscreteBlurHeight / aBlurRange.getHeight()); // Get fully embedded ShadowPrimitives. This will also embed to // ModifiedColorPrimitive2D (what is not urgently needed) to create // the alpha channel, but a paint with all colors set to a single // one (like shadowColor here) is often less expensive due to possible // simplifications painting the primitives (e.g. gradient) Primitive2DContainer aEmbedded; getFullyEmbeddedShadowPrimitives(aEmbedded); // Embed content graphics to TransformPrimitive2D const primitive2d::Primitive2DReference xEmbedRef( new primitive2d::TransformPrimitive2D(aEmbedding, std::move(aEmbedded))); primitive2d::Primitive2DContainer xEmbedSeq{ xEmbedRef }; // Create BitmapEx using drawinglayer tooling, including a MaximumQuadraticPixel // limitation to be safe and not go runtime/memory havoc. Use a pretty small // limit due to this is Blurred Shadow functionality and will look good with bitmap // scaling anyways. The value of 250.000 square pixels below maybe adapted as needed. const basegfx::B2DVector aDiscreteClippedSize(rViewInformation.getObjectToViewTransformation() * aClippedRange.getRange()); const sal_uInt32 nDiscreteClippedWidth(ceil(aDiscreteClippedSize.getX())); const sal_uInt32 nDiscreteClippedHeight(ceil(aDiscreteClippedSize.getY())); const geometry::ViewInformation2D aViewInformation2D; const sal_uInt32 nMaximumQuadraticPixels(250000); // I have now added a helper that just creates the mask without having // to render the content, use it, it's faster const AlphaMask aAlpha(::drawinglayer::createAlphaMask( std::move(xEmbedSeq), aViewInformation2D, nDiscreteClippedWidth, nDiscreteClippedHeight, nMaximumQuadraticPixels)); // if we have no shadow, we are done if (aAlpha.IsEmpty()) return; const Size& rBitmapExSizePixel(aAlpha.GetSizePixel()); if (!(rBitmapExSizePixel.Width() > 0 && rBitmapExSizePixel.Height() > 0)) return; // We may have to take a corrective scaling into account when the // MaximumQuadraticPixel limit was used/triggered double fScale(1.0); if (static_cast(rBitmapExSizePixel.Width()) != nDiscreteClippedWidth || static_cast(rBitmapExSizePixel.Height()) != nDiscreteClippedHeight) { // scale in X and Y should be the same (see fReduceFactor in createAlphaMask), // so adapt numerically to a single scale value, they are integer rounded values const double fScaleX(static_cast(rBitmapExSizePixel.Width()) / static_cast(nDiscreteClippedWidth)); const double fScaleY(static_cast(rBitmapExSizePixel.Height()) / static_cast(nDiscreteClippedHeight)); fScale = (fScaleX + fScaleY) * 0.5; } // Use the Alpha as base to blur and apply the effect const AlphaMask mask(drawinglayer::primitive2d::ProcessAndBlurAlphaMask( aAlpha, 0, fDiscreteBlurRadius * fScale, 0, false)); // The end result is the bitmap filled with blur color and blurred 8-bit alpha mask Bitmap bmp(aAlpha.GetSizePixel(), vcl::PixelFormat::N24_BPP); bmp.Erase(Color(getShadowColor())); BitmapEx result(bmp, mask); #ifdef DBG_UTIL static bool bDoSaveForVisualControl(false); // loplugin:constvars:ignore if (bDoSaveForVisualControl) { // VCL_DUMP_BMP_PATH should be like C:/path/ or ~/path/ static const OUString sDumpPath( OUString::createFromAscii(std::getenv("VCL_DUMP_BMP_PATH"))); if (!sDumpPath.isEmpty()) { SvFileStream aNew(sDumpPath + "test_shadowblur.png", StreamMode::WRITE | StreamMode::TRUNC); vcl::PngImageWriter aPNGWriter(aNew); aPNGWriter.write(result); } } #endif // Independent from discrete sizes of blur alpha creation, always // map and project blur result to geometry range extended by blur // radius, but to the eventually clipped instance (ClippedRange) const primitive2d::Primitive2DReference xEmbedRefBitmap( new BitmapPrimitive2D(result, basegfx::utils::createScaleTranslateB2DHomMatrix( aClippedRange.getWidth(), aClippedRange.getHeight(), aClippedRange.getMinX(), aClippedRange.getMinY()))); rContainer = primitive2d::Primitive2DContainer{ xEmbedRefBitmap }; } void ShadowPrimitive2D::get2DDecomposition( Primitive2DDecompositionVisitor& rVisitor, const geometry::ViewInformation2D& rViewInformation) const { if (getShadowBlur() <= 0.0) { // normal (non-blurred) shadow if (getChildren().empty()) return; // get fully embedded ShadowPrimitives Primitive2DContainer aEmbedded; getFullyEmbeddedShadowPrimitives(aEmbedded); rVisitor.visit(aEmbedded); return; } // here we have a blurred shadow, check conditions of last // buffered decompose and decide re-use or re-create by using // setBuffered2DDecomposition to reset local buffered version basegfx::B2DRange aBlurRange; basegfx::B2DRange aClippedRange; basegfx::B2DVector aDiscreteBlurSize; double fDiscreteBlurRadius(0.0); // Check various validity details and calculate/prepare values. If false, we are done if (!prepareValuesAndcheckValidity(aBlurRange, aClippedRange, aDiscreteBlurSize, fDiscreteBlurRadius, rViewInformation)) return; if (!getBuffered2DDecomposition().empty()) { // First check is to detect if the last created decompose is capable // to represent the now requested visualization (see similar // implementation at GlowPrimitive2D). if (!maLastClippedRange.isEmpty() && !maLastClippedRange.isInside(aClippedRange)) { basegfx::B2DRange aLastClippedRangeAndHairline(maLastClippedRange); if (!rViewInformation.getObjectToViewTransformation().isIdentity()) { // Grow by view-dependent size of 1/2 pixel const double fHalfPixel((rViewInformation.getInverseObjectToViewTransformation() * basegfx::B2DVector(0.5, 0)) .getLength()); aLastClippedRangeAndHairline.grow(fHalfPixel); } if (!aLastClippedRangeAndHairline.isInside(aClippedRange)) { // Conditions of last local decomposition have changed, delete const_cast(this)->setBuffered2DDecomposition( Primitive2DContainer()); } } } if (!getBuffered2DDecomposition().empty()) { // Second check is to react on changes of the DiscreteSoftRadius when // zooming in/out (see similar implementation at ShadowPrimitive2D). bool bFree(mfLastDiscreteBlurRadius <= 0.0 || fDiscreteBlurRadius <= 0.0); if (!bFree) { const double fDiff(fabs(mfLastDiscreteBlurRadius - fDiscreteBlurRadius)); const double fLen(fabs(mfLastDiscreteBlurRadius) + fabs(fDiscreteBlurRadius)); const double fRelativeChange(fDiff / fLen); // Use lower fixed values here to change more often, higher to change less often. // Value is in the range of ]0.0 .. 1.0] bFree = fRelativeChange >= 0.15; } if (bFree) { // Conditions of last local decomposition have changed, delete const_cast(this)->setBuffered2DDecomposition( Primitive2DContainer()); } } if (getBuffered2DDecomposition().empty()) { // refresh last used DiscreteBlurRadius and ClippedRange to new remembered values const_cast(this)->mfLastDiscreteBlurRadius = fDiscreteBlurRadius; const_cast(this)->maLastClippedRange = aClippedRange; } // call parent, that will check for empty, call create2DDecomposition and // set as decomposition BufferedDecompositionGroupPrimitive2D::get2DDecomposition(rVisitor, rViewInformation); } basegfx::B2DRange ShadowPrimitive2D::getB2DRange(const geometry::ViewInformation2D& rViewInformation) const { // Hint: Do *not* use GroupPrimitive2D::getB2DRange, that will (unnecessarily) // use the decompose - what works, but is not needed here. // We know the to-be-visualized geometry and the radius it needs to be extended, // so simply calculate the exact needed range. basegfx::B2DRange aRetval(getChildren().getB2DRange(rViewInformation)); if (getShadowBlur() > 0.0) { // blurred shadow, that extends the geometry aRetval.grow(getShadowBlur()); } aRetval.transform(getShadowTransform()); return aRetval; } // provide unique ID sal_uInt32 ShadowPrimitive2D::getPrimitive2DID() const { return PRIMITIVE2D_ID_SHADOWPRIMITIVE2D; } } // end of namespace /* vim:set shiftwidth=4 softtabstop=4 expandtab: */