From 04bc53cd7e566a3a781e4d77524d6fcc36590204 Mon Sep 17 00:00:00 2001 From: Mike Kaganski Date: Mon, 9 Mar 2026 19:54:19 +0500 Subject: [PATCH] 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 --- .../drawingml/customshapeproperties.cxx | 2 + sd/qa/unit/layout-tests.cxx | 24 ++++++-- svx/qa/unit/data/3d_rotated_text.pptx | Bin 0 -> 10708 bytes svx/qa/unit/sdr.cxx | 52 ++++++++++++++++++ svx/source/svdraw/svdotextdecomposition.cxx | 32 +++++------ 5 files changed, 88 insertions(+), 22 deletions(-) create mode 100644 svx/qa/unit/data/3d_rotated_text.pptx 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 0000000000000000000000000000000000000000..f745b3073d39178002a5cba8b0d622440c4f76d4 GIT binary patch literal 10708 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 bM*fehRg^=5>G3x=j191ax#Op_-`@TYGp`8p literal 0 HcmV?d00001 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();