oox: do not guess qFormat

- 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 <tomaz.vajngerl@collabora.com>
Tested-by: Jenkins CPCI <releng@collaboraoffice.com>
(cherry picked from commit 9cb18a05167ff6a079a649956cca969df4564927)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/206359
Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org>
Tested-by: Jenkins
This commit is contained in:
Szymon Kłos
2026-06-13 02:04:02 +02:00
committed by Xisco Fauli
parent 582c2ace3f
commit 489269d71b
5 changed files with 73 additions and 19 deletions
+14 -4
View File
@@ -24,9 +24,11 @@
#include "BorderCacheOwner.hxx"
#include "calbck.hxx"
#include "hintids.hxx"
#include "poolfmt.hxx"
#include "swatrset.hxx"
#include "names.hxx"
#include <memory>
#include <optional>
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<bool> m_bIsFavourite; ///< Show in the basic UI; unset means unknown
std::shared_ptr<SfxGrabBagItem> 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<bool>(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<bool> IsFavourite() const { return m_bIsFavourite; }
void SetFavourite(std::optional<bool> oValue) { m_bIsFavourite = oValue; }
bool IsHidden() const { return m_bHidden; }
void SetHidden( bool bValue ) { m_bHidden = bValue; }
+6 -2
View File
@@ -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);
}
+8 -4
View File
@@ -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<bool>(false));
else
SetFavourite(true);
SetFavourite(std::optional<bool>(true));
}
}
else
{
SetFavourite(false);
SetFavourite(std::optional<bool>(false));
}
}
+42 -6
View File
@@ -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<beans::PropertyValue> 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<bool> oQFormat;
OUString aRsid, aUiPriority;
rtl::Reference<FastAttributeList> 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<OUString>();
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<sal_Int32>() != 0;
}
else if (rProp.Name == "rsid")
aRsid = rProp.Value.get<OUString>();
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);
+3 -3
View File
@@ -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<OUString>& 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<OUString>& 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: