sw/qa/extras/ooxmlexport/data/mixednumberings.docx |binary
 sw/qa/extras/ooxmlexport/ooxmlexport13.cxx         |   15 ------
 sw/qa/extras/ooxmlexport/ooxmlexport25.cxx         |   13 +++++
 sw/source/filter/ww8/docxattributeoutput.cxx       |   35 --------------
 sw/source/filter/ww8/wrtw8num.cxx                  |    7 +-
 sw/source/filter/ww8/ww8atr.cxx                    |    9 ++-
 sw/source/filter/ww8/ww8par.cxx                    |   51 +++++++++++++++++++++
 sw/source/filter/ww8/ww8par.hxx                    |    2 
 8 files changed, 78 insertions(+), 54 deletions(-)

New commits:
commit fed5426a3a0058f630a7c418bb1ba0cb95caadd8
Author:     Jaume Pujantell <[email protected]>
AuthorDate: Wed Oct 29 20:03:24 2025 +0100
Commit:     Miklos Vajna <[email protected]>
CommitDate: Mon Nov 3 08:39:50 2025 +0100

    sw: reduce duplication of w:num items
    
    When saving to .docx format we create new w:num entries for lists even
    if equivalent entries already exist. This creates a lot of bloat in big
    documents that have heavy use of numbered lists. A document that can be
    saved with ~150 num entries can end up with ~2000. This causes
    compatibility issues with other software that doesn't expect such high
    number of numbering styles.
    
    Change-Id: Ie8667c0a6d98a77c7c429a9a2925d31ee19fd31c
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/193164
    Reviewed-by: Miklos Vajna <[email protected]>
    Tested-by: Jenkins CollaboraOffice <[email protected]>
    (cherry picked from commit aa0c3d7eab9a611f915b4262b37d34860f527bc7)
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/193248
    Reviewed-by: Jaume Pujantell <[email protected]>
    Tested-by: Jenkins
    Signed-off-by: Xisco Fauli <[email protected]>
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/193278

