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