[Libreoffice-commits] core.git: include/oox oox/qa oox/source
Regina Henschel (via logerrit)
logerrit at kemper.freedesktop.org
Tue May 11 15:23:00 UTC 2021
include/oox/drawingml/shape.hxx | 4
oox/qa/unit/data/tdf141463_GroupTransform.pptx |binary
oox/qa/unit/shape.cxx | 55 +++++
oox/source/drawingml/shape.cxx | 235 +++++++++++++++----------
4 files changed, 205 insertions(+), 89 deletions(-)
New commits:
commit 36499d8bf6cd5c6af7b2ceb6071baf5c7421bd0a
Author: Regina Henschel <rb.henschel at t-online.de>
AuthorDate: Mon May 10 00:47:13 2021 +0200
Commit: Miklos Vajna <vmiklos at collabora.com>
CommitDate: Tue May 11 17:19:34 2021 +0200
tdf#141463 avoid skew in shape group in ooxml import ..
and implement special resize handling for rotation angles larger 45deg.
This solves tdf#93952 and tdf#141953 too.
Change-Id: I798f6d2cea29c4a5285f530e9cf7bb10e7f6c41d
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/115296
Tested-by: Jenkins
Reviewed-by: Miklos Vajna <vmiklos at collabora.com>
diff --git a/include/oox/drawingml/shape.hxx b/include/oox/drawingml/shape.hxx
index eac27b68c7a5..53401d18a1c1 100644
--- a/include/oox/drawingml/shape.hxx
+++ b/include/oox/drawingml/shape.hxx
@@ -188,7 +188,7 @@ public:
const basegfx::B2DHomMatrix& aTransformation,
FillProperties& rShapeOrParentShapeFillProps,
ShapeIdMap* pShapeMap = nullptr,
- bool bInGroup = false);
+ oox::drawingml::ShapePtr pParentGroupShape = nullptr);
const css::uno::Reference< css::drawing::XShape > &
getXShape() const { return mxShape; }
@@ -265,7 +265,7 @@ protected:
bool bDoNotInsertEmptyTextBody,
basegfx::B2DHomMatrix& aTransformation,
FillProperties& rShapeOrParentShapeFillProps,
- bool bInGroup = false
+ oox::drawingml::ShapePtr pParentGroupShape = nullptr
);
void addChildren(
diff --git a/oox/qa/unit/data/tdf141463_GroupTransform.pptx b/oox/qa/unit/data/tdf141463_GroupTransform.pptx
new file mode 100644
index 000000000000..36c506262333
Binary files /dev/null and b/oox/qa/unit/data/tdf141463_GroupTransform.pptx differ
diff --git a/oox/qa/unit/shape.cxx b/oox/qa/unit/shape.cxx
index 6195e4955a39..a6949ff4fec3 100644
--- a/oox/qa/unit/shape.cxx
+++ b/oox/qa/unit/shape.cxx
@@ -14,6 +14,8 @@
#include <test/bootstrapfixture.hxx>
#include <unotest/macros_test.hxx>
+#include <com/sun/star/awt/Point.hpp>
+#include <com/sun/star/awt/Size.hpp>
#include <com/sun/star/drawing/XDrawPagesSupplier.hpp>
#include <com/sun/star/frame/Desktop.hpp>
#include <com/sun/star/beans/XPropertySet.hpp>
@@ -22,6 +24,24 @@
using namespace ::com::sun::star;
+namespace
+{
+/// Gets one child of xShape, which one is specified by nIndex.
+uno::Reference<drawing::XShape> getChildShape(const uno::Reference<drawing::XShape>& xShape,
+ sal_Int32 nIndex)
+{
+ uno::Reference<container::XIndexAccess> xGroup(xShape, uno::UNO_QUERY);
+ CPPUNIT_ASSERT(xGroup.is());
+
+ CPPUNIT_ASSERT(xGroup->getCount() > nIndex);
+
+ uno::Reference<drawing::XShape> xRet(xGroup->getByIndex(nIndex), uno::UNO_QUERY);
+ CPPUNIT_ASSERT(xRet.is());
+
+ return xRet;
+}
+}
+
constexpr OUStringLiteral DATA_DIRECTORY = u"/oox/qa/unit/data/";
/// oox shape tests.
@@ -58,6 +78,41 @@ void OoxShapeTest::load(std::u16string_view rFileName)
mxComponent = loadFromDesktop(aURL);
}
+CPPUNIT_TEST_FIXTURE(OoxShapeTest, testGroupTransform)
+{
+ load(u"tdf141463_GroupTransform.pptx");
+
+ uno::Reference<drawing::XDrawPagesSupplier> xDrawPagesSupplier(getComponent(), uno::UNO_QUERY);
+ uno::Reference<drawing::XDrawPage> xDrawPage(xDrawPagesSupplier->getDrawPages()->getByIndex(0),
+ uno::UNO_QUERY);
+ uno::Reference<drawing::XShape> xGroup(xDrawPage->getByIndex(0), uno::UNO_QUERY);
+ uno::Reference<drawing::XShape> xShape(getChildShape(xGroup, 0), uno::UNO_QUERY);
+ uno::Reference<beans::XPropertySet> xPropSet(xShape, uno::UNO_QUERY);
+ // Without the accompanying fix in place, this test would have failed in several properties.
+
+ sal_Int32 nAngle;
+ xPropSet->getPropertyValue("ShearAngle") >>= nAngle;
+ // Failed with - Expected: 0
+ // - Actual : -810
+ // i.e. the shape was sheared although shearing does not exist in oox
+ CPPUNIT_ASSERT_EQUAL(sal_Int32(0), nAngle);
+
+ xPropSet->getPropertyValue("RotateAngle") >>= nAngle;
+ // Failed with - Expected: 26000 (is in 1/100deg)
+ // - Actual : 26481 (is in 1/100deg)
+ // 100deg in PowerPoint UI = 360deg - 100deg in LO.
+ CPPUNIT_ASSERT_EQUAL(sal_Int32(26000), nAngle);
+
+ sal_Int32 nActual = xShape->getSize().Width;
+ // The group has ext.cy=2880000 and chExt.cy=4320000 resulting in Y-scale=2/3.
+ // The child has ext 2880000 x 1440000. Because of rotation angle 80deg, the Y-scale has to be
+ // applied to the width, resulting in 2880000 * 2/3 = 1920000EMU = 5333Hmm
+ // ToDo: Expected value currently 1 off.
+ // Failed with - Expected: 5332
+ // - Actual : 5432
+ CPPUNIT_ASSERT_EQUAL(sal_Int32(5332), nActual);
+}
+
CPPUNIT_TEST_FIXTURE(OoxShapeTest, testMultipleGroupShapes)
{
load(u"multiple-group-shapes.docx");
diff --git a/oox/source/drawingml/shape.cxx b/oox/source/drawingml/shape.cxx
index 49f82a6fd137..ed26e2313941 100644
--- a/oox/source/drawingml/shape.cxx
+++ b/oox/source/drawingml/shape.cxx
@@ -85,6 +85,7 @@
#include <basegfx/point/b2dpoint.hxx>
#include <basegfx/polygon/b2dpolygon.hxx>
#include <basegfx/matrix/b2dhommatrix.hxx>
+#include <basegfx/matrix/b2dhommatrixtools.hxx>
#include <com/sun/star/document/XActionLockable.hpp>
#include <com/sun/star/chart2/data/XDataReceiver.hpp>
#include <svx/svdtrans.hxx>
@@ -270,7 +271,7 @@ void Shape::addShape(
const basegfx::B2DHomMatrix& aTransformation,
FillProperties& rShapeOrParentShapeFillProps,
ShapeIdMap* pShapeMap,
- bool bInGroup )
+ oox::drawingml::ShapePtr pParentGroupShape)
{
SAL_INFO("oox.drawingml", "Shape::addShape: id='" << msId << "'");
@@ -280,7 +281,7 @@ void Shape::addShape(
if( !sServiceName.isEmpty() )
{
basegfx::B2DHomMatrix aMatrix( aTransformation );
- Reference< XShape > xShape( createAndInsert( rFilterBase, sServiceName, pTheme, rxShapes, false, false, aMatrix, rShapeOrParentShapeFillProps, bInGroup ) );
+ Reference< XShape > xShape( createAndInsert( rFilterBase, sServiceName, pTheme, rxShapes, false, false, aMatrix, rShapeOrParentShapeFillProps, pParentGroupShape) );
if( pShapeMap && !msId.isEmpty() )
{
@@ -393,41 +394,10 @@ void Shape::addChildren(
ShapeIdMap* pShapeMap,
const basegfx::B2DHomMatrix& aTransformation )
{
- basegfx::B2DHomMatrix aChildTransformation;
-
- aChildTransformation.translate(-maChPosition.X, -maChPosition.Y);
- aChildTransformation.scale(1/(maChSize.Width ? maChSize.Width : 1.0), 1/(maChSize.Height ? maChSize.Height : 1.0));
-
- // Child position and size is typically non-zero, but it's allowed to have
- // it like that, and in that case Word ignores the parent transformation
- // (excluding translate component).
- if (!mbWps || maChPosition.X || maChPosition.Y || maChSize.Width || maChSize.Height)
- {
- aChildTransformation *= aTransformation;
- }
- else
- {
- basegfx::B2DVector aScale, aTranslate;
- double fRotate, fShearX;
- aTransformation.decompose(aScale, aTranslate, fRotate, fShearX);
- aChildTransformation.translate(aTranslate.getX(), aTranslate.getY());
- }
-
- SAL_INFO("oox.drawingml", "Shape::addChildren: parent matrix:\n"
- << aChildTransformation.get(0, 0) << " "
- << aChildTransformation.get(0, 1) << " "
- << aChildTransformation.get(0, 2) << "\n"
- << aChildTransformation.get(1, 0) << " "
- << aChildTransformation.get(1, 1) << " "
- << aChildTransformation.get(1, 2) << "\n"
- << aChildTransformation.get(2, 0) << " "
- << aChildTransformation.get(2, 1) << " "
- << aChildTransformation.get(2, 2));
-
for (auto const& child : rMaster.maChildren)
{
child->setMasterTextListStyle( mpMasterTextListStyle );
- child->addShape( rFilterBase, pTheme, rxShapes, aChildTransformation, getFillProperties(), pShapeMap, true );
+ child->addShape( rFilterBase, pTheme, rxShapes, aTransformation, getFillProperties(), pShapeMap, rMaster.shared_from_this());
}
}
@@ -652,6 +622,53 @@ static void lcl_createPresetShape(const uno::Reference<drawing::XShape>& xShape,
uno::makeAny(comphelper::containerToSequence(aGeomPropVec)));
}
+// Some helper methods for createAndInsert
+namespace
+{
+// mirrors aTransformation at its center axis
+// only valid if neither rotation or shear included
+void lcl_mirrorAtCenter(basegfx::B2DHomMatrix& aTransformation, bool bFlipH, bool bFlipV)
+{
+ if (!bFlipH && !bFlipV)
+ return;
+ basegfx::B2DPoint aCenter(0.5, 0.5);
+ aCenter *= aTransformation;
+ aTransformation.translate(-aCenter);
+ aTransformation.scale(bFlipH ? -1.0 : 1.0, bFlipV ? -1.0 : 1.0);
+ aTransformation.translate(aCenter);
+ return;
+}
+
+// only valid if neither rotation or shear included
+void lcl_doSpecialMSOWidthHeightToggle(basegfx::B2DHomMatrix& aTransformation)
+{
+ // The values are directly set at the matrix without any matrix multiplication.
+ // That way it is valid for lines too. Those have zero width or height.
+ const double fSx(aTransformation.get(0, 0));
+ const double fSy(aTransformation.get(1, 1));
+ const double fTx(aTransformation.get(0, 2));
+ const double fTy(aTransformation.get(1, 2));
+ aTransformation.set(0, 0, fSy);
+ aTransformation.set(1, 1, fSx);
+ aTransformation.set(0, 2, fTx + 0.5 * (fSx - fSy));
+ aTransformation.set(1, 2, fTy + 0.5 * (fSy - fSx));
+ return;
+}
+
+void lcl_RotateAtCenter(basegfx::B2DHomMatrix& aTransformation, const sal_Int32& rMSORotationAngle)
+{
+ if (rMSORotationAngle == 0)
+ return;
+ double fRad = basegfx::deg2rad(rMSORotationAngle / 60000.0);
+ basegfx::B2DPoint aCenter(0.5, 0.5);
+ aCenter *= aTransformation;
+ aTransformation.translate(-aCenter);
+ aTransformation.rotate(fRad);
+ aTransformation.translate(aCenter);
+ return;
+}
+}
+
Reference< XShape > const & Shape::createAndInsert(
::oox::core::XmlFilterBase& rFilterBase,
const OUString& rServiceName,
@@ -661,7 +678,7 @@ Reference< XShape > const & Shape::createAndInsert(
bool bDoNotInsertEmptyTextBody,
basegfx::B2DHomMatrix& aParentTransformation,
FillProperties& rShapeOrParentShapeFillProps,
- bool bInGroup )
+ oox::drawingml::ShapePtr pParentGroupShape)
{
bool bIsEmbMedia = false;
SAL_INFO("oox.drawingml", "Shape::createAndInsert: id='" << msId << "' service='" << rServiceName << "'");
@@ -731,6 +748,7 @@ Reference< XShape > const & Shape::createAndInsert(
(mpCustomShapePropertiesPtr->getShapePresetType() >= 0 || mpCustomShapePropertiesPtr->getPath2DList().size() > 0) &&
mpCustomShapePropertiesPtr->getShapePresetType() != XML_Rect &&
mpCustomShapePropertiesPtr->getShapePresetType() != XML_rect);
+ // ToDo: Why is ConnectorShape here treated as custom shape, but below with start and end point?
bool bIsCustomShape = ( aServiceName == "com.sun.star.drawing.CustomShape" ||
aServiceName == "com.sun.star.drawing.ConnectorShape" ||
bIsCroppedGraphic);
@@ -745,84 +763,129 @@ Reference< XShape > const & Shape::createAndInsert(
mbFlipH ||
mbFlipV );
- basegfx::B2DHomMatrix aTransformation;
+ basegfx::B2DHomMatrix aTransformation; // will be cumulative transformation of this object
+ // Special for SmartArt import. Rotate diagram's shape around object's center before sizing.
if (bUseRotationTransform && mnDiagramRotation != 0)
{
- // rotate diagram's shape around object's center before sizing
aTransformation.translate(-0.5, -0.5);
aTransformation.rotate(basegfx::deg2rad(mnDiagramRotation / 60000.0));
aTransformation.translate(0.5, 0.5);
}
- if( maSize.Width != 1 || maSize.Height != 1)
+ // Build object matrix from shape size and position; corresponds to MSO ext and off
+ // Only LineShape and ConnectorShape may have zero width or height.
+ if (aServiceName == "com.sun.star.drawing.LineShape"
+ || aServiceName == "com.sun.star.drawing.ConnectorShape")
+ aTransformation.scale(maSize.Width, maSize.Height);
+ else
{
- // take care there are no zeros used by error
- aTransformation.scale(
- maSize.Width ? maSize.Width : 1.0,
- maSize.Height ? maSize.Height : 1.0 );
+ aTransformation.scale(maSize.Width ? maSize.Width : 1.0,
+ maSize.Height ? maSize.Height : 1.0);
}
- bool bNoTranslation = !aParentTransformation.isIdentity();
- if( mbFlipH || mbFlipV || mnRotation != 0 || bNoTranslation )
+ // Evaluate object flip. Other shapes than custom shapes have no attribute for flip but use
+ // negative scale. Flip in MSO is at object center.
+ if (!bIsCustomShape && (mbFlipH || mbFlipV))
+ lcl_mirrorAtCenter(aTransformation, mbFlipH, mbFlipV);
+
+ // Evaluate parent flip.
+ // A CustomShape has mirror not as negative scale, but as attributes.
+ basegfx::B2DVector aParentScale(1.0, 1.0);
+ basegfx::B2DVector aParentTranslate(0.0, 0.0);
+ double fParentRotate(0.0);
+ double fParentShearX(0.0);
+ if (pParentGroupShape)
{
- // calculate object's center
- basegfx::B2DPoint aCenter(0.5, 0.5);
- aCenter *= aTransformation;
-
- // center object at origin
- aTransformation.translate( -aCenter.getX(), -aCenter.getY() );
-
- if( !bIsCustomShape && ( mbFlipH || mbFlipV ) )
+ aParentTransformation.decompose(aParentScale, aParentTranslate, fParentRotate, fParentShearX);
+ if (bIsCustomShape)
{
- // mirror around object's center
- aTransformation.scale( mbFlipH ? -1.0 : 1.0, mbFlipV ? -1.0 : 1.0 );
+ lcl_mirrorAtCenter(aTransformation, aParentScale.getX() < 0, aParentScale.getY() < 0);
+ if(aParentScale.getX() < 0)
+ mbFlipH = !mbFlipH;
+ if(aParentScale.getY() < 0)
+ mbFlipV = !mbFlipV;
}
-
- if( bUseRotationTransform )
- {
- // OOXML flips shapes before rotating them.
- if( bIsCustomShape )
- {
- basegfx::B2DVector aScale, aTranslate;
- double fRotate, fShearX;
- aParentTransformation.decompose(aScale, aTranslate, fRotate, fShearX);
- // A negative scale means that the shape needs to be flipped
- if(aScale.getX() < 0)
- {
- mbFlipH = !mbFlipH;
- aTransformation.scale(-1, 1);
- }
- if(aScale.getY() < 0)
- {
- mbFlipV = !mbFlipV;
- aTransformation.scale(1, -1);
- }
- }
- // rotate around object's center
- aTransformation.rotate(basegfx::deg2rad(static_cast<double>(mnRotation) / 60000.0));
- }
-
- // move object back from center
- aTransformation.translate( aCenter.getX(), aCenter.getY() );
}
- if( maPosition.X != 0 || maPosition.Y != 0)
+ if (maPosition.X != 0 || maPosition.Y != 0)
{
// if global position is used, add it to transformation
- if (mbWps && !bInGroup)
+ if (mbWps && pParentGroupShape == nullptr)
aTransformation.translate(
o3tl::convert(maPosition.X, o3tl::Length::mm100, o3tl::Length::emu),
o3tl::convert(maPosition.Y, o3tl::Length::mm100, o3tl::Length::emu));
else
- aTransformation.translate( maPosition.X, maPosition.Y );
+ aTransformation.translate(maPosition.X, maPosition.Y);
}
- aTransformation = aParentTransformation*aTransformation;
+ // Apply further parent transformations. First scale object then rotate. Other way round would
+ // introduce shearing.
+
+ // The attributes chExt and chOff of the group in oox file contain the values on which the size
+ // and position of the child is based on. If they differ from the actual size of the group as
+ // given in its ext and off attributes, the child has to be transformed according the new values.
+ if (pParentGroupShape)
+ {
+ // ToDo: A diagram in a group might need special handling because it cannot flip and only
+ // resize uniformly. But currently it is imported with zero size, see tdf#139575. That needs
+ // to be fixed beforehand.
+
+ // Scaling is done from left/top edges of the group. So these need to become coordinate axes.
+ aTransformation.translate(-pParentGroupShape->maChPosition.X,
+ -pParentGroupShape->maChPosition.Y);
+
+ // oox allows zero or missing attribute chExt. In that case the scaling factor is 1.
+ // Transform2DContext::onCreateContext has set maChSize to maSize for groups in oox file in
+ // such cases. For own made groups (e.g. diagrams) that is missing.
+ // The factors cumulate on the way through the parent groups, so we do not use maSize of the
+ // direct parent group but the cumulated value from aParentScale.
+ double fFactorX = 1.0;
+ double fFactorY = 1.0;
+ if (pParentGroupShape->maChSize.Width != 0)
+ fFactorX = aParentScale.getX() / pParentGroupShape->maChSize.Width;
+ if (pParentGroupShape->maChSize.Height != 0)
+ fFactorY = aParentScale.getY() / pParentGroupShape->maChSize.Height;
+ if (fFactorX != 1 || fFactorY != 1)
+ {
+ // It depends on the object rotation angle whether scaling is applied to switched
+ // width and height. MSO acts strange in that case (as of May 2021).
+ const sal_Int32 nDeg(mnRotation / 60000);
+ const bool bNeedsMSOWidhtHeightToggle
+ = (nDeg >= 45 && nDeg < 135) || (nDeg >= 225 && nDeg < 315);
+ if (bNeedsMSOWidhtHeightToggle)
+ lcl_doSpecialMSOWidthHeightToggle(aTransformation);
+
+ aTransformation.scale(fFactorX, fFactorY);
+
+ if (bNeedsMSOWidhtHeightToggle)
+ {
+ lcl_doSpecialMSOWidthHeightToggle(aTransformation);
+ // In case of flip the special case needs an additional 180deg rotation.
+ if ((aParentScale.getX() < 0) != (aParentScale.getY() < 0))
+ lcl_RotateAtCenter(aTransformation, 10800000);
+ }
+ }
+ }
+
+ // Apply object rotation at current object center
+ // The flip contained in aParentScale will affect orientation of object rotation angle.
+ sal_Int16 nOrientation = ((aParentScale.getX() < 0) != (aParentScale.getY() < 0)) ? -1 : 1;
+ // ToDo: Not sure about the restrictions given by bUseRotationTransform.
+ if (bUseRotationTransform && mnRotation != 0)
+ lcl_RotateAtCenter(aTransformation, nOrientation * mnRotation);
+
+ if (fParentRotate != 0.0)
+ aTransformation.rotate(fParentRotate);
+ if (!aParentTranslate.equalZero())
+ aTransformation.translate(aParentTranslate);
+
aParentTransformation = aTransformation;
+
constexpr double fEmuToMm100 = o3tl::convert(1.0, o3tl::Length::emu, o3tl::Length::mm100);
aTransformation.scale(fEmuToMm100, fEmuToMm100);
+ // OOXML flips shapes before rotating them, so the rotation needs to be inverted
if( bIsCustomShape && mbFlipH != mbFlipV )
{
basegfx::B2DVector aScale, aTranslate;
@@ -831,11 +894,9 @@ Reference< XShape > const & Shape::createAndInsert(
if(fRotate != 0)
{
- // calculate object's center
basegfx::B2DPoint aCenter(0.5, 0.5);
aCenter *= aTransformation;
aTransformation.translate( -aCenter.getX(), -aCenter.getY() );
- // OOXML flips shapes before rotating them, so the rotation needs to be inverted
aTransformation.rotate( fRotate * -2.0 );
aTransformation.translate( aCenter.getX(), aCenter.getY() );
}
More information about the Libreoffice-commits
mailing list