oox/inc/drawingml/textbodyproperties.hxx             |    6 +
 oox/source/drawingml/textbodyproperties.cxx          |   48 ++++++++++++-
 oox/source/drawingml/textbodypropertiescontext.cxx   |    3 
 oox/source/export/drawingml.cxx                      |   30 ++++++++
 sd/qa/unit/ShapeImportExportTest.cxx                 |   66 +++++++++++++++++++
 sd/qa/unit/data/TextDistancesInsetsWithRotation.pptx |binary
 svx/qa/unit/svdraw.cxx                               |    4 -
 7 files changed, 151 insertions(+), 6 deletions(-)

New commits:
commit d5994ee1dbfe309faac34e8a443842b2fc057569
Author:     Andras Timar <[email protected]>
AuthorDate: Mon Feb 16 13:26:47 2026 +0100
Commit:     Miklos Vajna <[email protected]>
CommitDate: Tue Feb 17 09:19:19 2026 +0100

    pptx import: fix text margin rotation for vert/rot text directions
    
    In PowerPoint, the inner margins (lIns, tIns, rIns, bIns) of bodyPr
    are always relative to the unrotated shape frame, regardless of text
    rotation. In LibreOffice, when text rotation is applied (via vert or
    bodyPr rot), the rendering rotates the entire text area including
    margin layout, causing margins to appear at wrong edges.
    
    Fix pushTextDistances() in two ways:
    
    1. Add moTextAreaRotation handling (bodyPr rot attribute) to pre-rotate
       margins so they end up at the correct edges after rendering rotation.
       This follows the established pattern for moTextPreRotation and moVert.
    
    2. When any rotation is active (nOff != 0), fill in OOXML default inset
       values (lIns=91440, tIns=45720, rIns=91440, bIns=45720 EMU) for any
       insets not explicitly specified. Previously, omitted insets left
       pushTextDistances() unable to rotate the defaults, so LO's un-rotated
       defaults were used. This caused e.g. vert270 shapes with default
       insets to have left/right margins of 254 HMM instead of the correct
       127 HMM, making text wrap when it shouldn't.
    
    Also add the reverse margin rotation in the OOXML export path so that
    round-tripped files preserve the original inset values.
    
    Change-Id: I0f2b14effb14c51a347061bcc40b7fd93e328cbf
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/199486
    Tested-by: Jenkins CollaboraOffice <[email protected]>
    Reviewed-by: Miklos Vajna <[email protected]>

diff --git a/oox/inc/drawingml/textbodyproperties.hxx 
b/oox/inc/drawingml/textbodyproperties.hxx
index 46cf99614c56..a9e4364d8cb9 100644
--- a/oox/inc/drawingml/textbodyproperties.hxx
+++ b/oox/inc/drawingml/textbodyproperties.hxx
@@ -36,8 +36,12 @@ struct TextBodyProperties
     PropertyMap                                     maPropertyMap;
     // TextPreRotateAngle. Used in diagram (SmartArt) import.
     std::optional< sal_Int32 >                      moTextPreRotation;
-    // TextRotateAngle. ODF draw:text-rotate-angle, OOXML 'rot' attribute in 
<bodyPr> element
+    // TextRotateAngle. ODF draw:text-rotate-angle, OOXML 'rot' attribute in 
<bodyPr> element.
+    // Note: also accumulates txXfrm rotation from SmartArt/diagram layout.
     std::optional< sal_Int32 >                      moTextAreaRotation;
+    // True when moTextAreaRotation was set from bodyPr 'rot' attribute (not 
txXfrm).
+    // Used to decide whether to compensate margins for text area rotation.
+    bool                                            mbBodyPrRotation = false;
     bool                                            mbAnchorCtr;
     std::optional< sal_Int32 >                      moVert;
     bool                                            moUpright = false;
diff --git a/oox/source/drawingml/textbodyproperties.cxx 
b/oox/source/drawingml/textbodyproperties.cxx
index ff501e40c413..d9163f146694 100644
--- a/oox/source/drawingml/textbodyproperties.cxx
+++ b/oox/source/drawingml/textbodyproperties.cxx
@@ -92,6 +92,48 @@ void TextBodyProperties::pushTextDistances(Size const& 
rTextAreaSize)
     else if (moVert && moVert.value() == XML_vert270)
         nOff = (nOff + 1) % aProps.size();
 
