From 489269d71b92a15901c77e156cfeeb3a7fdcd6a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20K=C5=82os?= Date: Wed, 22 Apr 2026 07:01:16 +0000 Subject: [PATCH] oox: do not guess qFormat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - if set on import - reuse - if not set - guess to be sure we show default styles when document was built from scratch - do not force non-used automatical styles to be always visible - styles like "Heading" are automatically added when we use some ODT file - on DOCX side there is no matching style and we assumed it is a custom style and should be "favourite" Change-Id: Ifc2b704b8051f93d46a783ef90ecd18e8fcf273d Reviewed-on: https://gerrit.collaboraoffice.com/c/core/+/1345 Reviewed-by: Tomaž Vajngerl Tested-by: Jenkins CPCI (cherry picked from commit 9cb18a05167ff6a079a649956cca969df4564927) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/206359 Reviewed-by: Xisco Fauli Tested-by: Jenkins --- sw/inc/format.hxx | 18 ++++++-- sw/qa/extras/ooxmlexport/ooxmlexport25.cxx | 8 +++- sw/source/core/attr/format.cxx | 12 +++-- sw/source/filter/ww8/docxattributeoutput.cxx | 48 +++++++++++++++++--- sw/source/uibase/app/docstyle.cxx | 6 +-- 5 files changed, 73 insertions(+), 19 deletions(-) diff --git a/sw/inc/format.hxx b/sw/inc/format.hxx index 06a38919ca4d..34a619c6a3b1 100644 --- a/sw/inc/format.hxx +++ b/sw/inc/format.hxx @@ -24,9 +24,11 @@ #include "BorderCacheOwner.hxx" #include "calbck.hxx" #include "hintids.hxx" +#include "poolfmt.hxx" #include "swatrset.hxx" #include "names.hxx" #include +#include class IDocumentSettingAccess; class IDocumentDrawModelAccess; @@ -64,7 +66,7 @@ class SW_DLLPUBLIC SwFormat : public sw::BorderCacheOwner, public sw::Broadcasti bool m_bAutoUpdateOnDirectFormat : 1;/**< TRUE: Set attributes of a whole paragraph at format (UI-side!). */ bool m_bHidden : 1; - bool m_bIsFavourite : 1; ///< Show in the basic UI + std::optional m_bIsFavourite; ///< Show in the basic UI; unset means unknown std::shared_ptr m_pGrabBagItem; ///< Style InteropGrabBag. virtual void InvalidateInSwFntCache(sal_uInt16) {}; virtual void InvalidateInSwFntCache() {}; @@ -165,7 +167,15 @@ public: /// Get and set Pool style IDs. SwPoolFormatId GetPoolFormatId() const { return m_nPoolFormatId; } - void SetPoolFormatId( SwPoolFormatId nId ) { m_nPoolFormatId = nId; } + void SetPoolFormatId( SwPoolFormatId nId ) + { + m_nPoolFormatId = nId; + if (nId == SwPoolFormatId::UNKNOWN) + { + // it's non-default style + m_bIsFavourite = std::optional(true); + } + } /// Get and set Help-IDs for document templates. sal_uInt32 GetPoolHelpId() const { return m_nPoolHelpId; } @@ -182,8 +192,8 @@ public: bool IsAuto() const { return m_bAutoFormat; } void SetAuto( bool bNew ) { m_bAutoFormat = bNew; } - bool IsFavourite() const { return m_bIsFavourite; } - void SetFavourite(bool bValue) { m_bIsFavourite = bValue; } + std::optional IsFavourite() const { return m_bIsFavourite; } + void SetFavourite(std::optional oValue) { m_bIsFavourite = oValue; } bool IsHidden() const { return m_bHidden; } void SetHidden( bool bValue ) { m_bHidden = bValue; } diff --git a/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx b/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx index 26b9ac4d49ef..a6cef3685d11 100644 --- a/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx +++ b/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx @@ -731,8 +731,12 @@ CPPUNIT_TEST_FIXTURE(Test, testQFormatPreservation) xmlDocUniquePtr pXmlStyles = parseExport(u"word/styles.xml"_ustr); - assertXPath(pXmlStyles, "//w:style[@w:styleId='Heading']/w:qFormat", 1); - // not used currently and had qFormat = 0 on import + // "Heading" is the LO pool style auto-created as parent of Heading 1-9. + // The source DOCX has no such style, so the export must not invent qFormat + // for it via the lcl_guessQFormat fallback - import silence is preserved. + assertXPath(pXmlStyles, "//w:style[@w:styleId='Heading']/w:qFormat", 0); + // "No spacing" had w:qFormat w:val="0" in the source - that explicit + // "no qFormat" must round-trip as no qFormat element on export. assertXPath(pXmlStyles, "//w:style[@w:styleId='No spacing']/w:qFormat", 0); } diff --git a/sw/source/core/attr/format.cxx b/sw/source/core/attr/format.cxx index 125ddeb7129c..dc29a0d74aaa 100644 --- a/sw/source/core/attr/format.cxx +++ b/sw/source/core/attr/format.cxx @@ -50,13 +50,17 @@ SwFormat::SwFormat( SwAttrPool& rPool, const UIName& rFormatNm, m_bAutoUpdateOnDirectFormat = false; // LAYER_IMPL m_bAutoFormat = true; m_bFormatInDTOR = m_bHidden = false; - m_bIsFavourite = true; if( pDrvdFrame ) { pDrvdFrame->Add(*this); m_aSet.SetParent( &pDrvdFrame->m_aSet ); } + + // m_bIsFavourite left as std::nullopt: unknown until the format is + // imported from DOCX (SetGrabBagItem -> ParseFavourites). For non-DOCX + // sources (new documents, ODT import) the optional stays unset so the + // DOCX exporter falls back to lcl_guessQFormat for well-known styles. } SwFormat::SwFormat( const SwFormat& rFormat ) : @@ -745,14 +749,14 @@ void SwFormat::ParseFavourites() if (aIt->second >>= nVal) { if (nVal == 0) - SetFavourite(false); + SetFavourite(std::optional(false)); else - SetFavourite(true); + SetFavourite(std::optional(true)); } } else { - SetFavourite(false); + SetFavourite(std::optional(false)); } } diff --git a/sw/source/filter/ww8/docxattributeoutput.cxx b/sw/source/filter/ww8/docxattributeoutput.cxx index 0ff71602d712..31251d57f2c0 100644 --- a/sw/source/filter/ww8/docxattributeoutput.cxx +++ b/sw/source/filter/ww8/docxattributeoutput.cxx @@ -7137,12 +7137,30 @@ static bool lcl_guessQFormat(const OUString& rName, sal_uInt16 nWwId) return aAllowlist.find(rName) != aAllowlist.end(); } +/// Detect whether the source document was loaded from DOCX. The writerfilter +/// import populates the document-level InteropGrabBag (latentStyles, default +/// tab stops, etc.) on every DOCX it loads; new documents and ODT imports +/// leave it empty. A non-empty bag is therefore a reliable "DOCX origin" +/// signal we can use to suppress the lcl_guessQFormat fallback for styles +/// that were auto-created (e.g. the Heading pool style spawned as parent of +/// Heading 1) rather than imported from the source. +static bool lcl_sourceWasDocx(const MSWordExportBase& rExport) +{ + if (!rExport.m_xTextDoc.is()) + return false; + uno::Sequence aDocGrabBag; + rExport.m_xTextDoc->getPropertyValue(u"InteropGrabBag"_ustr) >>= aDocGrabBag; + return aDocGrabBag.hasElements(); +} + void DocxAttributeOutput::StartStyle( const OUString& rName, StyleType eType, sal_uInt16 nBase, sal_uInt16 nNext, sal_uInt16 nLink, sal_uInt16 nWwId, sal_uInt16 nSlot, bool bAutoUpdate ) { bool bUnhideWhenUsed = false, bSemiHidden = false, bLocked = false, bDefault = false, bCustomStyle = false; - bool bQFormat = false; // DEPRECATED: from grab-bag - bool bRealQFormat = true; // from SwFormat + // unset means we have no information from import: fall back to lcl_guessQFormat. + // Set means the format was imported from a DOCX (via ParseFavourites) and the + // import-time qFormat state must be preserved on export. + std::optional oQFormat; OUString aRsid, aUiPriority; rtl::Reference pStyleAttributeList = FastSerializerHelper::createAttrList(); @@ -7151,7 +7169,7 @@ void DocxAttributeOutput::StartStyle( const OUString& rName, StyleType eType, { const SwFormat* pFormat = m_rExport.m_pStyles->GetSwFormat(nSlot); pFormat->GetGrabBagItem(aAny); - bRealQFormat = pFormat->IsFavourite(); + oQFormat = pFormat->IsFavourite(); } else { @@ -7165,7 +7183,14 @@ void DocxAttributeOutput::StartStyle( const OUString& rName, StyleType eType, if (rProp.Name == "uiPriority") aUiPriority = rProp.Value.get(); else if (rProp.Name == "qFormat") - bQFormat = true; + { + // For paragraph and character styles SwFormat::IsFavourite already + // carries the import-time decision (set by ParseFavourites). Only + // honor the grab-bag value when we have no SwFormat-level state, + // which today only happens for numbering styles. + if (!oQFormat.has_value()) + oQFormat = rProp.Value.get() != 0; + } else if (rProp.Name == "rsid") aRsid = rProp.Value.get(); else if (rProp.Name == "unhideWhenUsed") @@ -7229,8 +7254,19 @@ void DocxAttributeOutput::StartStyle( const OUString& rName, StyleType eType, m_pSerializer->singleElementNS(XML_w, XML_semiHidden); if (bUnhideWhenUsed) m_pSerializer->singleElementNS(XML_w, XML_unhideWhenUsed); - // by default we use old guess, if user marks style as non-favourite -> do not export qFormat - if (bRealQFormat && (bQFormat || lcl_guessQFormat(rName, nWwId))) + // Honor the explicit qFormat value from import. Otherwise: for DOCX-sourced + // documents the absence of qFormat means the style was auto-created and + // wasn't in the source - preserve that silence. For new or ODT-imported + // documents fall back to guessing so well-known built-in styles still get + // qFormat in their first DOCX export. + bool bEmitQFormat; + if (oQFormat.has_value()) + bEmitQFormat = oQFormat.value(); + else if (lcl_sourceWasDocx(m_rExport)) + bEmitQFormat = false; + else + bEmitQFormat = lcl_guessQFormat(rName, nWwId); + if (bEmitQFormat) m_pSerializer->singleElementNS(XML_w, XML_qFormat); if (bLocked) m_pSerializer->singleElementNS(XML_w, XML_locked); diff --git a/sw/source/uibase/app/docstyle.cxx b/sw/source/uibase/app/docstyle.cxx index 510d33611df0..8a64933651f4 100644 --- a/sw/source/uibase/app/docstyle.cxx +++ b/sw/source/uibase/app/docstyle.cxx @@ -3341,7 +3341,7 @@ SfxStyleSheetBase* SwStyleSheetIterator::First() if ( tmpMask & SfxStyleSearchBits::Favourite ) { SwFormat* pFormat = rDoc.FindTextFormatCollByName( pColl->GetName() ); - if (!(pFormat && pFormat->IsFavourite())) continue; + if (!(pFormat && pFormat->IsFavourite().value_or(false))) continue; } // searched for used and found none @@ -3628,7 +3628,7 @@ void SwStyleSheetIterator::AppendStyleList(const std::vector& rList, bUsed = rDoc.getIDocumentStylePoolAccess().IsPoolTextCollUsed( nId ); SwFormat* pFormat = rDoc.FindTextFormatCollByName( i ); bHidden = pFormat && pFormat->IsHidden( ); - bFavourite = pFormat && pFormat->IsFavourite(); + bFavourite = pFormat && pFormat->IsFavourite().value_or(false); } break; case SwGetPoolIdFromName::ChrFmt: @@ -3636,7 +3636,7 @@ void SwStyleSheetIterator::AppendStyleList(const std::vector& rList, bUsed = rDoc.getIDocumentStylePoolAccess().IsPoolFormatUsed( nId ); SwFormat* pFormat = rDoc.FindCharFormatByName( i ); bHidden = pFormat && pFormat->IsHidden( ); - bFavourite = pFormat && pFormat->IsFavourite(); + bFavourite = pFormat && pFormat->IsFavourite().value_or(false); } break; case SwGetPoolIdFromName::FrmFmt: