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@collabora.com>
Signed-off-by: Xisco Fauli <xiscofauli@libreoffice.org>
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/115715
diff --git a/include/oox/drawingml/shape.hxx b/include/oox/drawingml/shape.hxx
index fea94105..6484ae8 100644
--- a/include/oox/drawingml/shape.hxx
+++ b/include/oox/drawingml/shape.hxx
@@ -187,7 +187,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; }
@@ -264,7 +264,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 0000000..36c5062
--- /dev/null
+++ b/oox/qa/unit/data/tdf141463_GroupTransform.pptx
Binary files differ
diff --git a/oox/qa/unit/shape.cxx b/oox/qa/unit/shape.cxx
index a57c779..9d01128 100644
--- a/oox/qa/unit/shape.cxx
+++ b/oox/qa/unit/shape.cxx
@@ -10,6 +10,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>
@@ -18,6 +20,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;
}
}

char const DATA_DIRECTORY[] = "/oox/qa/unit/data/";

/// oox shape tests.
@@ -54,6 +74,41 @@ void OoxShapeTest::load(const OUString& 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("multiple-group-shapes.docx");
diff --git a/oox/source/drawingml/shape.cxx b/oox/source/drawingml/shape.cxx
index 8e298bf..a9388e8 100644
--- a/oox/source/drawingml/shape.cxx
+++ b/oox/source/drawingml/shape.cxx
@@ -84,6 +84,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>
@@ -269,7 +270,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 << "'");

@@ -279,7 +280,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() )
            {
@@ -392,41 +393,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());
    }
}

@@ -651,6 +621,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,
@@ -660,7 +677,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 << "'");
@@ -726,6 +743,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);
@@ -740,81 +758,125 @@ 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( maPosition.X * EMU_PER_HMM, maPosition.Y * EMU_PER_HMM);
        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;
    aTransformation.scale(1/double(EMU_PER_HMM), 1/double(EMU_PER_HMM));

    // OOXML flips shapes before rotating them, so the rotation needs to be inverted
    if( bIsCustomShape && mbFlipH != mbFlipV )
    {
        basegfx::B2DVector aScale, aTranslate;
@@ -823,11 +885,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() );
        }