+    // Compensate for text area rotation (bodyPr rot attribute).
+    // In PowerPoint, margins are relative to the unrotated frame.
+    // The rendering rotates the text area including margins, so pre-rotate
+    // margins in the opposite direction to compensate.
+    // Only do this for bodyPr 'rot', not for txXfrm rotation 
(SmartArt/diagram).
+    if (mbBodyPrRotation && moTextAreaRotation)
+    {
+        sal_Int32 nRotDeg = (*moTextAreaRotation / 60000) % 360;
+        if (nRotDeg < 0)
+            nRotDeg += 360;
+
+        if (nRotDeg >= 45 && nRotDeg < 135)       // ~90° CW
+            nOff = (nOff + 1) % aProps.size();
+        else if (nRotDeg >= 135 && nRotDeg < 225)  // ~180°
+            nOff = (nOff + 2) % aProps.size();
+        else if (nRotDeg >= 225 && nRotDeg < 315)  // ~270° CW
+            nOff = (nOff + 3) % aProps.size();
+    }
+
+    // When vert or bodyPr rot is active, fill in OOXML default insets for any
+    // not explicitly specified, so the defaults get rotated to the correct 
edges.
+    // Without rotation, LO's own defaults (which match OOXML) are fine as-is.
+    // Only do this for moVert / bodyPr rot (OOXML shape-level attributes),
+    // not for moTextPreRotation or txXfrm rotation (SmartArt/diagram 
mechanism).
+    // OOXML defaults: lIns=91440 tIns=45720 rIns=91440 bIns=45720 (EMU)
+    //                 → 254, 127, 254, 127 (HMM)
+    bool bNeedsDefaultInsets
+        = (moVert.has_value()
+           && (moVert.value() == XML_eaVert || moVert.value() == XML_vert
+               || moVert.value() == XML_vert270))
+          || mbBodyPrRotation;
+    auto aInsets = moInsets;
+    if (nOff != 0 && bNeedsDefaultInsets)
+    {
+        static constexpr sal_Int32 aDefaultInsets[] = { 254, 127, 254, 127 };
+        for (size_t i = 0; i < aInsets.size(); i++)
+        {
+            if (!aInsets[i])
+                aInsets[i] = aDefaultInsets[i];
+        }
+    }
+
     for (size_t i = 0; i < aProps.size(); i++)
     {
         sal_Int32 nVal = 0;
@@ -113,14 +155,14 @@ void TextBodyProperties::pushTextDistances(Size const& 
rTextAreaSize)
 
         sal_Int32 nTextOffsetValue = nVal;
 
-        if (moInsets[i])
+        if (aInsets[i])
         {
-            nTextOffsetValue = *moInsets[i] + nVal;
+            nTextOffsetValue = *aInsets[i] + nVal;
         }
 
         // if inset is set, then always set the value
         // this prevents the default to be set (0 is a valid value)
-        if (moInsets[i] || nTextOffsetValue)
+        if (aInsets[i] || nTextOffsetValue)
         {
             maTextDistanceValues[nOff] = nTextOffsetValue;
         }
diff --git a/oox/source/drawingml/textbodypropertiescontext.cxx 
b/oox/source/drawingml/textbodypropertiescontext.cxx
index 2508513f89d1..2116c1943feb 100644
--- a/oox/source/drawingml/textbodypropertiescontext.cxx
+++ b/oox/source/drawingml/textbodypropertiescontext.cxx
@@ -112,7 +112,10 @@ TextBodyPropertiesContext::TextBodyPropertiesContext( 
ContextHandler2Helper cons
 
     // ST_Angle
     if (rAttribs.getInteger(XML_rot).has_value())
+    {
         mrTextBodyProp.moTextAreaRotation = 
rAttribs.getInteger(XML_rot).value();
+        mrTextBodyProp.mbBodyPrRotation = true;
+    }
 
 //   bool bRtlCol = rAttribs.getBool( XML_rtlCol, false );
     // ST_PositiveCoordinate
diff --git a/oox/source/export/drawingml.cxx b/oox/source/export/drawingml.cxx
index 0bf735cd48ea..a0a14460c9f3 100644
--- a/oox/source/export/drawingml.cxx
+++ b/oox/source/export/drawingml.cxx
@@ -4219,6 +4219,36 @@ void DrawingML::WriteText(const Reference<XInterface>& 
rXIface, bool bBodyPr, bo
         }
     }
 
+    // Reverse the margin compensation applied during import for text area 
rotation.
+    if (nTextRotateAngleDeg100.has_value())
+    {
+        // Convert LO CCW Degree100 to CW degrees
+        sal_Int32 nCWDeg = (-nTextRotateAngleDeg100->get() / 100) % 360;
+        if (nCWDeg < 0)
+            nCWDeg += 360;
+
+        if (nCWDeg >= 45 && nCWDeg < 135)        // ~90° CW
+        {
+            sal_Int32 nHelp = nLeft;
+            nLeft = nTop;
+            nTop = nRight;
+            nRight = nBottom;
+            nBottom = nHelp;
+        }
+        else if (nCWDeg >= 135 && nCWDeg < 225)   // ~180°
+        {
+            std::swap(nLeft, nRight);
+            std::swap(nTop, nBottom);
+        }
+        else if (nCWDeg >= 225 && nCWDeg < 315)   // ~270° CW
+        {
+            sal_Int32 nHelp = nLeft;
+            nLeft = nBottom;
+            nBottom = nRight;
+            nRight = nTop;
+            nTop = nHelp;
+        }
+    }
 
     std::optional<OString> sTextRotateAngleMSUnit;
     if (nTextRotateAngleDeg100.has_value())
diff --git a/sd/qa/unit/ShapeImportExportTest.cxx 
b/sd/qa/unit/ShapeImportExportTest.cxx
index 9da163e11c80..bc2c3f6822a3 100644
--- a/sd/qa/unit/ShapeImportExportTest.cxx
+++ b/sd/qa/unit/ShapeImportExportTest.cxx
@@ -31,12 +31,14 @@ public:
     void testTextDistancesOOXML_LargerThanTextAreaSpecialCase();
     void testTextDistancesOOXML_Export();
     void testTextDistancesODP_OOXML_Export();
+    void testTextDistancesWithRotationOOXML();
 
     CPPUNIT_TEST_SUITE(ShapeImportExportTest);
     CPPUNIT_TEST(testTextDistancesOOXML);
     CPPUNIT_TEST(testTextDistancesOOXML_LargerThanTextAreaSpecialCase);
     CPPUNIT_TEST(testTextDistancesOOXML_Export);
     CPPUNIT_TEST(testTextDistancesODP_OOXML_Export);
+    CPPUNIT_TEST(testTextDistancesWithRotationOOXML);
     CPPUNIT_TEST_SUITE_END();
 };
 
