oox/source/drawingml/customshapeproperties.cxx | 2 sd/qa/unit/layout-tests.cxx | 24 +++++++++-- svx/qa/unit/data/3d_rotated_text.pptx |binary svx/qa/unit/sdr.cxx | 52 +++++++++++++++++++++++++ svx/source/svdraw/svdotextdecomposition.cxx | 32 +++++++-------- 5 files changed, 88 insertions(+), 22 deletions(-)
New commits: commit 04bc53cd7e566a3a781e4d77524d6fcc36590204 Author: Mike Kaganski <[email protected]> AuthorDate: Mon Mar 9 19:54:19 2026 +0500 Commit: Mike Kaganski <[email protected]> CommitDate: Tue Mar 10 07:01:48 2026 +0100 tdf#171225: don't lose camera rotation angle for mnShapePresetType < 0 For the case when mnShapePresetType >= 0, the code setting it was added in commit c50e44b270bc3048ff9c1a000c3afed1dab9e0bf (tdf#126060 Handle text camera z rotation while pptx import., 2019-10-16). But it omitted the other case. This change also improves positioning of the rotated text, initially implemented in that commit. The rotation is now added to the block text decomposition as a separate transform primitive. The text center is now obtained directly from the decomposition. After the change, I see about 1-pixel difference compared to PowerPoint render at 100% zoom (before that, the offset was tens of millimeters). testTdf128212 had to be corrected, because the metafile now has a new element in the parsed XML, and the final positioning has changed (it is better now). Unfortunately, I don't know what would it now produce if the fix is broken, so I had to remove respective comment. This change does not fix tdf#128206. Change-Id: I86cbd0710744b14e8b9b68a436affc5b08703a12 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/201286 Tested-by: Jenkins Reviewed-by: Mike Kaganski <[email protected]> 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 <svx/sdr/contact/displayinfo.hxx> #include <svx/sdr/contact/viewcontact.hxx> #include <svx/sdr/contact/viewobjectcontact.hxx> +#include <svx/svdoashp.hxx> #include <svx/svdpage.hxx> #include <svx/unopage.hxx> #include <vcl/virdev.hxx> @@ -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<drawing::XDrawPagesSupplier>()->getDrawPages(); + auto xDrawPage = xDrawPages->getByIndex(0).queryThrow<drawing::XDrawPage>(); + auto pDrawPage = dynamic_cast<SvxDrawPage*>(xDrawPage.get()); + CPPUNIT_ASSERT(pDrawPage); + auto* pSdrTextObj = static_cast<SdrTextObj*>(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 <drawinglayer/primitive2d/texthierarchyprimitive2d.hxx> #include <drawinglayer/primitive2d/graphicprimitive2d.hxx> #include <drawinglayer/primitive2d/textlayoutdevice.hxx> +#include <drawinglayer/primitive2d/transformprimitive2d.hxx> #include <svx/unoapi.hxx> #include <drawinglayer/geometry/viewinformation2d.hxx> #include <editeng/outlobj.hxx> @@ -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();
