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();

Reply via email to