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 <[email protected]>
AuthorDate: Thu Nov 28 13:23:11 2024 +0100
Commit: Tomaž Vajngerl <[email protected]>
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 <[email protected]>
Tested-by: Jenkins CollaboraOffice <[email protected]>
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)