@@ -383,6 +385,70 @@ void 
ShapeImportExportTest::testTextDistancesODP_OOXML_Export()
                      { { "tIns", u"720000" }, { "bIns", u"2520000" } });
 }
 
+/* Test text distances (insets) with text area rotation */
+void ShapeImportExportTest::testTextDistancesWithRotationOOXML()
+{
+    createSdImpressDoc("TextDistancesInsetsWithRotation.pptx");
+    SdrPage const* pPage = GetPage(1);
+
+    // 90° CW + lIns=1cm: nOff=1 → lIns maps to TextUpperDistance
+    {
+        auto* pTextObj = DynCastSdrTextObj(searchObject(pPage, u"Rot90_L1cm"));
+        CPPUNIT_ASSERT(pTextObj);
+        CPPUNIT_ASSERT_EQUAL(tools::Long(0), pTextObj->GetTextLeftDistance());
+        CPPUNIT_ASSERT_EQUAL(tools::Long(1000), 
pTextObj->GetTextUpperDistance());
+        CPPUNIT_ASSERT_EQUAL(tools::Long(0), pTextObj->GetTextRightDistance());
+        CPPUNIT_ASSERT_EQUAL(tools::Long(0), pTextObj->GetTextLowerDistance());
+    }
+    // 90° CW + tIns=2cm: nOff=1 → tIns maps to TextRightDistance
+    {
+        auto* pTextObj = DynCastSdrTextObj(searchObject(pPage, u"Rot90_T2cm"));
+        CPPUNIT_ASSERT(pTextObj);
+        CPPUNIT_ASSERT_EQUAL(tools::Long(0), pTextObj->GetTextLeftDistance());
+        CPPUNIT_ASSERT_EQUAL(tools::Long(0), pTextObj->GetTextUpperDistance());
+        CPPUNIT_ASSERT_EQUAL(tools::Long(2000), 
pTextObj->GetTextRightDistance());
+        CPPUNIT_ASSERT_EQUAL(tools::Long(0), pTextObj->GetTextLowerDistance());
+    }
+    // 270° CW + lIns=1cm: nOff=3 → lIns maps to TextLowerDistance
+    {
+        auto* pTextObj = DynCastSdrTextObj(searchObject(pPage, 
u"Rot270_L1cm"));
+        CPPUNIT_ASSERT(pTextObj);
+        CPPUNIT_ASSERT_EQUAL(tools::Long(0), pTextObj->GetTextLeftDistance());
+        CPPUNIT_ASSERT_EQUAL(tools::Long(0), pTextObj->GetTextUpperDistance());
+        CPPUNIT_ASSERT_EQUAL(tools::Long(0), pTextObj->GetTextRightDistance());
+        CPPUNIT_ASSERT_EQUAL(tools::Long(1000), 
pTextObj->GetTextLowerDistance());
+    }
+    // 180° + lIns=1cm: nOff=2 → lIns maps to TextRightDistance
+    {
+        auto* pTextObj = DynCastSdrTextObj(searchObject(pPage, 
u"Rot180_L1cm"));
+        CPPUNIT_ASSERT(pTextObj);
+        CPPUNIT_ASSERT_EQUAL(tools::Long(0), pTextObj->GetTextLeftDistance());
+        CPPUNIT_ASSERT_EQUAL(tools::Long(0), pTextObj->GetTextUpperDistance());
+        CPPUNIT_ASSERT_EQUAL(tools::Long(1000), 
pTextObj->GetTextRightDistance());
+        CPPUNIT_ASSERT_EQUAL(tools::Long(0), pTextObj->GetTextLowerDistance());
+    }
+    // No rotation control: lIns=1cm stays as TextLeftDistance
+    {
+        auto* pTextObj = DynCastSdrTextObj(searchObject(pPage, u"NoRot_L1cm"));
+        CPPUNIT_ASSERT(pTextObj);
+        CPPUNIT_ASSERT_EQUAL(tools::Long(1000), 
pTextObj->GetTextLeftDistance());
+        CPPUNIT_ASSERT_EQUAL(tools::Long(0), pTextObj->GetTextUpperDistance());
+        CPPUNIT_ASSERT_EQUAL(tools::Long(0), pTextObj->GetTextRightDistance());
+        CPPUNIT_ASSERT_EQUAL(tools::Long(0), pTextObj->GetTextLowerDistance());
+    }
+    // vert270 with default insets (no explicit lIns/tIns/rIns/bIns):
+    // OOXML defaults are lIns=254, tIns=127, rIns=254, bIns=127 (HMM).
+    // With nOff=1 for vert270: bIns→Left, lIns→Upper, tIns→Right, rIns→Lower
+    {
+        auto* pTextObj = DynCastSdrTextObj(searchObject(pPage, 
u"Vert270_DefaultIns"));
+        CPPUNIT_ASSERT(pTextObj);
+        CPPUNIT_ASSERT_EQUAL(tools::Long(127), 
pTextObj->GetTextLeftDistance());
+        CPPUNIT_ASSERT_EQUAL(tools::Long(254), 
pTextObj->GetTextUpperDistance());
+        CPPUNIT_ASSERT_EQUAL(tools::Long(127), 
pTextObj->GetTextRightDistance());
+        CPPUNIT_ASSERT_EQUAL(tools::Long(254), 
pTextObj->GetTextLowerDistance());
+    }
+}
+
 CPPUNIT_TEST_SUITE_REGISTRATION(ShapeImportExportTest);
 
 CPPUNIT_PLUGIN_IMPLEMENT();
