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/+/183448
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
Tested-by: Jenkins
This commit is contained in:
Marco Cecchetti
2025-03-28 14:36:16 +01:00
committed by Miklos Vajna
parent d9df9ebe85
commit 264ec12d91
9 changed files with 301 additions and 23 deletions
+1
View File
@@ -40,6 +40,7 @@ $(eval $(call gb_CppunitTest_use_libraries,sd_tiledrendering, \
$(eval $(call gb_CppunitTest_use_externals,sd_tiledrendering,\
boost_headers \
frozen \
libxml2 \
))
+114 -4
View File
@@ -2955,7 +2955,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);
@@ -2976,9 +2976,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()
@@ -3409,6 +3434,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
@@ -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
+66 -15
View File
@@ -150,6 +150,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)
@@ -298,6 +300,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)
@@ -456,9 +470,26 @@ public:
closeRenderPass();
}
}
// 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;
}
@@ -564,6 +595,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())
@@ -778,27 +823,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);
}
}
}
+101 -4
View File
@@ -718,6 +718,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())
@@ -729,12 +822,16 @@ void AnimationsExporter::exportAnimations()
exportNodeImpl(mxRootNode);
}
}
void AnimationsExporter::exportNode(const Reference<XAnimationNode>& xNode)
{
if (!isValidNode(xNode))
return;
auto 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;
auto aStruct = mrWriter.startStruct();
exportNodeImpl(xNode);
}
void AnimationsExporter::exportNodeImpl(const Reference<XAnimationNode>& xNode)