diff --git a/oox/source/drawingml/customshapeproperties.cxx b/oox/source/drawingml/customshapeproperties.cxx index 1b069f8939d7..7afbead80c99 100644 --- a/oox/source/drawingml/customshapeproperties.cxx +++ b/oox/source/drawingml/customshapeproperties.cxx @@ -227,6 +227,8 @@ void CustomShapeProperties::pushToPropSet( aPropertyMap.setProperty( PROP_MirroredY, mbMirroredY ); if( mnTextPreRotateAngle ) aPropertyMap.setProperty( PROP_TextPreRotateAngle, mnTextPreRotateAngle ); + if (mnTextCameraZRotateAngle) + aPropertyMap.setProperty(PROP_TextCameraZRotateAngle, mnTextCameraZRotateAngle); if (moTextAreaRotateAngle.has_value()) aPropertyMap.setProperty(PROP_TextRotateAngle, moTextAreaRotateAngle.value()); // Note 1: If Equations are defined - they are processed using internal div by 360 coordinates diff --git a/sd/qa/unit/layout-tests.cxx b/sd/qa/unit/layout-tests.cxx index 76f8305af512..befec96e91e4 100644 --- a/sd/qa/unit/layout-tests.cxx +++ b/sd/qa/unit/layout-tests.cxx @@ -105,11 +105,25 @@ CPPUNIT_TEST_FIXTURE(SdLayoutTest, testTdf128212) createSdImpressDoc("pptx/tdf128212.pptx"); xmlDocUniquePtr pXmlDoc = parseLayout(); - // Without the fix in place, this test would have failed with - // - Expected: 7795 - // - Actual : 12068 - assertXPath(pXmlDoc, "/metafile/push[1]/push[1]/textarray", "x", u"4523"); - assertXPath(pXmlDoc, "/metafile/push[1]/push[1]/textarray", "y", u"7795"); + // The position of the rotated text depends on the calculated text size. This depends on + // rendering, so it differs a bit on different platforms. Hence rather big delta here. + + // translation + assertXPath(pXmlDoc, "//push[@flags='PushMapMode']", 1); + assertXPath(pXmlDoc, "//push[@flags='PushMapMode']/mapmode", "mapunit", u"MapRelative"); + CPPUNIT_ASSERT_DOUBLES_EQUAL( + 331.0, getXPath(pXmlDoc, "//push[@flags='PushMapMode']/mapmode", "x").toDouble(), 3.0); + CPPUNIT_ASSERT_DOUBLES_EQUAL( + 9420.0, getXPath(pXmlDoc, "//push[@flags='PushMapMode']/mapmode", "y").toDouble(), 10.0); + // no scaling + assertXPath(pXmlDoc, "//push[@flags='PushMapMode']/mapmode", "scalex", u"(1/1)"); + assertXPath(pXmlDoc, "//push[@flags='PushMapMode']/mapmode", "scaley", u"(1/1)"); + + // text position + CPPUNIT_ASSERT_DOUBLES_EQUAL( + 4760.0, getXPath(pXmlDoc, "//push[@flags='PushMapMode']/textarray", "x").toDouble(), 3.0); + CPPUNIT_ASSERT_DOUBLES_EQUAL( + -2250.0, getXPath(pXmlDoc, "//push[@flags='PushMapMode']/textarray", "y").toDouble(), 3.0); } CPPUNIT_TEST_FIXTURE(SdLayoutTest, testColumnsLayout) diff --git a/svx/qa/unit/data/3d_rotated_text.pptx b/svx/qa/unit/data/3d_rotated_text.pptx new file mode 100644 index 000000000000..f745b3073d39 Binary files /dev/null and b/svx/qa/unit/data/3d_rotated_text.pptx differ diff --git a/svx/qa/unit/sdr.cxx b/svx/qa/unit/sdr.cxx index b85ccd64eb65..fa7f69c5cd75 100644 --- a/svx/qa/unit/sdr.cxx +++ b/svx/qa/unit/sdr.cxx @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -189,6 +190,57 @@ CPPUNIT_TEST_FIXTURE(SdrTest, testSlideBackground) // i.e. the rendering did not find the bitmap. assertXPath(pDocument, "//bitmap", 1); } + +CPPUNIT_TEST_FIXTURE(SdrTest, test3DRotatedText) +{ + // The document contains a shape with text "Vertical" and a 3D scene camera rotation of 90 deg. + // The text should appear rotated and positioned ~at the top-left of the text frame. + + loadFromFile(u"3d_rotated_text.pptx"); + + // verify the camera rotation was imported correctly + auto xDrawPages = mxComponent.queryThrow()->getDrawPages(); + auto xDrawPage = xDrawPages->getByIndex(0).queryThrow(); + auto pDrawPage = dynamic_cast(xDrawPage.get()); + CPPUNIT_ASSERT(pDrawPage); + auto* pSdrTextObj = static_cast(pDrawPage->GetSdrPage()->GetObj(0)); + CPPUNIT_ASSERT(pSdrTextObj); + CPPUNIT_ASSERT_EQUAL(90.0, pSdrTextObj->GetCameraZRotation()); + + auto aPrimitives = renderPageToPrimitives(xDrawPage); + svx::ExtendedPrimitive2dXmlDump aDumper; + xmlDocUniquePtr pDocument = aDumper.dumpAndParse(aPrimitives); + CPPUNIT_ASSERT(pDocument); + + assertXPath(pDocument, "//textsimpleportion", "text", u"Vertical"); + // x/y are the unrotated text position (in page coordinates, before the transform) + assertXPath(pDocument, "//textsimpleportion", "x", u"7799"); + assertXPath(pDocument, "//textsimpleportion", "y", u"6303"); + + assertXPath(pDocument, "//transform", 1); + double fXY11 = getXPath(pDocument, "//transform", "xy11").toDouble(); + double fXY12 = getXPath(pDocument, "//transform", "xy12").toDouble(); + double fXY13 = getXPath(pDocument, "//transform", "xy13").toDouble(); + double fXY21 = getXPath(pDocument, "//transform", "xy21").toDouble(); + double fXY22 = getXPath(pDocument, "//transform", "xy22").toDouble(); + double fXY23 = getXPath(pDocument, "//transform", "xy23").toDouble(); + basegfx::B2DHomMatrix aTransform(fXY11, fXY12, fXY13, fXY21, fXY22, fXY23); + basegfx::B2DTuple aScale, aTranslate; + double fRotate, fShearX; + aTransform.decompose(aScale, aTranslate, fRotate, fShearX); + + // no scaling, no shear + CPPUNIT_ASSERT_EQUAL(1.0, aScale.getX()); + CPPUNIT_ASSERT_EQUAL(1.0, aScale.getY()); + CPPUNIT_ASSERT_EQUAL(0.0, fShearX); + + // The text is rotated 90 degrees counterclockwise around its unrotated center. + CPPUNIT_ASSERT_EQUAL(-M_PI_2, fRotate); + // The translation values reflect current state; they may change a bit (the position of the + // rotated text is not pixel-perfect; it is about one pixel off compared to Powerpoint). + CPPUNIT_ASSERT_DOUBLES_EQUAL(2744.0, aTranslate.getX(), 10.0); + CPPUNIT_ASSERT_DOUBLES_EQUAL(14896.0, aTranslate.getY(), 10.0); +} } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/svx/source/svdraw/svdotextdecomposition.cxx b/svx/source/svdraw/svdotextdecomposition.cxx index a5b46709fb63..91c0517c1b8b 100644 --- a/svx/source/svdraw/svdotextdecomposition.cxx +++ b/svx/source/svdraw/svdotextdecomposition.cxx @@ -51,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -596,23 +597,6 @@ void SdrTextObj::impDecomposeBlockTextPrimitive( const double fStartInY(bVerticalWriting && !bTopToBottom ? aAdjustTranslate.getY() + aOutlinerScale.getY() : aAdjustTranslate.getY()); basegfx::B2DHomMatrix aNewTransformA(basegfx::utils::createTranslateB2DHomMatrix(fStartInX, fStartInY)); - // Apply the camera rotation. It have to be applied after adjustment of - // the text (top, bottom, center, left, right). - if(GetCameraZRotation() != 0) - { - // First find the text rect. - basegfx::B2DRange aTextRectangle(/*x1=*/aTranslate.getX() + aAdjustTranslate.getX(), - /*y1=*/aTranslate.getY() + aAdjustTranslate.getY(), - /*x2=*/aTranslate.getX() + aOutlinerScale.getX() - aAdjustTranslate.getX(), - /*y2=*/aTranslate.getY() + aOutlinerScale.getY() - aAdjustTranslate.getY()); - - // Rotate the text from the center point. - basegfx::B2DVector aTranslateToCenter(aTextRectangle.getWidth() / 2, aTextRectangle.getHeight() / 2); - aNewTransformA.translate(-aTranslateToCenter.getX(), -aTranslateToCenter.getY()); - aNewTransformA.rotate(basegfx::deg2rad(360.0 - GetCameraZRotation() )); - aNewTransformA.translate(aTranslateToCenter.getX(), aTranslateToCenter.getY()); - } - // mirroring. We are now in aAnchorTextRange sizes. When mirroring in X and Y, // move the null point which was top left to bottom right. const bool bMirrorX(aScale.getX() < 0.0); @@ -639,6 +623,20 @@ void SdrTextObj::impDecomposeBlockTextPrimitive( rOutliner.StripPortions(aBreakup); rTarget = aBreakup.getTextPortionPrimitives(); + // Apply 3D camera Z rotation as a post-processing step + if (GetCameraZRotation() != 0 && !rTarget.empty()) + { + // Rotate around the center of the unrotated text content + const drawinglayer::geometry::ViewInformation2D aViewInfoLocal; + const basegfx::B2DRange aOrigBounds = rTarget.getB2DRange(aViewInfoLocal); + const basegfx::B2DHomMatrix aRotationMat(basegfx::utils::createRotateAroundPoint( + aOrigBounds.getCenter(), basegfx::deg2rad(360.0 - GetCameraZRotation()))); + + rtl::Reference pRotation( + new drawinglayer::primitive2d::TransformPrimitive2D(aRotationMat, std::move(rTarget))); + rTarget = drawinglayer::primitive2d::Primitive2DContainer{ pRotation }; + } + // cleanup outliner rOutliner.SetBackgroundColor(aOriginalBackColor); rOutliner.Clear();