diff --git a/sd/qa/unit/data/TextDistancesInsetsWithRotation.pptx 
b/sd/qa/unit/data/TextDistancesInsetsWithRotation.pptx
new file mode 100644
index 000000000000..dd7db7c6c3a0
Binary files /dev/null and 
b/sd/qa/unit/data/TextDistancesInsetsWithRotation.pptx differ
diff --git a/svx/qa/unit/svdraw.cxx b/svx/qa/unit/svdraw.cxx
index b3262e588e94..45ec8fe7a92f 100644
--- a/svx/qa/unit/svdraw.cxx
+++ b/svx/qa/unit/svdraw.cxx
@@ -766,8 +766,8 @@ CPPUNIT_TEST_FIXTURE(SvdrawTest, 
testClipVerticalTextOverflow)
     // Test vertically overflowing text, with vertical text direction
     assertXPathContent(pDocument, 
"count((//sdrblocktext)[6]//textsimpleportion)", u"12");
     // make sure text is aligned correctly after the overflowing text is 
clipped
-    assertXPath(pDocument, "((//sdrblocktext)[6]//textsimpleportion)[1]", "x", 
u"13093");
-    assertXPath(pDocument, "((//sdrblocktext)[6]//textsimpleportion)[12]", 
"x", u"4711");
+    assertXPath(pDocument, "((//sdrblocktext)[6]//textsimpleportion)[1]", "x", 
u"12964");
+    assertXPath(pDocument, "((//sdrblocktext)[6]//textsimpleportion)[12]", 
"x", u"4582");
 
     // make sure the text that isn't overflowing is still aligned properly
     assertXPathContent(pDocument, 
"count((//sdrblocktext)[7]//textsimpleportion)", u"3");

Reply via email to