sw/qa/extras/ooxmlexport/data/floattable-anchorpos.docx |binary
 sw/qa/extras/ooxmlexport/ooxmlexport25.cxx              |   24 +++++
 sw/source/filter/ww8/docxattributeoutput.cxx            |   41 +++++++--
 sw/source/filter/ww8/docxexport.cxx                     |   70 ++++++++++++++++
 sw/source/filter/ww8/docxexport.hxx                     |    8 +
 sw/source/filter/ww8/wrtw8nds.cxx                       |    6 +
 sw/source/filter/ww8/wrtww8.hxx                         |    2 
 7 files changed, 141 insertions(+), 10 deletions(-)

New commits:
commit c9851022d102a2abfc16c033c0249f24573300e7
Author:     Miklos Vajna <vmik...@collabora.com>
AuthorDate: Thu Jul 10 14:52:18 2025 +0200
Commit:     Miklos Vajna <vmik...@collabora.com>
CommitDate: Fri Jul 11 20:12:29 2025 +0200

    tdf#167379 sw floattable: ignore dummy anchor nodes in DOCX export
    
    Open the bugdoc in Writer, looks OK, save to DOCX, open in Word: an
    unexpected paragraph appears between the two floating tables.
    
    What happens is that the DOCX import inserts dummy anchor paragraphs
    between floating tables, so we can maintain the invariant that each text
    node has at most one floating table anchored to it (which simplifies
    layout code), but then these dummy text nodes are not filtered out on
    the export side.
    
    Fix the problem by scanning the nodes array for such dummy nodes once at
    the start of the exporter, omitting such dummy nodes from the export
    result and write the affected floating tables when the next text node is
    written in the output.
    
    An alternative I considered is to leave
    MSWordExportBase::OutputContentNode() unchanged and just make some of
    the m_pSerializer calls conditional, so the dummy anchor node is missing
    from the export result. The problem is that it needed ~12 conditions and
    it would be easy to change the code in the future in a way that some
    part of the dummy node markup would be still emitted. So instead skip
    the entire text node and write the table with the next node.
    
    Change-Id: Ic15342d431a5c1e3086dc2029df6455ad675e13e
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/187696
    Reviewed-by: Miklos Vajna <vmik...@collabora.com>
    Tested-by: Jenkins

diff --git a/sw/qa/extras/ooxmlexport/data/floattable-anchorpos.docx 
b/sw/qa/extras/ooxmlexport/data/floattable-anchorpos.docx
new file mode 100644
index 000000000000..d93e5772aa3f
Binary files /dev/null and 
b/sw/qa/extras/ooxmlexport/data/floattable-anchorpos.docx differ
diff --git a/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx 
b/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx
index d9faf3a7287d..637d8a17773f 100644
--- a/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx
+++ b/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx
@@ -171,6 +171,30 @@ CPPUNIT_TEST_FIXTURE(Test, testTdf167082)
     CPPUNIT_ASSERT_EQUAL(OUString("Heading 1"), aStyleName);
 }
 
+CPPUNIT_TEST_FIXTURE(Test, testFloatingTableAnchorPosExport)
+{
+    // Given a document with two floating tables after each other:
+    // When saving that document to DOCX:
+    loadAndSave("floattable-anchorpos.docx");
+
+    // Then make sure that the dummy anchor of the first floating table is not 
written to the export
+    // result:
+    xmlDocUniquePtr pXmlDoc = parseExport(u"word/document.xml"_ustr);
+    // Check the order of the floating tables: C is from the previous node, A 
is normal floating
+    // table.
+    CPPUNIT_ASSERT_EQUAL(u"C"_ustr,
+                         getXPathContent(pXmlDoc, 
"//w:body/w:tbl[1]/w:tr/w:tc/w:p/w:r/w:t"));
+    CPPUNIT_ASSERT_EQUAL(u"A"_ustr,
+                         getXPathContent(pXmlDoc, 
"//w:body/w:tbl[2]/w:tr/w:tc/w:p/w:r/w:t"));
+    // Without the accompanying fix in place, this test would have failed with:
+    // - Expected: 1
+    // - Actual  : 2
+    // i.e. the dummy anchor node was written to DOCX, leading to a Writer vs 
Word layout
+    // difference.
+    CPPUNIT_ASSERT_EQUAL(1, countXPathNodes(pXmlDoc, "//w:body/w:p"));
+    CPPUNIT_ASSERT_EQUAL(u"D"_ustr, getXPathContent(pXmlDoc, 
"//w:body/w:p/w:r/w:t"));
+}
+
 } // end of anonymous namespace
 CPPUNIT_PLUGIN_IMPLEMENT();
 