diff --git a/sw/qa/extras/ooxmlexport/data/mixednumberings.docx 
b/sw/qa/extras/ooxmlexport/data/mixednumberings.docx
new file mode 100644
index 000000000000..fbbe7dc50ee0
Binary files /dev/null and b/sw/qa/extras/ooxmlexport/data/mixednumberings.docx 
differ
diff --git a/sw/qa/extras/ooxmlexport/ooxmlexport13.cxx 
b/sw/qa/extras/ooxmlexport/ooxmlexport13.cxx
index 0b6459f9f5d2..f6871744f6ab 100644
--- a/sw/qa/extras/ooxmlexport/ooxmlexport13.cxx
+++ b/sw/qa/extras/ooxmlexport/ooxmlexport13.cxx
@@ -184,11 +184,6 @@ DECLARE_OOXMLEXPORT_TEST(testTdf95848, "tdf95848.docx")
     {
         uno::Reference<beans::XPropertySet> xPara(getParagraph(3), 
uno::UNO_QUERY);
         CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int16>(2), 
getProperty<sal_Int16>(xPara, u"NumberingLevel"_ustr));
-        // different numbering style
-        OUString listStyle3;
-        CPPUNIT_ASSERT(xPara->getPropertyValue(u"NumberingStyleName"_ustr) >>= 
listStyle3);
-        CPPUNIT_ASSERT(listStyle3.startsWith("WWNum"));
-        CPPUNIT_ASSERT(listStyle3 != listStyle);
         // but same list
         CPPUNIT_ASSERT_EQUAL(u"1.1.3"_ustr, getProperty<OUString>(xPara, 
u"ListLabelString"_ustr));
         CPPUNIT_ASSERT_EQUAL(listId, getProperty<OUString>(xPara, 
u"ListId"_ustr));
@@ -280,11 +275,6 @@ DECLARE_OOXMLEXPORT_TEST(testTdf108496, "tdf108496.docx")
     }
     {
         uno::Reference<beans::XPropertySet> xPara(getParagraph(5), 
uno::UNO_QUERY);
-        // different numbering style
-        OUString listStyle2;
-        CPPUNIT_ASSERT(xPara->getPropertyValue(u"NumberingStyleName"_ustr) >>= 
listStyle2);
-        CPPUNIT_ASSERT(listStyle2.startsWith("WWNum"));
-        CPPUNIT_ASSERT(listStyle2 != listStyle);
         // restarted numeration due to override
         CPPUNIT_ASSERT_EQUAL(u"1"_ustr, getProperty<OUString>(xPara, 
u"ListLabelString"_ustr));
         CPPUNIT_ASSERT_EQUAL(listId, getProperty<OUString>(xPara, 
u"ListId"_ustr));
@@ -316,11 +306,6 @@ DECLARE_OOXMLEXPORT_TEST(testTdf108496, "tdf108496.docx")
     }
     {
         uno::Reference<beans::XPropertySet> xPara(getParagraph(11), 
uno::UNO_QUERY);
-        // different numbering style
-        OUString listStyle2;
-        CPPUNIT_ASSERT(xPara->getPropertyValue(u"NumberingStyleName"_ustr) >>= 
listStyle2);
-        CPPUNIT_ASSERT(listStyle2.startsWith("WWNum"));
-        CPPUNIT_ASSERT(listStyle2 != listStyle);
         // numeration is continued
         CPPUNIT_ASSERT_EQUAL(u"3"_ustr, getProperty<OUString>(xPara, 
u"ListLabelString"_ustr));
         CPPUNIT_ASSERT_EQUAL(listId, getProperty<OUString>(xPara, 
u"ListId"_ustr));
diff --git a/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx 
b/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx
index 2f16a4eac671..79ccfd138041 100644
--- a/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx
+++ b/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx
@@ -201,6 +201,19 @@ CPPUNIT_TEST_FIXTURE(Test, testTdf167297)
     fnVerify();
 }
 
+CPPUNIT_TEST_FIXTURE(Test, testWNumDuplication)
+{
+    // Given a document that changes a lot between a few numbering styles and 
overrides:
+    loadAndSave("mixednumberings.docx");
+
+    // Then make sure that we export a reasonable number of "w:num" elements:
+    xmlDocUniquePtr pXmlNum = parseExport(u"word/numbering.xml"_ustr);
+    // Without the accompanying fix in place, this test would have failed with:
+    // - Expected: 10
+    // - Actual  : 61
+    CPPUNIT_ASSERT_EQUAL(10, countXPathNodes(pXmlNum, "//w:numbering/w:num"));
+}
+
 } // end of anonymous namespace
 CPPUNIT_PLUGIN_IMPLEMENT();
 
diff --git a/sw/source/filter/ww8/docxattributeoutput.cxx 
b/sw/source/filter/ww8/docxattributeoutput.cxx
index df80ef3581f4..0565a321d20b 100644
--- a/sw/source/filter/ww8/docxattributeoutput.cxx
+++ b/sw/source/filter/ww8/docxattributeoutput.cxx
@@ -7702,38 +7702,6 @@ void DocxAttributeOutput::NumberingDefinition( 
sal_uInt16 nId, const SwNumRule &
     m_pSerializer->endElementNS( XML_w, XML_num );
 }
 
