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 d7212eeeb2ec8a2072a33063e81ee4b27af8f031 Author: Miklos Vajna <vmik...@collabora.com> AuthorDate: Thu Jul 10 14:52:18 2025 +0200 Commit: Adolfo Jayme Barrientos <fit...@ubuntu.com> CommitDate: Mon Jul 14 21:37:12 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 (cherry picked from commit c9851022d102a2abfc16c033c0249f24573300e7) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/187768 Reviewed-by: Adolfo Jayme Barrientos <fit...@ubuntu.com> 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 60b1aa082538..334ef92c4ead 100644 --- a/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx +++ b/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx @@ -145,6 +145,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 a50349ae4e1e..553b763af98b 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 daf484eed592..a4e80227666e 100644 --- a/sw/source/filter/ww8/docxexport.cxx +++ b/sw/source/filter/ww8/docxexport.cxx @@ -101,6 +101,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; @@ -524,6 +526,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 @@ -540,6 +603,8 @@ ErrCode DocxExport::ExportDocument_Impl() // Make sure images are counted from one, even when exporting multiple documents. rGraphicExportCache.push(); + CollectFloatingTables(); + WriteMainText(); WriteFootnotesEndnotes(); @@ -1908,6 +1973,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 36d45f197338..d8ee2bc50caf 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 97651227b652..b5796ea19db9 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 bd28580aa436..c52665f8d16b 100644 --- a/sw/source/filter/ww8/wrtww8.hxx +++ b/sw/source/filter/ww8/wrtww8.hxx @@ -919,6 +919,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();