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:
committed by
Miklos Vajna
parent
d9df9ebe85
commit
264ec12d91
@@ -40,6 +40,7 @@ $(eval $(call gb_CppunitTest_use_libraries,sd_tiledrendering, \
|
||||
|
||||
$(eval $(call gb_CppunitTest_use_externals,sd_tiledrendering,\
|
||||
boost_headers \
|
||||
frozen \
|
||||
libxml2 \
|
||||
))
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user