sd/CppunitTest_sd_tiledrendering.mk | 1 sd/qa/unit/tiledrendering/data/SlideRenderingTest_Animated_Group.odp |binary sd/qa/unit/tiledrendering/data/SlideRenderingTest_Animated_Groups.odp |binary sd/qa/unit/tiledrendering/data/SlideRenderingTest_Animated_MultiLevel_Group.odp |binary sd/qa/unit/tiledrendering/data/SlideRenderingTest_Animated_Shape_Inside_A_Group.odp |binary sd/qa/unit/tiledrendering/tiledrendering.cxx | 118 +++++++++- sd/source/ui/inc/SlideshowLayerRenderer.hxx | 19 + sd/source/ui/tools/SlideshowLayerRenderer.cxx | 83 +++++-- sd/source/ui/unoidl/unomodel.cxx | 105 ++++++++ 9 files changed, 302 insertions(+), 24 deletions(-)
New commits: commit 5c96c56704d591ffaf2f8eb36f446c056d152d67 Author: Marco Cecchetti <marco.cecche...@collabora.com> AuthorDate: Thu Nov 28 13:23:11 2024 +0100 Commit: Tomaž Vajngerl <qui...@gmail.com> CommitDate: Mon Dec 2 07:07:49 2024 +0100 lok: slideshow: support effects applied to a group of shapes What has been achieved: - when a group is animated a layer with all shapes belonging to the group is created and marked as an animated layer - any effect applied to a shape belonging to a group (animated or not) is discarded - any effect based on color animations applied to a group of shapes is discarded For the last 2 points, we mimic the same behavior that occurs on LibreOffice. Unit tests for several scenarios as been provided. Change-Id: Ie094ac2a6a85f08e0e873062b0a780fe322c83bd Reviewed-on: https://gerrit.libreoffice.org/c/core/+/177479 Reviewed-by: Tomaž Vajngerl <qui...@gmail.com> Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com> diff --git a/sd/CppunitTest_sd_tiledrendering.mk b/sd/CppunitTest_sd_tiledrendering.mk index 1fcfbf881292..809fc4d020a4 100644 --- a/sd/CppunitTest_sd_tiledrendering.mk +++ b/sd/CppunitTest_sd_tiledrendering.mk @@ -47,6 +47,7 @@ $(eval $(call gb_CppunitTest_set_include,sd_tiledrendering,\ -I$(SRCDIR)/sd/inc \ -I$(SRCDIR)/sd/source/ui/inc \ -I$(SRCDIR)/sd/qa/unit \ + -I$(WORKDIR)/UnpackedTarball/frozen/include \ $$(INCLUDE) \ )) diff --git a/sd/qa/unit/tiledrendering/data/SlideRenderingTest_Animated_Group.odp b/sd/qa/unit/tiledrendering/data/SlideRenderingTest_Animated_Group.odp new file mode 100644 index 000000000000..8be4426d207b Binary files /dev/null and b/sd/qa/unit/tiledrendering/data/SlideRenderingTest_Animated_Group.odp differ diff --git a/sd/qa/unit/tiledrendering/data/SlideRenderingTest_Animated_Groups.odp b/sd/qa/unit/tiledrendering/data/SlideRenderingTest_Animated_Groups.odp new file mode 100644 index 000000000000..73191f3901c2 Binary files /dev/null and b/sd/qa/unit/tiledrendering/data/SlideRenderingTest_Animated_Groups.odp differ diff --git a/sd/qa/unit/tiledrendering/data/SlideRenderingTest_Animated_MultiLevel_Group.odp b/sd/qa/unit/tiledrendering/data/SlideRenderingTest_Animated_MultiLevel_Group.odp new file mode 100644 index 000000000000..7d3555f57988 Binary files /dev/null and b/sd/qa/unit/tiledrendering/data/SlideRenderingTest_Animated_MultiLevel_Group.odp differ diff --git a/sd/qa/unit/tiledrendering/data/SlideRenderingTest_Animated_Shape_Inside_A_Group.odp b/sd/qa/unit/tiledrendering/data/SlideRenderingTest_Animated_Shape_Inside_A_Group.odp new file mode 100644 index 000000000000..3265dd11e013 Binary files /dev/null and b/sd/qa/unit/tiledrendering/data/SlideRenderingTest_Animated_Shape_Inside_A_Group.odp differ diff --git a/sd/qa/unit/tiledrendering/tiledrendering.cxx b/sd/qa/unit/tiledrendering/tiledrendering.cxx index f0c4601137b3..0683209e687f 100644 --- a/sd/qa/unit/tiledrendering/tiledrendering.cxx +++ b/sd/qa/unit/tiledrendering/tiledrendering.cxx @@ -3364,7 +3364,7 @@ public: } } - void checkPageLayer(int nIndex, const std::string& rGroup) + void checkPageLayer(int nIndex, const std::string& rGroup, bool bIsAnimated = false) { const std::string sMsg = rGroup + " Layer Index: " + std::to_string(nIndex); @@ -3384,9 +3384,34 @@ public: CPPUNIT_ASSERT_EQUAL_MESSAGE(sMsg, rGroup, aTree.get_child("group").get_value<std::string>()); CPPUNIT_ASSERT_EQUAL_MESSAGE(sMsg, nIndex, aTree.get_child("index").get_value<int>()); - CPPUNIT_ASSERT_EQUAL_MESSAGE(sMsg, std::string("bitmap"), - aTree.get_child("type").get_value<std::string>()); - CPPUNIT_ASSERT_EQUAL_MESSAGE(sMsg, true, has_child(aTree, "content")); + + if (!bIsAnimated) + { + CPPUNIT_ASSERT_EQUAL_MESSAGE(sMsg, std::string("bitmap"), + aTree.get_child("type").get_value<std::string>()); + CPPUNIT_ASSERT_EQUAL_MESSAGE(sMsg, true, has_child(aTree, "content")); + } + else + { + CPPUNIT_ASSERT_EQUAL_MESSAGE(sMsg, std::string("animated"), + aTree.get_child("type").get_value<std::string>()); + CPPUNIT_ASSERT_EQUAL_MESSAGE(sMsg, true, has_child(aTree, "content")); + + auto aContentChild = aTree.get_child("content"); + CPPUNIT_ASSERT_EQUAL_MESSAGE(sMsg, true, has_child(aContentChild, "hash")); + CPPUNIT_ASSERT_EQUAL_MESSAGE(sMsg, true, has_child(aContentChild, "initVisible")); + CPPUNIT_ASSERT_EQUAL_MESSAGE(sMsg, std::string("bitmap"), + aContentChild.get_child("type").get_value<std::string>()); + CPPUNIT_ASSERT_EQUAL_MESSAGE(sMsg, true, has_child(aContentChild, "content")); + + auto aContentChildChild = aContentChild.get_child("content"); + CPPUNIT_ASSERT_EQUAL_MESSAGE( + sMsg, std::string("%IMAGETYPE%"), + aContentChildChild.get_child("type").get_value<std::string>()); + CPPUNIT_ASSERT_EQUAL_MESSAGE( + sMsg, std::string("%IMAGECHECKSUM%"), + aContentChildChild.get_child("checksum").get_value<std::string>()); + } } void checkFinalEmptyLayer() @@ -3804,6 +3829,91 @@ CPPUNIT_TEST_FIXTURE(SdTiledRenderingTest, testSlideshowLayeredRendering_Skip_Ba pXImpressDocument->postSlideshowCleanup(); } +CPPUNIT_TEST_FIXTURE(SdTiledRenderingTest, testSlideshowLayeredRendering_Animated_Shape_Inside_A_Group) +{ + // 1 not animated groups made up by 2 shapes + // one of the 2 shapes is animated + + SdXImpressDocument* pXImpressDocument + = createDoc("SlideRenderingTest_Animated_Shape_Inside_A_Group.odp"); + pXImpressDocument->initializeForTiledRendering(uno::Sequence<beans::PropertyValue>()); + SlideRendererChecker aSlideRendererChecker(pXImpressDocument, 0, 2000, 2000, false, false); + aSlideRendererChecker.checkSlideSize(2000, 1125); + + // not animated group, no layer should be created for the animated shape + aSlideRendererChecker.checkPageLayer(0, "DrawPage", /*bIsAnimated=*/ false); + + aSlideRendererChecker.checkFinalEmptyLayer(); + + pXImpressDocument->postSlideshowCleanup(); +} + +CPPUNIT_TEST_FIXTURE(SdTiledRenderingTest, testSlideshowLayeredRendering_Animated_Group) +{ + // 1 animated groups made up by 2 not animated shapes + // a single not animated shape + + SdXImpressDocument* pXImpressDocument = createDoc("SlideRenderingTest_Animated_Group.odp"); + pXImpressDocument->initializeForTiledRendering(uno::Sequence<beans::PropertyValue>()); + SlideRendererChecker aSlideRendererChecker(pXImpressDocument, 0, 2000, 2000, false, false); + aSlideRendererChecker.checkSlideSize(2000, 1125); + + // animated group + aSlideRendererChecker.checkPageLayer(0, "DrawPage", /*bIsAnimated=*/ true); + + // not animated shape + aSlideRendererChecker.checkPageLayer(1, "DrawPage", /*bIsAnimated=*/ false); + + aSlideRendererChecker.checkFinalEmptyLayer(); + + pXImpressDocument->postSlideshowCleanup(); +} + +CPPUNIT_TEST_FIXTURE(SdTiledRenderingTest, testSlideshowLayeredRendering_Animated_Groups) +{ + // 2 animated groups made up by 2 shapes each + + SdXImpressDocument* pXImpressDocument = createDoc("SlideRenderingTest_Animated_Groups.odp"); + pXImpressDocument->initializeForTiledRendering(uno::Sequence<beans::PropertyValue>()); + SlideRendererChecker aSlideRendererChecker(pXImpressDocument, 0, 2000, 2000, false, false); + aSlideRendererChecker.checkSlideSize(2000, 1125); + + // 1st group + aSlideRendererChecker.checkPageLayer(0, "DrawPage", /*bIsAnimated=*/ true); + + // 2nd group + aSlideRendererChecker.checkPageLayer(1, "DrawPage", /*bIsAnimated=*/ true); + + aSlideRendererChecker.checkFinalEmptyLayer(); + + pXImpressDocument->postSlideshowCleanup(); +} + +CPPUNIT_TEST_FIXTURE(SdTiledRenderingTest, testSlideshowLayeredRendering_Animated_MultiLevel_Group) +{ + // 3 1st level groups made up by 2 shapes each + // the 1st group is not animated but one of its shape is + // the 2nd group is animated, none of its shapes is + // the 3rd group is animated with a color based effect + // 1st and 2nd group are grouped together and the 2nd level group is animated + + SdXImpressDocument* pXImpressDocument = createDoc("SlideRenderingTest_Animated_MultiLevel_Group.odp"); + pXImpressDocument->initializeForTiledRendering(uno::Sequence<beans::PropertyValue>()); + SlideRendererChecker aSlideRendererChecker(pXImpressDocument, 0, 2000, 2000, false, false); + aSlideRendererChecker.checkSlideSize(2000, 1125); + + // a single layer should be created for the highest level group, + // embedded animated groups or animated shapes should be ignored + aSlideRendererChecker.checkPageLayer(0, "DrawPage", /*bIsAnimated=*/ true); + + // a group with applied an effect based on color animations should not be animated + aSlideRendererChecker.checkPageLayer(1, "DrawPage", /*bIsAnimated=*/ false); + + aSlideRendererChecker.checkFinalEmptyLayer(); + + pXImpressDocument->postSlideshowCleanup(); +} + CPPUNIT_TEST_FIXTURE(SdTiledRenderingTest, testSlideshowLayeredRendering_Animations) { // Check rendering of animated objects - each in own layer diff --git a/sd/source/ui/inc/SlideshowLayerRenderer.hxx b/sd/source/ui/inc/SlideshowLayerRenderer.hxx index 93f4f5e36513..f6c3c6534564 100644 --- a/sd/source/ui/inc/SlideshowLayerRenderer.hxx +++ b/sd/source/ui/inc/SlideshowLayerRenderer.hxx @@ -24,6 +24,10 @@ #include <unordered_map> #include <unordered_set> +#include <frozen/bits/defines.h> +#include <frozen/bits/elsa_std.h> +#include <frozen/unordered_set.h> + class SdrPage; class SdrModel; class SdrObject; @@ -41,6 +45,21 @@ class ViewObjectContactRedirector; namespace sd { +constexpr auto constNonValidEffectsForGroupSet = frozen::make_unordered_set<std::string_view>({ + "ooo-emphasis-fill-color", + "ooo-emphasis-font-color", + "ooo-emphasis-line-color", + "ooo-emphasis-color-blend", + "ooo-emphasis-complementary-color", + "ooo-emphasis-complementary-color-2", + "ooo-emphasis-contrasting-color", + "ooo-emphasis-darken", + "ooo-emphasis-desaturate", + "ooo-emphasis-flash-bulb", + "ooo-emphasis-lighten", + "ooo-emphasis-grow-with-color", +}); + class RenderContext; enum class RenderStage diff --git a/sd/source/ui/tools/SlideshowLayerRenderer.cxx b/sd/source/ui/tools/SlideshowLayerRenderer.cxx index 66bd549f02bb..51c1209803eb 100644 --- a/sd/source/ui/tools/SlideshowLayerRenderer.cxx +++ b/sd/source/ui/tools/SlideshowLayerRenderer.cxx @@ -151,6 +151,8 @@ OUString getMasterTextFieldType(SdrObject* pObject) return aType; } +bool isGroup(SdrObject* pObject) { return pObject->getChildrenOfSdrObject() != nullptr; } + /// Sets visible for all kinds of polypolys in the container void changePolyPolys(drawinglayer::primitive2d::Primitive2DContainer& rContainer, bool bRenderObject) @@ -299,6 +301,18 @@ private: || (mrRenderState.mbDateTimeEnabled && svType == u"DateTime"); } + SdrObject* getAnimatedAncestor(SdrObject* pObject) const + { + SdrObject* pAncestor = pObject; + while ((pAncestor = pAncestor->getParentSdrObjectFromSdrObject())) + { + auto aIterator = mrRenderState.maAnimationRenderInfoList.find(pAncestor); + if (aIterator != mrRenderState.maAnimationRenderInfoList.end()) + return pAncestor; + } + return pAncestor; + } + public: AnalyzeRenderingRedirector(RenderState& rRenderState, bool bRenderMasterPage) : mrRenderState(rRenderState) @@ -457,9 +471,26 @@ public: closeRenderPass(); } } - // No specal handling is needed, just add the object to the current rendering pass + // check if object is part of an animated group + else if (SdrObject* pAncestor = getAnimatedAncestor(pObject)) + { + // a new animated group is started ? + if (mpCurrentRenderPass->mpObject && mpCurrentRenderPass->mpObject != pAncestor) + closeRenderPass(); + + // Add the animated object + mpCurrentRenderPass->maObjectsAndParagraphs.emplace(pObject, std::deque<sal_Int32>()); + mpCurrentRenderPass->meStage = eCurrentStage; + mpCurrentRenderPass->mbAnimation = true; + mpCurrentRenderPass->mpObject = pAncestor; + } + // No special handling is needed, just add the object to the current rendering pass else { + // an animated group is complete ? + if (mpCurrentRenderPass->mpObject) + closeRenderPass(); + mpCurrentRenderPass->maObjectsAndParagraphs.emplace(pObject, std::deque<sal_Int32>()); mpCurrentRenderPass->meStage = eCurrentStage; } @@ -565,6 +596,20 @@ void SlideshowLayerRenderer::resolveEffect(CustomAnimationEffectPtr const& rEffe if (!pObject) return; + // afaics, when a shape is part of a group any applied effect is ignored, + // so no layer should be created + if (pObject->getParentSdrObjectFromSdrObject()) + return; + + // some kind of effect, like the ones based on color animations, + // is ignored when applied to a group + if (isGroup(pObject)) + { + if (constNonValidEffectsForGroupSet.find(rEffect->getPresetId().toUtf8()) + != constNonValidEffectsForGroupSet.end()) + return; + } + AnimationRenderInfo aAnimationInfo; auto aIterator = maRenderState.maAnimationRenderInfoList.find(pObject); if (aIterator != maRenderState.maAnimationRenderInfoList.end()) @@ -779,27 +824,33 @@ void writeAnimated(::tools::JsonWriter& aJsonWriter, AnimationLayerInfo const& r writeContentNode(aJsonWriter); writeBoundingBox(aJsonWriter, pObject); - if (nParagraph < 0) + // a group of object has no such property + if (!isGroup(pObject)) { - drawing::FillStyle aFillStyle - = pObject->GetProperties().GetItem(XATTR_FILLSTYLE).GetValue(); - if (aFillStyle == drawing::FillStyle::FillStyle_SOLID) + if (nParagraph < 0) { - auto aFillColor = pObject->GetProperties().GetItem(XATTR_FILLCOLOR).GetColorValue(); - aJsonWriter.put("fillColor", "#" + aFillColor.AsRGBHEXString()); + drawing::FillStyle aFillStyle + = pObject->GetProperties().GetItem(XATTR_FILLSTYLE).GetValue(); + if (aFillStyle == drawing::FillStyle::FillStyle_SOLID) + { + auto aFillColor + = pObject->GetProperties().GetItem(XATTR_FILLCOLOR).GetColorValue(); + aJsonWriter.put("fillColor", "#" + aFillColor.AsRGBHEXString()); + } + drawing::LineStyle aLineStyle + = pObject->GetProperties().GetItem(XATTR_LINESTYLE).GetValue(); + if (aLineStyle == drawing::LineStyle::LineStyle_SOLID) + { + auto aLineColor + = pObject->GetProperties().GetItem(XATTR_LINECOLOR).GetColorValue(); + aJsonWriter.put("lineColor", "#" + aLineColor.AsRGBHEXString()); + } } - drawing::LineStyle aLineStyle - = pObject->GetProperties().GetItem(XATTR_LINESTYLE).GetValue(); - if (aLineStyle == drawing::LineStyle::LineStyle_SOLID) + else { - auto aLineColor = pObject->GetProperties().GetItem(XATTR_LINECOLOR).GetColorValue(); - aJsonWriter.put("lineColor", "#" + aLineColor.AsRGBHEXString()); + writeFontColor(aJsonWriter, pObject, nParagraph); } } - else - { - writeFontColor(aJsonWriter, pObject, nParagraph); - } } } diff --git a/sd/source/ui/unoidl/unomodel.cxx b/sd/source/ui/unoidl/unomodel.cxx index 9fccb11d82d5..07f9415a000a 100644 --- a/sd/source/ui/unoidl/unomodel.cxx +++ b/sd/source/ui/unoidl/unomodel.cxx @@ -722,6 +722,99 @@ bool isValidNode(const Reference<XAnimationNode>& xNode) return false; } +SdrObject* getObjectForShape(uno::Reference<drawing::XShape> const& xShape) +{ + if (!xShape.is()) + return nullptr; + SvxShape* pShape = comphelper::getFromUnoTunnel<SvxShape>(xShape); + if (pShape) + return pShape->GetSdrObject(); + return nullptr; +} + +SdrObject* getTargetObject(const uno::Any& aTargetAny) +{ + SdrObject* pObject = nullptr; + uno::Reference<drawing::XShape> xShape; + + if ((aTargetAny >>= xShape) && xShape.is()) + { + pObject = getObjectForShape(xShape); + } + else // if target is not a shape - could be paragraph target containing a shape + { + presentation::ParagraphTarget aParagraphTarget; + if ((aTargetAny >>= aParagraphTarget) && aParagraphTarget.Shape.is()) + { + pObject = getObjectForShape(aParagraphTarget.Shape); + } + } + + return pObject; +} + +bool isNodeTargetInShapeGroup(const Reference<XAnimationNode>& xNode) +{ + Reference<XAnimate> xAnimate(xNode, UNO_QUERY); + if (xAnimate.is()) + { + SdrObject* pObject = getTargetObject(xAnimate->getTarget()); + if (pObject) + return pObject->getParentSdrObjectFromSdrObject() != nullptr; + } + return false; +} + +bool isNodeTargetAGroup(const Reference<XAnimationNode>& xNode) +{ + Reference<XAnimate> xAnimate(xNode, UNO_QUERY); + if (xAnimate.is()) + { + SdrObject* pObject = getTargetObject(xAnimate->getTarget()); + if (pObject) + return pObject->getChildrenOfSdrObject() != nullptr; + } + return false; +} + +bool isEffectValidForTarget(const Reference<XAnimationNode>& xNode) +{ + const Sequence<NamedValue> aUserData(xNode->getUserData()); + for (const auto& rValue : aUserData) + { + if (!IsXMLToken(rValue.Name, XML_PRESET_ID)) + continue; + + OUString aPresetId; + if (rValue.Value >>= aPresetId) + { + if (constNonValidEffectsForGroupSet.find(aPresetId.toUtf8()) + != constNonValidEffectsForGroupSet.end()) + { + // it's in the list, so we need to check if the effect target is a group or not + Reference<XTimeContainer> xContainer(xNode, UNO_QUERY); + if (xContainer.is()) + { + Reference<XEnumerationAccess> xEnumerationAccess(xContainer, UNO_QUERY); + Reference<XEnumeration> xEnumeration = xEnumerationAccess->createEnumeration(); + + // target is the same for all children, check the first one + if (xEnumeration.is() && xEnumeration->hasMoreElements()) + { + Reference<XAnimationNode> xChildNode(xEnumeration->nextElement(), + UNO_QUERY); + if (isNodeTargetAGroup(xChildNode)) + return false; + } + } + } + } + // preset id found and checked, we can exit + break; + } + return true; +} + void AnimationsExporter::exportAnimations() { if (!mxDrawPage.is() || !mxPageProps.is() || !mxRootNode.is() || !hasEffects()) @@ -733,12 +826,16 @@ void AnimationsExporter::exportAnimations() exportNodeImpl(mxRootNode); } } + void AnimationsExporter::exportNode(const Reference<XAnimationNode>& xNode) { - if (!isValidNode(xNode)) - return; - ::tools::ScopedJsonWriterStruct aStruct = mrWriter.startStruct(); - exportNodeImpl(xNode); + // afaics, when a shape is part of a group any applied effect is ignored + // moreover, some kind of effect, like the ones based on color animations, + // is ignored when applied to a group + if (!isValidNode(xNode) || isNodeTargetInShapeGroup(xNode) || !isEffectValidForTarget(xNode)) + return; + ::tools::ScopedJsonWriterStruct aStruct = mrWriter.startStruct(); + exportNodeImpl(xNode); } void AnimationsExporter::exportNodeImpl(const Reference<XAnimationNode>& xNode)