include/oox/export/drawingml.hxx | 5 + oox/qa/unit/data/outliner-list-style.odp |binary oox/qa/unit/export.cxx | 25 ++++++ oox/source/export/drawingml.cxx | 129 +++++++++++++++++++++++++++++-- 4 files changed, 151 insertions(+), 8 deletions(-)
New commits: commit 6a8b96ddd47af2be3f06e299ee7058438083ba5b Author: Miklos Vajna <[email protected]> AuthorDate: Fri Sep 26 11:52:23 2025 +0200 Commit: Caolán McNamara <[email protected]> CommitDate: Fri Sep 26 14:49:36 2025 +0200 PPTX export: fix missing non-first level list style for outline shapes Export the bugdoc to PPTX, open in PowerPoint, go to the first slide, go to the end of the first bullet, enter, tab, indent grows to a large value, while it should only grow to match the existing other already indented bullet. It seems this happens because these indents are defined in the "list style" of the shape, where we wrote the paragraph properties only for the first level, see commit 0f9dc676eefce79ea63218edd910af486a09a52b (tdf#59323: pptx export: add initial support for lstStyles in textboxes, 2021-06-16). Fix the problem by writing paragraph properties for all list levels that the outliner shape stores: Impress has 7 levels and PowerPoint has markup for 9 levels, so in practice this can be mapped without problems. A further improvement would be to make sure the outline shape on the normal page and the master page is associated correctly, and then it would not be necessary to repeat the formatting for shapes on the normal slide. Similarly, the PPTX import still needs to parse this list style of the shape, which is not yet done here, so the editing in PowerPoint is now correct, but not in Impress. Change-Id: I1aecb3c062ccbe5014d322d0561f0d2ff50ac698 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/191536 Tested-by: Caolán McNamara <[email protected]> Tested-by: Jenkins CollaboraOffice <[email protected]> Reviewed-by: Caolán McNamara <[email protected]> diff --git a/include/oox/export/drawingml.hxx b/include/oox/export/drawingml.hxx index 810fc4ff492e..e5eaf6f60363 100644 --- a/include/oox/export/drawingml.hxx +++ b/include/oox/export/drawingml.hxx @@ -451,6 +451,11 @@ public: void WriteText( const css::uno::Reference< css::uno::XInterface >& rXIface, bool bBodyPr, bool bText = true, sal_Int32 nXmlNamespace = 0, bool bWritePropertiesAsLstStyles = false); + /// Writes one list level inside the list styles container. + void WriteLstStyle(const css::uno::Reference<css::text::XTextContent>& rParagraph, + bool& rbOverridingCharHeight, sal_Int32& rnCharHeight, + const css::uno::Reference<css::beans::XPropertySet>& rXShapePropSet, + sal_Int32 nElement); /** Populates the lstStyle with the shape's text run and paragraph properties */ void WriteLstStyles(const css::uno::Reference<css::text::XTextContent>& rParagraph, bool& rbOverridingCharHeight, sal_Int32& rnCharHeight, diff --git a/oox/qa/unit/data/outliner-list-style.odp b/oox/qa/unit/data/outliner-list-style.odp new file mode 100644 index 000000000000..dc86caf053bf Binary files /dev/null and b/oox/qa/unit/data/outliner-list-style.odp differ diff --git a/oox/qa/unit/export.cxx b/oox/qa/unit/export.cxx index 63351ae79e4b..30273cd2acf2 100644 --- a/oox/qa/unit/export.cxx +++ b/oox/qa/unit/export.cxx @@ -1432,6 +1432,31 @@ CPPUNIT_TEST_FIXTURE(Test, testTdf163803_ImageFill) assertXPath(pXmlDoc, "//p:pic/p:spPr/a:solidFill"); assertXPath(pXmlDoc, "//p:pic/p:spPr/a:solidFill/a:srgbClr", "val", u"000000"); } + +CPPUNIT_TEST_FIXTURE(Test, testPPTXExportOutlinerListStyle) +{ + // Given a slide with an outliner shape and a matching master slide with its own outliner shape: + loadFromFile(u"outliner-list-style.odp"); + + // When saving to PPTX: + save(u"Impress Office Open XML"_ustr); + + // Then make sure that the list style of the outliner shape on the master page is written: + xmlDocUniquePtr pXmlDoc = parseExport(u"ppt/slideMasters/slideMaster1.xml"_ustr); + assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl1pPr", 1); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 1 + // - Actual : 0 + // - XPath '//p:sp[2]/p:txBody/a:lstStyle/a:lvl2pPr' number of nodes is incorrect + // i.e. only the first list level was written, the UI couldn't format a new 2nd level paragraph + // correctly. + assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl2pPr", 1); + assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl3pPr", 1); + assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl4pPr", 1); + assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl5pPr", 1); + assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl6pPr", 1); + assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl7pPr", 1); +} } CPPUNIT_PLUGIN_IMPLEMENT(); diff --git a/oox/source/export/drawingml.cxx b/oox/source/export/drawingml.cxx index 3efaac66122d..85aa932240f6 100644 --- a/oox/source/export/drawingml.cxx +++ b/oox/source/export/drawingml.cxx @@ -137,6 +137,7 @@ #include <svx/EnhancedCustomShape2d.hxx> #include <drawingml/presetgeometrynames.hxx> #include <docmodel/uno/UnoGradientTools.hxx> +#include <svx/svdpage.hxx> using namespace ::css; using namespace ::css::beans; @@ -3648,15 +3649,31 @@ bool DrawingML::WriteParagraphProperties(const Reference<XTextContent>& rParagra WriteParagraphTabStops( rXPropSet ); // do not end element for lstStyles since, defRPr should be stacked inside it - if( nElement != XML_lvl1pPr ) + bool bLstStyle = false; + switch (nElement) + { + case XML_lvl1pPr: + case XML_lvl2pPr: + case XML_lvl3pPr: + case XML_lvl4pPr: + case XML_lvl5pPr: + case XML_lvl6pPr: + case XML_lvl7pPr: + case XML_lvl8pPr: + case XML_lvl9pPr: + bLstStyle = true; + break; + } + if( !bLstStyle ) mpFS->endElementNS( XML_a, nElement ); return true; } -void DrawingML::WriteLstStyles(const css::uno::Reference<css::text::XTextContent>& rParagraph, +void DrawingML::WriteLstStyle(const css::uno::Reference<css::text::XTextContent>& rParagraph, bool& rbOverridingCharHeight, sal_Int32& rnCharHeight, - const css::uno::Reference<css::beans::XPropertySet>& rXShapePropSet) + const css::uno::Reference<css::beans::XPropertySet>& rXShapePropSet, + sal_Int32 nElement) { Reference<XEnumerationAccess> xAccess(rParagraph, UNO_QUERY); if (!xAccess.is()) @@ -3683,14 +3700,110 @@ void DrawingML::WriteLstStyles(const css::uno::Reference<css::text::XTextContent if (xFirstRunPropSetInfo->hasPropertyByName(u"CharHeight"_ustr)) fFirstCharHeight = xFirstRunPropSet->getPropertyValue(u"CharHeight"_ustr).get<float>(); - mpFS->startElementNS(XML_a, XML_lstStyle); - if( !WriteParagraphProperties(rParagraph, fFirstCharHeight, XML_lvl1pPr) ) - mpFS->startElementNS(XML_a, XML_lvl1pPr); + if( !WriteParagraphProperties(rParagraph, fFirstCharHeight, nElement) ) + mpFS->startElementNS(XML_a, nElement); WriteRunProperties(xFirstRunPropSet, false, XML_defRPr, true, rbOverridingCharHeight, rnCharHeight, GetScriptType(rRun->getString()), rXShapePropSet); - mpFS->endElementNS(XML_a, XML_lvl1pPr); - mpFS->endElementNS(XML_a, XML_lstStyle); + mpFS->endElementNS(XML_a, nElement); + } +} + +namespace +{ +/// In case xShapeProps is an outliner shape, return a paragraph enumeration describing the outline +/// text format. +uno::Reference<container::XEnumeration> GetOutlinerTextFormatParaEnum(const uno::Reference<beans::XPropertySet>& xShapeProps) +{ + SdrObject* pShape = SdrObject::getSdrObjectFromXShape(xShapeProps); + if (pShape->GetObjIdentifier() != SdrObjKind::OutlineText) + { + // Not an outliner shape. + return {}; + } + + SdrPage* pPage = pShape->getSdrPageFromSdrObject(); + if (pPage->TRG_HasMasterPage()) + { + // Not a master page, the matching master page contains the shape that provides the outline + // text format. + SdrPage& rMasterPage = pPage->TRG_GetMasterPage(); + for (const rtl::Reference<SdrObject>& pObject : rMasterPage) + { + if (pObject->GetObjIdentifier() == SdrObjKind::OutlineText) + { + pShape = pObject.get(); + break; + } + } } + + uno::Reference<text::XTextRange> xOutliner(pShape->getUnoShape(), uno::UNO_QUERY); + uno::Reference<container::XEnumerationAccess> xText(xOutliner->getText(), uno::UNO_QUERY); + return xText->createEnumeration(); +} +} + +void DrawingML::WriteLstStyles(const css::uno::Reference<css::text::XTextContent>& rParagraph, + bool& rbOverridingCharHeight, sal_Int32& rnCharHeight, + const css::uno::Reference<css::beans::XPropertySet>& rXShapePropSet) +{ + mpFS->startElementNS(XML_a, XML_lstStyle); + + uno::Reference<container::XEnumeration> xMasterParagraphEnum = GetOutlinerTextFormatParaEnum(rXShapePropSet); + if (xMasterParagraphEnum.is()) + { + // Outliner shape, write the outline text format as multiple list levels, each with its + // paragraph properties. + sal_Int32 nLevel = 0; + while (xMasterParagraphEnum->hasMoreElements()) + { + uno::Reference<css::text::XTextContent> xParagraph(xMasterParagraphEnum->nextElement(), uno::UNO_QUERY); + sal_Int32 nElement = 0; + switch (nLevel) + { + case 0: + nElement = XML_lvl1pPr; + break; + case 1: + nElement = XML_lvl2pPr; + break; + case 2: + nElement = XML_lvl3pPr; + break; + case 3: + nElement = XML_lvl4pPr; + break; + case 4: + nElement = XML_lvl5pPr; + break; + case 5: + nElement = XML_lvl6pPr; + break; + case 6: + nElement = XML_lvl7pPr; + break; + case 7: + nElement = XML_lvl8pPr; + break; + case 8: + nElement = XML_lvl9pPr; + break; + } + if (!nElement) + { + break; + } + + WriteLstStyle(xParagraph, rbOverridingCharHeight, rnCharHeight, rXShapePropSet, nElement); + ++nLevel; + } + } + else + { + WriteLstStyle(rParagraph, rbOverridingCharHeight, rnCharHeight, rXShapePropSet, XML_lvl1pPr); + } + + mpFS->endElementNS(XML_a, XML_lstStyle); } void DrawingML::WriteParagraph( const Reference< XTextContent >& rParagraph,
