oox/inc/drawingml/textliststyle.hxx | 2 - oox/qa/unit/drawingml.cxx | 24 +++++++++++++++- oox/source/drawingml/shape.cxx | 48 +++++++++++++++++++++++++++++++++ oox/source/drawingml/textliststyle.cxx | 13 ++++++-- 4 files changed, 82 insertions(+), 5 deletions(-)
New commits: commit b2c5d52f733cca2656daa0a2cfdd85a1108635f4 Author: Miklos Vajna <[email protected]> AuthorDate: Mon Oct 13 09:02:10 2025 +0200 Commit: Caolán McNamara <[email protected]> CommitDate: Mon Oct 13 09:54:56 2025 +0200 tdf#168559 PPTX imp: fix missing list style for outline shapes on master pages Load oox/qa/unit/data/outliner-list-style.odp into Impress, save as PPTX, load it the PPTX, save the PPTX again (source is now PPTX), open it again, go to the end of the first bullet, enter, tab, indent grows to a large value. What happens is that the PPTX export could write the correct list style for the shape (based on the master page) when the source was ODP, but it can't do the same when the source was PPTX, so after all this a PPTX import problem. Fix this by improving the formatting of the outliner shape on master pages: if the outliner shape was empty, then we don't insert formatted text body, so instead try to format the existing content of the outliner shape, given that such shapes have pre-existing content on the master page. Notes: - Do this only for outliner shapes, CppunitTest_sd_import_tests2's testTdf103792 is a sample that shows doing this for all placeholders on the master page is not correct. - The first line offset is typically negative, but do guard for positive values, otherwise the exported ODP would be invalid, see CppunitTest_sd_export_tests's testTdf128550. - It's possible to have outliner shapes with custom prompt text, which is typically not bulleted, so leave those alone, otherwise we would lose that prompt text, as visible in CppunitTest_sd_export_tests-ooxml4's testCustomPromptTexts. Change-Id: I5b1768a4a4b9cb8494785a38e040dacdf4ab5289 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/192264 Tested-by: Jenkins CollaboraOffice <[email protected]> Reviewed-by: Caolán McNamara <[email protected]> diff --git a/oox/inc/drawingml/textliststyle.hxx b/oox/inc/drawingml/textliststyle.hxx index 88de4621de13..282391452456 100644 --- a/oox/inc/drawingml/textliststyle.hxx +++ b/oox/inc/drawingml/textliststyle.hxx @@ -61,7 +61,7 @@ public: /// Set properties on xNumRules based on maListStyle, for all levels except nIgnoreLevel. void pushToNumberingRules(const css::uno::Reference<css::container::XIndexReplace>& xNumRules, - size_t nIgnoreLevel); + std::optional<size_t> nIgnoreLevel); #ifdef DBG_UTIL void dump() const; diff --git a/oox/qa/unit/drawingml.cxx b/oox/qa/unit/drawingml.cxx index a442ef1f28ef..e5fc762aa12d 100644 --- a/oox/qa/unit/drawingml.cxx +++ b/oox/qa/unit/drawingml.cxx @@ -31,6 +31,7 @@ #include <com/sun/star/util/XTheme.hpp> #include <com/sun/star/drawing/HomogenMatrix3.hpp> #include <com/sun/star/drawing/PointSequenceSequence.hpp> +#include <com/sun/star/drawing/XMasterPagesSupplier.hpp> #include <docmodel/uno/UnoGradientTools.hxx> #include <docmodel/uno/UnoComplexColor.hxx> @@ -786,7 +787,7 @@ CPPUNIT_TEST_FIXTURE(OoxDrawingmlTest, testDOCXVerticalLineRotation) CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(0), nRotateAngle); } -CPPUNIT_TEST_FIXTURE(OoxDrawingmlTest, testPPTXImportOutlinerListStyleDirectFormat) +CPPUNIT_TEST_FIXTURE(OoxDrawingmlTest, testPPTXImportOutlinerListStyle) { // Given a PPTX file with a slide with an outline shape: // When loading that document: @@ -818,6 +819,27 @@ CPPUNIT_TEST_FIXTURE(OoxDrawingmlTest, testPPTXImportOutlinerListStyleDirectForm // - Actual : -800 CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(-889), nFirstLineOffset); // i.e. the sum of these two were 2800, not 1499, the indent was larger than expected. + + // Also check that the master slide's outliner has the correct numbering rules: + uno::Reference<drawing::XMasterPagesSupplier> xMasterPagesSupplier(mxComponent, uno::UNO_QUERY); + uno::Reference<drawing::XDrawPage> xMasterPage( + xMasterPagesSupplier->getMasterPages()->getByIndex(0), uno::UNO_QUERY); + // First would be the title shape. + uno::Reference<text::XTextRange> xMasterShape(xMasterPage->getByIndex(1), uno::UNO_QUERY); + xShapeText.set(xMasterShape->getText(), uno::UNO_QUERY); + xParagraphs = xShapeText->createEnumeration(); + // As a sample, check the 2nd paragraph's level 3 left margin. + xParagraphs->nextElement(); + xParagraph.set(xParagraphs->nextElement(), uno::UNO_QUERY); + xParagraph->getPropertyValue(u"NumberingRules"_ustr) >>= xNumberingRules; + aMap = comphelper::SequenceAsHashMap(xNumberingRules->getByIndex(2)); + nLeftMargin = 0; + aMap["LeftMargin"] >>= nLeftMargin; + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 2388 + // - Actual : 3600 + // i.e. the master was wrong, re-export to PPTX could not write the correct numbering rules. + CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(2388), nLeftMargin); } CPPUNIT_PLUGIN_IMPLEMENT(); diff --git a/oox/source/drawingml/shape.cxx b/oox/source/drawingml/shape.cxx index 897a7ac9fc51..8967f616c958 100644 --- a/oox/source/drawingml/shape.cxx +++ b/oox/source/drawingml/shape.cxx @@ -71,6 +71,7 @@ #include <com/sun/star/awt/FontWeight.hpp> #include <com/sun/star/graphic/XGraphic.hpp> #include <com/sun/star/container/XNamed.hpp> +#include <com/sun/star/container/XEnumerationAccess.hpp> #include <com/sun/star/lang/XMultiServiceFactory.hpp> #include <com/sun/star/xml/dom/XDocument.hpp> #include <com/sun/star/xml/sax/XFastSAXSerializable.hpp> @@ -914,6 +915,45 @@ Degree100 lcl_MSORotateAngleToAPIAngle(const sal_Int32 nMSORotationAngle) // API RotateAngle has opposite direction than nMSORotationAngle, thus 'minus'. return NormAngle36000(-nAngle); } + +void PushMasterTextListStyleToMasterShapeParagraphs(TextListStyle& rMasterTextListStyle, const uno::Reference<drawing::XShape>& xShape) +{ + uno::Reference<text::XTextRange> xShapeText(xShape, UNO_QUERY); + if (!xShapeText.is()) + { + return; + } + + uno::Reference<container::XEnumerationAccess> xText(xShapeText->getText(), uno::UNO_QUERY); + if (!xText.is()) + { + return; + } + + uno::Reference<container::XEnumeration> xParagraphs = xText->createEnumeration(); + if (!xParagraphs.is()) + { + return; + } + + while (xParagraphs->hasMoreElements()) + { + uno::Reference<beans::XPropertySet> xParagraph(xParagraphs->nextElement(), uno::UNO_QUERY); + if (!xParagraph.is()) + { + continue; + } + + uno::Reference<container::XIndexReplace> xNumberingRules; + if (!(xParagraph->getPropertyValue("NumberingRules") >>= xNumberingRules)) + { + continue; + } + + rMasterTextListStyle.pushToNumberingRules(xNumberingRules, std::nullopt); + xParagraph->setPropertyValue("NumberingRules", uno::Any(xNumberingRules)); + } +} } Reference< XShape > const & Shape::createAndInsert( @@ -1286,6 +1326,7 @@ Reference< XShape > const & Shape::createAndInsert( } Reference< XPropertySet > xSet( mxShape, UNO_QUERY ); + bool bPlaceholderWithCustomPrompt = pPlaceholder && pPlaceholder->getCustomPrompt(); if (xSet.is()) { if (bTopWriterLine && !maSize.Width) @@ -2209,6 +2250,13 @@ Reference< XShape > const & Shape::createAndInsert( } } } + else if (mpTextBody && aServiceName == "com.sun.star.presentation.OutlinerShape" + && mpMasterTextListStyle && !bPlaceholderWithCustomPrompt) + { + // If mpTextBody is not empty, then the insertAt() above inserts formatted text. If it's + // empty, then we format the existing text of the outliner shape here. + PushMasterTextListStyleToMasterShapeParagraphs(*mpMasterTextListStyle, mxShape); + } else if (mbTextBox) { // No drawingML text, but WPS text is expected: save the theme diff --git a/oox/source/drawingml/textliststyle.cxx b/oox/source/drawingml/textliststyle.cxx index ee695908d48a..0ad0b5108b27 100644 --- a/oox/source/drawingml/textliststyle.cxx +++ b/oox/source/drawingml/textliststyle.cxx @@ -70,7 +70,7 @@ void TextListStyle::apply( const TextListStyle& rTextListStyle ) } void TextListStyle::pushToNumberingRules(const uno::Reference<container::XIndexReplace>& xNumRules, - size_t nIgnoreLevel) + std::optional<size_t> oIgnoreLevel) { TextParagraphPropertiesArray& rListStyle = getListStyle(); size_t nLevels = xNumRules->getCount(); @@ -81,7 +81,7 @@ void TextListStyle::pushToNumberingRules(const uno::Reference<container::XIndexR for (size_t nLevel = 0; nLevel < nLevels; ++nLevel) { - if (nLevel == nIgnoreLevel) + if (oIgnoreLevel && nLevel == *oIgnoreLevel) { continue; } @@ -96,7 +96,14 @@ void TextListStyle::pushToNumberingRules(const uno::Reference<container::XIndexR } if (rLevel.getFirstLineIndentation()) { - aMap["FirstLineOffset"] <<= *rLevel.getFirstLineIndentation(); + sal_Int32 nFirstLineIndentation = *rLevel.getFirstLineIndentation(); + if (nFirstLineIndentation > 0) + { + // The opposite of this would be exported as <style:list-level-properties + // text:min-label-width="..."> to ODF, where negative values are not allowed. + nFirstLineIndentation = 0; + } + aMap["FirstLineOffset"] <<= nFirstLineIndentation; bChanged = true; }
