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 5b8c4db77397bc0017cc9b8c19bee6441214f8f1 Author: Miklos Vajna <[email protected]> AuthorDate: Mon Oct 13 09:02:10 2025 +0200 Commit: Miklos Vajna <[email protected]> CommitDate: Mon Oct 13 14:24:42 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/+/192270 Reviewed-by: Miklos Vajna <[email protected]> Tested-by: Jenkins 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 8b443970f691..2c38faff9285 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 8caf552a7ceb..dedfffde0f48 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; }
