From 4802bf32b84655f6e1d3389070c76371ede8fbce Mon Sep 17 00:00:00 2001 From: Justin Luth Date: Wed, 24 Jun 2015 23:02:57 +0300 Subject: tdf#87348 enable docx exporting linked textboxes that LO can import Exporting linked textboxes to docx format is in terrible condition. Spacing, textboxes instead of frames, duplicate links and orphaned shapes lying around, not to mention being being unlinked in MSWord... This fix resolves this situation slightly: what LO saves can be re-imported and re-saved without breaking the links - an incremental improvement. Change-Id: I8f0aef39eeed88a2b3dfc673a565fb1d8f4713b0 Reviewed-on: https://gerrit.libreoffice.org/16516 Tested-by: Jenkins Reviewed-by: Miklos Vajna --- .../ooxmlexport/data/tdf87348_linkedTextboxes.docx | Bin 0 -> 18697 bytes sw/qa/extras/ooxmlexport/ooxmlexport7.cxx | 64 +++++++++++++++ sw/source/filter/ww8/docxexport.cxx | 8 ++ sw/source/filter/ww8/docxsdrexport.cxx | 91 ++++++++++++++------- sw/source/filter/ww8/wrtw8nds.cxx | 63 ++++++++++++++ sw/source/filter/ww8/wrtww8.cxx | 3 + sw/source/filter/ww8/wrtww8.hxx | 12 +++ 7 files changed, 213 insertions(+), 28 deletions(-) create mode 100644 sw/qa/extras/ooxmlexport/data/tdf87348_linkedTextboxes.docx diff --git a/sw/qa/extras/ooxmlexport/data/tdf87348_linkedTextboxes.docx b/sw/qa/extras/ooxmlexport/data/tdf87348_linkedTextboxes.docx new file mode 100644 index 000000000000..8f9eb97d83f4 Binary files /dev/null and b/sw/qa/extras/ooxmlexport/data/tdf87348_linkedTextboxes.docx differ diff --git a/sw/qa/extras/ooxmlexport/ooxmlexport7.cxx b/sw/qa/extras/ooxmlexport/ooxmlexport7.cxx index ac7ba3c6bb3f..6354f422d9e1 100644 --- a/sw/qa/extras/ooxmlexport/ooxmlexport7.cxx +++ b/sw/qa/extras/ooxmlexport/ooxmlexport7.cxx @@ -999,6 +999,70 @@ DECLARE_OOXMLEXPORT_TEST(testExportAdjustmentValue, "tdf91429.docx") assertXPath(pXmlDoc,"/w:document/w:body/w:p/w:r[1]/mc:AlternateContent/mc:Choice/w:drawing/wp:anchor/a:graphic/a:graphicData/wps:wsp/wps:spPr/a:prstGeom/a:avLst/a:gd", "fmla", "val 50000"); } + +DECLARE_OOXMLEXPORT_TEST(testTDF87348, "tdf87348_linkedTextboxes.docx") +{ +int followCount=0; +int precedeCount=0; +if( !parseDump("/root/page/body/txt/anchored/fly[1]/txt","follow").isEmpty() ) + followCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[1]/txt","precede").isEmpty() ) + precedeCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[2]/txt","follow").isEmpty() ) + followCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[2]/txt","precede").isEmpty() ) + precedeCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[3]/txt","follow").isEmpty() ) + followCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[3]/txt","precede").isEmpty() ) + precedeCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[4]/txt","follow").isEmpty() ) + followCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[4]/txt","precede").isEmpty() ) + precedeCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[5]/txt","follow").isEmpty() ) + followCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[5]/txt","precede").isEmpty() ) + precedeCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[6]/txt","follow").isEmpty() ) + followCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[6]/txt","precede").isEmpty() ) + precedeCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[7]/txt","follow").isEmpty() ) + followCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[7]/txt","precede").isEmpty() ) + precedeCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[8]/txt","follow").isEmpty() ) + followCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[8]/txt","precede").isEmpty() ) + precedeCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[9]/txt","follow").isEmpty() ) + followCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[9]/txt","precede").isEmpty() ) + precedeCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[10]/txt","follow").isEmpty() ) + followCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[10]/txt","precede").isEmpty() ) + precedeCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[11]/txt","follow").isEmpty() ) + followCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[11]/txt","precede").isEmpty() ) + precedeCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[12]/txt","follow").isEmpty() ) + followCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[12]/txt","precede").isEmpty() ) + precedeCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[13]/txt","follow").isEmpty() ) + followCount++; +if( !parseDump("/root/page/body/txt/anchored/fly[13]/txt","precede").isEmpty() ) + precedeCount++; + //there should be 4 chains/13 linked textboxes (set of 5, set of 3, set of 3, set of 2) + //that means 9 NEXT links and 9 PREV links. + //however, the current implementation adds leftover shapes, so can't go on exact numbers + // (unknown number of flys, unknown order of leftovers) + CPPUNIT_ASSERT ( (followCount >= 6) && (precedeCount >= 6) ); +} + #endif CPPUNIT_PLUGIN_IMPLEMENT(); diff --git a/sw/source/filter/ww8/docxexport.cxx b/sw/source/filter/ww8/docxexport.cxx index e91bea85b15f..de23a27ee345 100644 --- a/sw/source/filter/ww8/docxexport.cxx +++ b/sw/source/filter/ww8/docxexport.cxx @@ -453,6 +453,7 @@ void DocxExport::ExportDocument_Impl() WriteEmbeddings(); + m_aLinkedTextboxesHelper.clear(); //final cleanup delete m_pStyles, m_pStyles = NULL; delete m_pSections, m_pSections = NULL; } @@ -1314,6 +1315,10 @@ void DocxExport::WriteMainText() // setup the namespaces m_pDocumentFS->startElementNS( XML_w, XML_document, MainXmlNamespaces()); + // reset the incrementing linked-textboxes chain ID before re-saving. + m_nLinkedTextboxesChainId=0; + m_aLinkedTextboxesHelper.clear(); + // Write background page color if (boost::optional oBrush = getBackground()) { @@ -1331,6 +1336,9 @@ void DocxExport::WriteMainText() // the text WriteText(); + // clear linked textboxes since old ones can't be linked to frames in a different section (correct?) + m_aLinkedTextboxesHelper.clear(); + // the last section info m_pAttrOutput->EndParaSdtBlock(); const WW8_SepInfo *pSectionInfo = m_pSections? m_pSections->CurrentSectionInfo(): NULL; diff --git a/sw/source/filter/ww8/docxsdrexport.cxx b/sw/source/filter/ww8/docxsdrexport.cxx index 44d9ebb831fe..b59de527e7b0 100644 --- a/sw/source/filter/ww8/docxsdrexport.cxx +++ b/sw/source/filter/ww8/docxsdrexport.cxx @@ -143,8 +143,6 @@ struct DocxSdrExport::Impl sax_fastparser::FastAttributeList* m_pFlyWrapAttrList; sax_fastparser::FastAttributeList* m_pBodyPrAttrList; std::unique_ptr m_pDashLineStyleAttr; - sal_Int32 m_nId ; - sal_Int32 m_nSeq ; bool m_bDMLAndVMLDrawingOpen; /// List of TextBoxes in this document: they are exported as part of their shape, never alone. std::set m_aTextBoxes; @@ -166,8 +164,6 @@ struct DocxSdrExport::Impl m_bFlyFrameGraphic(false), m_pFlyWrapAttrList(0), m_pBodyPrAttrList(0), - m_nId(0), - m_nSeq(0), m_bDMLAndVMLDrawingOpen(false), m_aTextBoxes(SwTextBoxHelper::findTextBoxes(m_rExport.m_pDoc)), m_nDMLandVMLTextFrameRotation(0) @@ -1475,48 +1471,87 @@ void DocxSdrExport::writeDMLTextFrame(sw::Frame* pParentFrame, int nAnchorId, bo pFS->endElementNS(XML_wps, XML_spPr); } + //first, loop through ALL of the chained textboxes to identify a unique ID for each chain, and sequence number for each textbox in that chain. + std::map::iterator linkedTextboxesIter; + if( !m_pImpl->m_rExport.m_bLinkedTextboxesHelperInitialized ) + { + sal_Int32 nSeq=0; + linkedTextboxesIter = m_pImpl->m_rExport.m_aLinkedTextboxesHelper.begin(); + while ( linkedTextboxesIter != m_pImpl->m_rExport.m_aLinkedTextboxesHelper.end() ) + { + //find the start of a textbox chain: has no PREVIOUS link, but does have NEXT link + if ( linkedTextboxesIter->second.sPrevChain.isEmpty() && !linkedTextboxesIter->second.sNextChain.isEmpty() ) + { + //assign this chain a unique ID and start a new sequence + nSeq = 0; + linkedTextboxesIter->second.nId = ++m_pImpl->m_rExport.m_nLinkedTextboxesChainId; + linkedTextboxesIter->second.nSeq = nSeq; + + OUString sCheckForBrokenChains = linkedTextboxesIter->first; + + //follow the chain and assign the same id, and incremental sequence numbers. + std::map::iterator followChainIter; + followChainIter = m_pImpl->m_rExport.m_aLinkedTextboxesHelper.find(linkedTextboxesIter->second.sNextChain); + while ( followChainIter != m_pImpl->m_rExport.m_aLinkedTextboxesHelper.end() ) + { + //verify that the NEXT textbox also points to me as the PREVIOUS. + // A broken link indicates a leftover remnant that can be ignored. + if( followChainIter->second.sPrevChain != sCheckForBrokenChains ) + break; + + followChainIter->second.nId = m_pImpl->m_rExport.m_nLinkedTextboxesChainId; + followChainIter->second.nSeq = ++nSeq; + + //empty next chain indicates the end of the linked chain. + if ( followChainIter->second.sNextChain.isEmpty() ) + break; + + sCheckForBrokenChains = followChainIter->first; + followChainIter = m_pImpl->m_rExport.m_aLinkedTextboxesHelper.find(followChainIter->second.sNextChain); + } + } + ++linkedTextboxesIter; + } + m_pImpl->m_rExport.m_bLinkedTextboxesHelperInitialized = true; + } + m_pImpl->m_rExport.m_pParentFrame = NULL; bool skipTxBxContent = false ; bool isTxbxLinked = false ; - /* Check if the text box is linked and then decides whether - to write the tag txbx or linkedTxbx - */ - if (xPropSetInfo.is() && xPropSetInfo->hasPropertyByName("ChainPrevName") && - xPropSetInfo->hasPropertyByName("ChainNextName")) + OUString sLinkChainName; + if ( xPropSetInfo.is() ) { - OUString sChainPrevName; - OUString sChainNextName; - - xPropertySet->getPropertyValue("ChainPrevName") >>= sChainPrevName ; - xPropertySet->getPropertyValue("ChainNextName") >>= sChainNextName ; + if ( xPropSetInfo->hasPropertyByName("LinkDisplayName") ) + xPropertySet->getPropertyValue("LinkDisplayName") >>= sLinkChainName; + else if ( xPropSetInfo->hasPropertyByName("ChainName") ) + xPropertySet->getPropertyValue("ChainName") >>= sLinkChainName; + } - if (!sChainPrevName.isEmpty()) + // second, check if THIS textbox is linked and then decide whether to write the tag txbx or linkedTxbx + linkedTextboxesIter = m_pImpl->m_rExport.m_aLinkedTextboxesHelper.find(sLinkChainName); + if ( linkedTextboxesIter != m_pImpl->m_rExport.m_aLinkedTextboxesHelper.end() ) + { + if( (linkedTextboxesIter->second.nId !=0) && (linkedTextboxesIter->second.nSeq != 0) ) { + //not the first in the chain, so write the tag as linkedTxbx + pFS->singleElementNS(XML_wps, XML_linkedTxbx, + XML_id, I32S(linkedTextboxesIter->second.nId), + XML_seq, I32S(linkedTextboxesIter->second.nSeq), + FSEND); /* no text content should be added to this tag, since the textbox is linked, the entire content is written in txbx block */ - ++m_pImpl->m_nSeq ; - pFS->singleElementNS(XML_wps, XML_linkedTxbx, - XML_id, I32S(m_pImpl->m_nId), - XML_seq, I32S(m_pImpl->m_nSeq), - FSEND); skipTxBxContent = true ; - - //Text box chaining for a group of textboxes ends here, - //therefore reset the seq. - if (sChainNextName.isEmpty()) - m_pImpl->m_nSeq = 0 ; } - else if (sChainPrevName.isEmpty() && !sChainNextName.isEmpty()) + else if( (linkedTextboxesIter->second.nId != 0) && (linkedTextboxesIter->second.nSeq == 0) ) { /* this is the first textbox in the chaining, we add the text content to this block*/ - ++m_pImpl->m_nId ; //since the text box is linked, it needs an id. pFS->startElementNS(XML_wps, XML_txbx, - XML_id, I32S(m_pImpl->m_nId), + XML_id, I32S(linkedTextboxesIter->second.nId), FSEND); isTxbxLinked = true ; } diff --git a/sw/source/filter/ww8/wrtw8nds.cxx b/sw/source/filter/ww8/wrtw8nds.cxx index 608d2b377d18..1cc28037c404 100644 --- a/sw/source/filter/ww8/wrtw8nds.cxx +++ b/sw/source/filter/ww8/wrtw8nds.cxx @@ -549,6 +549,69 @@ bool SwWW8AttrIter::IsAnchorLinkedToThisNode( sal_uLong nNodePos ) FlyProcessingState SwWW8AttrIter::OutFlys(sal_Int32 nSwPos) { + // collection point to first gather info about all of the potentially linked textboxes: to be analyzed later. + OUString sLinkChainName; + sw::FrameIter linkedTextboxesIter = maFlyIter; + while ( linkedTextboxesIter != maFlyFrms.end() ) + { + uno::Reference< drawing::XShape > xShape; + sw::Frame xFrame = *linkedTextboxesIter; + const SdrObject* pSdrObj = xFrame.GetFrameFormat().FindRealSdrObject(); + if( pSdrObj ) + xShape = uno::Reference< drawing::XShape >(const_cast(pSdrObj)->getUnoShape(), uno::UNO_QUERY); + uno::Reference< beans::XPropertySet > xPropertySet(xShape, uno::UNO_QUERY); + uno::Reference< beans::XPropertySetInfo > xPropertySetInfo; + if( xPropertySet.is() ) + xPropertySetInfo = xPropertySet->getPropertySetInfo(); + if( xPropertySetInfo.is() ) + { + MSWordExportBase::LinkedTextboxInfo aLinkedTextboxInfo = MSWordExportBase::LinkedTextboxInfo(); + + if( xPropertySetInfo->hasPropertyByName("LinkDisplayName") ) + xPropertySet->getPropertyValue("LinkDisplayName") >>= sLinkChainName; + else if( xPropertySetInfo->hasPropertyByName("ChainName") ) + xPropertySet->getPropertyValue("ChainName") >>= sLinkChainName; + + if( xPropertySetInfo->hasPropertyByName("ChainNextName") ) + xPropertySet->getPropertyValue("ChainNextName") >>= aLinkedTextboxInfo.sNextChain; + if( xPropertySetInfo->hasPropertyByName("ChainPrevName") ) + xPropertySet->getPropertyValue("ChainPrevName") >>= aLinkedTextboxInfo.sPrevChain; + + //collect a list of linked textboxes: those with a NEXT or PREVIOUS link + if( !aLinkedTextboxInfo.sNextChain.isEmpty() || !aLinkedTextboxInfo.sPrevChain.isEmpty() ) + { + assert( !sLinkChainName.isEmpty() ); + + //there are many discarded duplicates in documents - no duplicates allowed in the list, so try to find the real one. + //if this LinkDisplayName/ChainName already exists on a different shape... + // the earlier processed duplicates are thrown out unless this one can be proved as bad. (last processed duplicate usually is stored) + std::map::iterator linkFinder; + linkFinder = m_rExport.m_aLinkedTextboxesHelper.find(sLinkChainName); + if( linkFinder != m_rExport.m_aLinkedTextboxesHelper.end() ) + { + //If my NEXT/PREV targets have already been discovered, but don't match me, then assume I'm an abandoned remnant + // (this logic fails if both me and one of my links are duplicated, and the remnants were added first.) + linkFinder = m_rExport.m_aLinkedTextboxesHelper.find(aLinkedTextboxInfo.sNextChain); + if( (linkFinder != m_rExport.m_aLinkedTextboxesHelper.end()) && (linkFinder->second.sPrevChain != sLinkChainName) ) + { + ++linkedTextboxesIter; + break; + } + + linkFinder = m_rExport.m_aLinkedTextboxesHelper.find(aLinkedTextboxInfo.sPrevChain); + if( (linkFinder != m_rExport.m_aLinkedTextboxesHelper.end()) && (linkFinder->second.sNextChain != sLinkChainName) ) + { + ++linkedTextboxesIter; + break; + } + } + m_rExport.m_bLinkedTextboxesHelperInitialized = false; + m_rExport.m_aLinkedTextboxesHelper[sLinkChainName] = aLinkedTextboxInfo; + } + } + ++linkedTextboxesIter; + } + /* #i2916# May have an anchored graphic to be placed, loop through sorted array diff --git a/sw/source/filter/ww8/wrtww8.cxx b/sw/source/filter/ww8/wrtww8.cxx index 564381d5ced2..a5efd2eaedb3 100644 --- a/sw/source/filter/ww8/wrtww8.cxx +++ b/sw/source/filter/ww8/wrtww8.cxx @@ -1682,6 +1682,9 @@ void MSWordExportBase::WriteSpecialText( sal_uLong nStart, sal_uLong nEnd, sal_u // bOutKF was setted / stored in WriteKF1 SetCurPam(nStart, nEnd); + // clear linked textboxes since old ones can't be linked to frames in this section + m_aLinkedTextboxesHelper.clear(); + WriteText(); m_bOutPageDescs = bOldPageDescs; diff --git a/sw/source/filter/ww8/wrtww8.hxx b/sw/source/filter/ww8/wrtww8.hxx index 8691bfa291f0..34f04129be47 100644 --- a/sw/source/filter/ww8/wrtww8.hxx +++ b/sw/source/filter/ww8/wrtww8.hxx @@ -490,6 +490,18 @@ public: WW8_WrPlcAnnotations* m_pAtn; WW8_WrPlcTextBoxes *m_pTextBxs, *m_pHFTextBxs; + struct LinkedTextboxInfo //help analyze textbox flow links + { + sal_Int32 nId; + sal_Int32 nSeq; + OUString sNextChain; + OUString sPrevChain; + LinkedTextboxInfo(): nId(0), nSeq(0) {} + }; + std::map m_aLinkedTextboxesHelper; + bool m_bLinkedTextboxesHelperInitialized = false; + sal_Int32 m_nLinkedTextboxesChainId=0; + const sw::Frame *m_pParentFrame; // If set we are exporting content inside // a frame, e.g. a graphic node -- cgit v1.2.3