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();