diff --git a/sw/source/filter/ww8/docxattributeoutput.cxx 
b/sw/source/filter/ww8/docxattributeoutput.cxx
index d6851b19903d..2d241e125e85 100644
--- a/sw/source/filter/ww8/docxattributeoutput.cxx
+++ b/sw/source/filter/ww8/docxattributeoutput.cxx
@@ -460,6 +460,7 @@ static void 
checkAndWriteFloatingTables(DocxAttributeOutput& rDocxAttributeOutpu
 {
     const auto& rExport = rDocxAttributeOutput.GetExport();
     // iterate though all SpzFrameFormats and check whether they are anchored 
to the current text node
+    std::vector<ww8::Frame> aFrames;
     for( sal_uInt16 nCnt = rExport.m_rDoc.GetSpzFrameFormats()->size(); nCnt; )
     {
         const SwFrameFormat* pFrameFormat = 
(*rExport.m_rDoc.GetSpzFrameFormats())[ --nCnt ];
@@ -469,7 +470,22 @@ static void 
checkAndWriteFloatingTables(DocxAttributeOutput& rDocxAttributeOutpu
         if (!pAnchorNode || ! rExport.m_pCurPam->GetPointNode().GetTextNode())
             continue;
 
-        if (*pAnchorNode != *rExport.m_pCurPam->GetPointNode().GetTextNode())
+        bool bAnchorMatchesNode = *pAnchorNode == 
*rExport.m_pCurPam->GetPointNode().GetTextNode();
+        bool bAnchorIsPreviousNode = false;
+        if (!bAnchorMatchesNode)
+        {
+            // The anchor doesn't match, but see if the previous node is a 
dummy anchor, we should
+            // emit floating tables to that anchor here, too.
+            SwNodeIndex aNodeIndex(rExport.m_pCurPam->GetPointNode());
+            --aNodeIndex;
+            if (*pAnchorNode == aNodeIndex.GetNode() && 
rExport.IsDummyFloattableAnchor(aNodeIndex.GetNode()))
+            {
+                bAnchorMatchesNode = true;
+                bAnchorIsPreviousNode = true;
+            }
+        }
+
+        if (!bAnchorMatchesNode)
             continue;
 
         const SwNodeIndex* pStartNode = 
pFrameFormat->GetContent().GetContentIdx();
@@ -500,19 +516,26 @@ static void 
checkAndWriteFloatingTables(DocxAttributeOutput& rDocxAttributeOutpu
         const SfxGrabBagItem* pTableGrabBag = 
pTableFormat->GetAttrSet().GetItem<SfxGrabBagItem>(RES_FRMATR_GRABBAG);
         const std::map<OUString, css::uno::Any> & rTableGrabBag = 
pTableGrabBag->GetGrabBag();
         // no grabbag?
-        if (rTableGrabBag.find(u"TablePosition"_ustr) == rTableGrabBag.end())
+        if (rTableGrabBag.find(u"TablePosition"_ustr) == rTableGrabBag.end() 
&& !pFrameFormat->GetFlySplit().GetValue())
         {
-            if (pFrameFormat->GetFlySplit().GetValue())
-            {
-                ww8::Frame aFrame(*pFrameFormat, *rAnchor.GetContentAnchor());
-                rDocxAttributeOutput.WriteFloatingTable(&aFrame);
-            }
             continue;
         }
 
-        // write table to docx
+        // write table to docx: first tables from previous node, then from 
this node.
         ww8::Frame aFrame(*pFrameFormat, *rAnchor.GetContentAnchor());
-        rDocxAttributeOutput.WriteFloatingTable(&aFrame);
+        if (bAnchorIsPreviousNode)
+        {
+            aFrames.insert(aFrames.begin(), aFrame);
+        }
+        else
+        {
+            aFrames.push_back(aFrame);
+        }
+    }
+
+    for (const auto& rFrame : aFrames)
+    {
+        rDocxAttributeOutput.WriteFloatingTable(&rFrame);
     }
 }
 
diff --git a/sw/source/filter/ww8/docxexport.cxx 
b/sw/source/filter/ww8/docxexport.cxx
index aa509e7f80e6..4ee0efdf2aea 100644
--- a/sw/source/filter/ww8/docxexport.cxx
+++ b/sw/source/filter/ww8/docxexport.cxx
@@ -102,6 +102,8 @@
 #include <unotools/ucbstreamhelper.hxx>
 #include <comphelper/diagnose_ex.hxx>
 #include <unotxdoc.hxx>
+#include <formatflysplit.hxx>
+#include <fmtanchr.hxx>
 
 using namespace sax_fastparser;
 using namespace ::comphelper;
@@ -525,6 +527,67 @@ void DocxExport::OutputDML(uno::Reference<drawing::XShape> 
const & xShape)
     aExport.WriteShape(xShape);
 }
 
+void DocxExport::CollectFloatingTables()
+{
+    if (!m_rDoc.GetSpzFrameFormats())
+    {
+        return;
+    }
+
+    sw::FrameFormats<sw::SpzFrameFormat*>& rSpzFormats = 
*m_rDoc.GetSpzFrameFormats();
+    for (sw::SpzFrameFormat* pFormat : rSpzFormats)
+    {
+        const SwFormatFlySplit& rFlySplit = pFormat->GetFlySplit();
+        if (!rFlySplit.GetValue())
+        {
+            continue;
+        }
+
+        const SwFormatAnchor& rAnchor = pFormat->GetAnchor();
+        const SwPosition* pContentAnchor = rAnchor.GetContentAnchor();
+        if (!pContentAnchor)
+        {
+            continue;
+        }
+
+        SwNode& rNode = pContentAnchor->GetNode();
+        SwTextNode* pTextNode = rNode.GetTextNode();
+        if (!pTextNode)
+        {
+            continue;
+        }
+
+        SwNodeIndex aNodeIndex(*pTextNode);
+        ++aNodeIndex;
+        if (!aNodeIndex.GetNode().GetTextNode())
+        {
+            // Only text nodes know to look for floating tables from previous 
text nodes.
+            continue;
+        }
+
+        if (!pTextNode->HasSwAttrSet())
+        {
+            continue;
+        }
+
+        const SwAttrSet& rAttrSet = pTextNode->GetSwAttrSet();
+        const SvxLineSpacingItem& rLineSpacing = rAttrSet.GetLineSpacing();
+        if (rLineSpacing.GetLineSpaceRule() != SvxLineSpaceRule::Fix)
+        {
+            continue;
+        }
+
+        if (rLineSpacing.GetLineHeight() != 0)
+        {
+            continue;
+        }
+
+        // This is text node which is effectively invisible in Writer and has 
a floating table
+        // anchored to it; omit such nodes from the DOCX output.
+        m_aDummyFloatingTableAnchors.insert(&pContentAnchor->GetNode());
+    }
+}
+
 ErrCode DocxExport::ExportDocument_Impl()
 {
     // Set the 'Reviewing' flags in the settings structure
@@ -541,6 +604,8 @@ ErrCode DocxExport::ExportDocument_Impl()
     // Make sure images are counted from one, even when exporting multiple 
documents.
     rGraphicExportCache.push();
 
+    CollectFloatingTables();
+
     WriteMainText();
 
     WriteFootnotesEndnotes();
@@ -1903,6 +1968,11 @@ bool DocxExport::isMirroredMargin()
     return bMirroredMargins;
 }
 
+bool DocxExport::IsDummyFloattableAnchor(SwNode& rNode) const
+{
+    return GetDummyFloatingTableAnchors().contains(&rNode);
+}
+
 void DocxExport::WriteDocumentBackgroundFill()
 {
     const std::unique_ptr<SvxBrushItem> pBrush = getBackground();
diff --git a/sw/source/filter/ww8/docxexport.hxx 
b/sw/source/filter/ww8/docxexport.hxx
index 929544e41fdf..6f092664ad1a 100644
--- a/sw/source/filter/ww8/docxexport.hxx
+++ b/sw/source/filter/ww8/docxexport.hxx
@@ -124,6 +124,8 @@ class DocxExport : public MSWordExportBase
     /// Storage for sdt data which need to be written to other XMLs
     std::vector<SdtData> m_SdtData;
 
+    std::set<SwNode*> m_aDummyFloatingTableAnchors;
+
 public:
 
     DocxExportFilter& GetFilter() { return m_rFilter; };
@@ -249,6 +251,8 @@ private:
     /// Write comments.xml
     void WritePostitFields();
 
+    void CollectFloatingTables();
+
     /// Write the numbering table.
     virtual void WriteNumbering() override;
 
@@ -320,6 +324,10 @@ public:
     /// return true if Page Layout is set as Mirrored
     bool isMirroredMargin();
 
+    const std::set<SwNode*>& GetDummyFloatingTableAnchors() const { return 
m_aDummyFloatingTableAnchors; }
+
+    bool IsDummyFloattableAnchor(SwNode& rNode) const override;
+
 private:
     DocxExport( const DocxExport& ) = delete;
 
diff --git a/sw/source/filter/ww8/wrtw8nds.cxx 
b/sw/source/filter/ww8/wrtw8nds.cxx
index 2d25e41c8ae6..ce860f1cf2c1 100644
--- a/sw/source/filter/ww8/wrtw8nds.cxx
+++ b/sw/source/filter/ww8/wrtw8nds.cxx
@@ -3757,7 +3757,11 @@ void MSWordExportBase::OutputContentNode( SwContentNode& 
rNode )
     switch ( rNode.GetNodeType() )
     {
         case SwNodeType::Text:
-            OutputTextNode( *rNode.GetTextNode() );
+            // Skip dummy anchors: the next node will emit their floating 
tables.
+            if (!IsDummyFloattableAnchor(*rNode.GetTextNode()))
+            {
+                OutputTextNode(*rNode.GetTextNode());
+            }
             break;
         case SwNodeType::Grf:
             OutputGrfNode( *rNode.GetGrfNode() );
diff --git a/sw/source/filter/ww8/wrtww8.hxx b/sw/source/filter/ww8/wrtww8.hxx
index 6097ac87c5ae..0bd4a29db14c 100644
--- a/sw/source/filter/ww8/wrtww8.hxx
+++ b/sw/source/filter/ww8/wrtww8.hxx
@@ -920,6 +920,8 @@ protected:
 
     std::vector<const Graphic*> m_vecBulletPic; ///< Vector to record all the 
graphics of bullets
 
+    virtual bool IsDummyFloattableAnchor(SwNode& /*rNode*/) const { return 
false; }
+
 public:
     MSWordExportBase(SwDoc& rDocument, std::shared_ptr<SwUnoCursor> & 
pCurrentPam, SwPaM* pOriginalPam);
     virtual ~MSWordExportBase();

Reply via email to