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 066b53a22f43a134e9189284a337898cd4caa8f2 Author: Miklos Vajna <[email protected]> AuthorDate: Thu Jul 10 14:52:18 2025 +0200 Commit: Caolán McNamara <[email protected]> CommitDate: Fri Jul 11 09:37:11 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/+/187638 Tested-by: Jenkins CollaboraOffice <[email protected]> Reviewed-by: Caolán McNamara <[email protected]> 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 ebf0e1f3570f..f8e43d79c68c 100644 --- a/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx +++ b/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx @@ -138,6 +138,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 72ae0ba83790..77023858b809 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 02ce025e6abe..5b72f3e5fa2b 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(); @@ -1931,6 +1996,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 43cdccce8a5c..ad339ad3bde1 100644 --- a/sw/source/filter/ww8/docxexport.hxx +++ b/sw/source/filter/ww8/docxexport.hxx @@ -129,6 +129,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; }; @@ -254,6 +256,8 @@ private: /// Write comments.xml void WritePostitFields(); + void CollectFloatingTables(); + /// Write the numbering table. virtual void WriteNumbering() override; @@ -326,6 +330,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 4a53aaf4dd51..feedee5f0e69 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 bd116fa83f12..fa34838426f2 100644 --- a/sw/source/filter/ww8/wrtww8.hxx +++ b/sw/source/filter/ww8/wrtww8.hxx @@ -930,6 +930,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();