-// Not all attributes of SwNumFormat are important for export, so can't just 
use embedded in
-// that classes comparison.
-static bool lcl_ListLevelsAreDifferentForExport(const SwNumFormat & rFormat1, 
const SwNumFormat & rFormat2)
-{
-    if (rFormat1 == rFormat2)
-        // They are equal, nothing to do
-        return false;
-
-    if (!rFormat1.GetCharFormat() != !rFormat2.GetCharFormat())
-        // One has charformat, other not. they are different
-        return true;
-
-    if (rFormat1.GetCharFormat() && rFormat2.GetCharFormat())
-    {
-        const SwAttrSet & a1 = rFormat1.GetCharFormat()->GetAttrSet();
-        const SwAttrSet & a2 = rFormat2.GetCharFormat()->GetAttrSet();
-
-        if (!(a1 == a2))
-            // Difference in charformat: they are different
-            return true;
-    }
-
-    // Compare numformats with empty charformats
-    SwNumFormat modified1 = rFormat1;
-    SwNumFormat modified2 = rFormat2;
-    modified1.SetCharFormatName(OUString());
-    modified2.SetCharFormatName(OUString());
-    modified1.SetCharFormat(nullptr);
-    modified2.SetCharFormat(nullptr);
-    return modified1 != modified2;
-}
-
 void DocxAttributeOutput::OverrideNumberingDefinition(
         SwNumRule const& rRule,
         sal_uInt16 const nNum, sal_uInt16 const nAbstractNum, const std::map< 
size_t, size_t > & rLevelOverrides )
@@ -7749,7 +7717,8 @@ void DocxAttributeOutput::OverrideNumberingDefinition(
     for (sal_uInt8 nLevel = 0; nLevel < nLevels; ++nLevel)
     {
         const auto levelOverride = rLevelOverrides.find(nLevel);
-        bool bListsAreDifferent = 
lcl_ListLevelsAreDifferentForExport(rRule.Get(nLevel), 
rAbstractRule.Get(nLevel));
+        bool bListsAreDifferent
+            = AreListLevelsDifferentForExport(rRule.Get(nLevel), 
rAbstractRule.Get(nLevel));
 
         // Export list override only if it is different to abstract one
         // or we have a level numbering override
diff --git a/sw/source/filter/ww8/wrtw8num.cxx 
b/sw/source/filter/ww8/wrtw8num.cxx
index 58a027b5bf4a..e72789615419 100644
--- a/sw/source/filter/ww8/wrtw8num.cxx
+++ b/sw/source/filter/ww8/wrtw8num.cxx
@@ -152,9 +152,10 @@ sal_uInt16 MSWordExportBase::GetNumberingId( const 
SwNumRule& rNumRule )
 {
     EnsureUsedNumberingTable();
     SwNumRule* p = const_cast<SwNumRule*>(&rNumRule);
-    sal_uInt16 nRet = o3tl::narrowing<sal_uInt16>(m_pUsedNumTable->GetPos(p));
-
-    return nRet;
+    size_t pos = m_pUsedNumTable->GetPos(p);
+    if (pos == SIZE_MAX)
+        return SAL_MAX_UINT16;
+    return o3tl::narrowing<sal_uInt16>(pos);
 }
 
 // GetFirstLineOffset should problem never appear unadorned apart from
diff --git a/sw/source/filter/ww8/ww8atr.cxx b/sw/source/filter/ww8/ww8atr.cxx
index 36c3f08655a1..77b531d0e14a 100644
--- a/sw/source/filter/ww8/ww8atr.cxx
+++ b/sw/source/filter/ww8/ww8atr.cxx
@@ -3933,10 +3933,13 @@ void AttributeOutputBase::ParaNumRule( const 
SwNumRuleItem& rNumRule )
                             GetExport().m_rDoc.FindNumRulePtr(
                                 pList->GetDefaultListStyleName()));
                         assert(pAbstractRule);
-                        if (pAbstractRule == pRule && !bListRestart)
+                        if (!AreListsDifferentForExport(*pAbstractRule, 
*pRule) && !bListRestart)
                         {
-                            // different list, but no override
-                            nNumId = GetExport().DuplicateAbsNum(listId, 
*pAbstractRule) + 1;
+                            nNumId = 
GetExport().GetNumberingId(*pAbstractRule);
+                            if (nNumId == SAL_MAX_UINT16)
+                                nNumId = GetExport().DuplicateAbsNum(listId, 
*pAbstractRule) + 1;
+                            else
+                                nNumId += 1;
                         }
                         else
                         {
diff --git a/sw/source/filter/ww8/ww8par.cxx b/sw/source/filter/ww8/ww8par.cxx
index 9476e055c954..1d3261b4a431 100644
--- a/sw/source/filter/ww8/ww8par.cxx
+++ b/sw/source/filter/ww8/ww8par.cxx
@@ -1231,6 +1231,57 @@ tools::Long GetListFirstLineIndent(const SwNumFormat 
&rFormat)
     return nReverseListIndented;
 }
 
+bool AreListsDifferentForExport(SwNumRule const& rRule1, SwNumRule const& 
rRule2)
+{
+    if (rRule1 == rRule2)
+        // They are equal, nothing to do
+        return false;
+
+    if (rRule1.IsContinusNum() != rRule2.IsContinusNum())
+        return true;
+
+    sal_uInt8 const nLevels = static_cast<sal_uInt8>(
+        rRule1.IsContinusNum() ? WW8ListManager::nMinLevel : 
WW8ListManager::nMaxLevel);
+    for (sal_uInt8 nLevel = 0; nLevel < nLevels; ++nLevel)
+    {
+        if (AreListLevelsDifferentForExport(rRule1.Get(nLevel), 
rRule2.Get(nLevel)))
+            return true;
+    }
+    return false;
+}
+
+// Not all attributes of SwNumFormat are important for export, so can't just 
use embedded in
+// that classes comparison.
+bool AreListLevelsDifferentForExport(SwNumFormat const& rFormat1, SwNumFormat 
const& rFormat2)
+{
+    if (rFormat1 == rFormat2)
+        // They are equal, nothing to do
+        return false;
+
+    if (!rFormat1.GetCharFormat() != !rFormat2.GetCharFormat())
+        // One has charformat, other not. they are different
+        return true;
+
+    if (rFormat1.GetCharFormat() && rFormat2.GetCharFormat())
+    {
+        const SwAttrSet& a1 = rFormat1.GetCharFormat()->GetAttrSet();
+        const SwAttrSet& a2 = rFormat2.GetCharFormat()->GetAttrSet();
+
+        if (!(a1 == a2))
+            // Difference in charformat: they are different
+            return true;
+    }
+
+    // Compare numformats with empty charformats
+    SwNumFormat modified1 = rFormat1;
+    SwNumFormat modified2 = rFormat2;
+    modified1.SetCharFormatName(OUString());
+    modified2.SetCharFormatName(OUString());
+    modified1.SetCharFormat(nullptr);
+    modified2.SetCharFormat(nullptr);
+    return modified1 != modified2;
+}
+
 static tools::Long lcl_GetTrueMargin(SvxFirstLineIndentItem const& rFirstLine,
         SvxTextLeftMarginItem const& rLeftMargin, const SwNumFormat &rFormat,
     tools::Long &rFirstLinePos)
diff --git a/sw/source/filter/ww8/ww8par.hxx b/sw/source/filter/ww8/ww8par.hxx
index 7176c6bc9dc9..caf93b45060a 100644
--- a/sw/source/filter/ww8/ww8par.hxx
+++ b/sw/source/filter/ww8/ww8par.hxx
@@ -1963,6 +1963,8 @@ void SyncIndentWithList( SvxFirstLineIndentItem & 
rFirstLine,
                          const bool bFirstLineOfStSet,
                          const bool bLeftIndentSet );
 tools::Long GetListFirstLineIndent(const SwNumFormat &rFormat);
+bool AreListsDifferentForExport(SwNumRule const& rRule1, SwNumRule const& 
rRule2);
+bool AreListLevelsDifferentForExport(SwNumFormat const& rFormat1, SwNumFormat 
const& rFormat2);
 OUString BookmarkToWriter(std::u16string_view rBookmark);
 bool RTLGraphicsHack(SwTwips &rLeft, SwTwips nWidth,
     sal_Int16 eHoriOri, sal_Int16 eHoriRel, SwTwips nPageLeft,

Reply via email to