/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This file is part of the LibreOffice project. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace oox; using namespace com::sun::star; namespace { void savePivotCacheRecordsXml( XclExpXmlStream& rStrm, const ScDPCache& rCache ) { SCROW nCount = rCache.GetDataSize(); size_t nFieldCount = rCache.GetFieldCount(); sax_fastparser::FSHelperPtr& pRecStrm = rStrm.GetCurrentStream(); pRecStrm->startElement(XML_pivotCacheRecords, XML_xmlns, rStrm.getNamespaceURL(OOX_NS(xls)).toUtf8(), FSNS(XML_xmlns, XML_r), rStrm.getNamespaceURL(OOX_NS(officeRel)).toUtf8(), XML_count, OString::number(static_cast(nCount))); for (SCROW i = 0; i < nCount; ++i) { pRecStrm->startElement(XML_r); for (size_t nField = 0; nField < nFieldCount; ++nField) { const ScDPCache::IndexArrayType* pArray = rCache.GetFieldIndexArray(nField); assert(pArray); assert(o3tl::make_unsigned(i) < pArray->size()); // We are using XML_x reference (like: ), instead of values here (eg: ). // That's why in SavePivotCacheXml method, we need to list all items. pRecStrm->singleElement(XML_x, XML_v, OString::number((*pArray)[i])); } pRecStrm->endElement(XML_r); } pRecStrm->endElement(XML_pivotCacheRecords); } const char* toOOXMLAxisType( sheet::DataPilotFieldOrientation eOrient ) { switch (eOrient) { case sheet::DataPilotFieldOrientation_COLUMN: return "axisCol"; case sheet::DataPilotFieldOrientation_ROW: return "axisRow"; case sheet::DataPilotFieldOrientation_PAGE: return "axisPage"; case sheet::DataPilotFieldOrientation_DATA: return "axisValues"; case sheet::DataPilotFieldOrientation_HIDDEN: default: ; } return ""; } const char* toOOXMLSubtotalType(ScGeneralFunction eFunc) { switch (eFunc) { case ScGeneralFunction::SUM: return "sum"; case ScGeneralFunction::COUNT: return "count"; case ScGeneralFunction::AVERAGE: return "average"; case ScGeneralFunction::MAX: return "max"; case ScGeneralFunction::MIN: return "min"; case ScGeneralFunction::PRODUCT: return "product"; case ScGeneralFunction::COUNTNUMS: return "countNums"; case ScGeneralFunction::STDEV: return "stdDev"; case ScGeneralFunction::STDEVP: return "stdDevp"; case ScGeneralFunction::VAR: return "var"; case ScGeneralFunction::VARP: return "varp"; default: ; } return nullptr; } } XclExpXmlPivotCaches::XclExpXmlPivotCaches( const XclExpRoot& rRoot ) : XclExpRoot(rRoot) {} void XclExpXmlPivotCaches::SaveXml( XclExpXmlStream& rStrm ) { sax_fastparser::FSHelperPtr& pWorkbookStrm = rStrm.GetCurrentStream(); pWorkbookStrm->startElement(XML_pivotCaches); for (size_t i = 0, n = maCaches.size(); i < n; ++i) { const Entry& rEntry = maCaches[i]; sal_Int32 nCacheId = i + 1; OUString aRelId; sax_fastparser::FSHelperPtr pPCStrm = rStrm.CreateOutputStream( XclXmlUtils::GetStreamName("xl/pivotCache/", "pivotCacheDefinition", nCacheId), XclXmlUtils::GetStreamName(nullptr, "pivotCache/pivotCacheDefinition", nCacheId), rStrm.GetCurrentStream()->getOutputStream(), CREATE_XL_CONTENT_TYPE("pivotCacheDefinition"), CREATE_OFFICEDOC_RELATION_TYPE("pivotCacheDefinition"), &aRelId); pWorkbookStrm->singleElement(XML_pivotCache, XML_cacheId, OString::number(nCacheId), FSNS(XML_r, XML_id), aRelId.toUtf8()); rStrm.PushStream(pPCStrm); SavePivotCacheXml(rStrm, rEntry, nCacheId); rStrm.PopStream(); } pWorkbookStrm->endElement(XML_pivotCaches); } void XclExpXmlPivotCaches::SetCaches( const std::vector& rCaches ) { maCaches = rCaches; } bool XclExpXmlPivotCaches::HasCaches() const { return !maCaches.empty(); } const XclExpXmlPivotCaches::Entry* XclExpXmlPivotCaches::GetCache( sal_Int32 nCacheId ) const { if (nCacheId <= 0) // cache ID is 1-based. return nullptr; size_t nPos = nCacheId - 1; if (nPos >= maCaches.size()) return nullptr; return &maCaches[nPos]; } namespace { /** * Create combined date and time string according the requirements of Excel. * A single point in time can be represented by concatenating a complete date expression, * the letter T as a delimiter, and a valid time expression. For example, "2007-04-05T14:30". * * fSerialDateTime - a number representing the number of days since 1900-Jan-0 (integer portion of the number), * plus a fractional portion of a 24 hour day (fractional portion of the number). */ OUString GetExcelFormattedDate( double fSerialDateTime, const SvNumberFormatter& rFormatter ) { // tdf#125055: properly round the value to seconds when truncating nanoseconds below constexpr double fHalfSecond = 1 / 86400.0 * 0.5; css::util::DateTime aUDateTime = (DateTime(rFormatter.GetNullDate()) + fSerialDateTime + fHalfSecond).GetUNODateTime(); // We need to reset nanoseconds, to avoid string like: "1982-02-18T16:04:47.999999849" aUDateTime.NanoSeconds = 0; OUStringBuffer sBuf; ::sax::Converter::convertDateTime(sBuf, aUDateTime, nullptr, true); return sBuf.makeStringAndClear(); } // Excel seems to expect different order of group item values; we need to rearrange elements // to output "date2" last. // Since ScDPItemData::DateFirst is -1, ScDPItemData::DateLast is 10000, and other date group // items would fit between those in order (like 0 = Jan, 1 = Feb, etc.), we can simply sort // the items by value. std::vector SortGroupItems(const ScDPCache& rCache, long nDim) { struct ItemData { sal_Int32 nVal; const ScDPItemData* pData; }; std::vector aDataToSort; ScfInt32Vec aGIIds; rCache.GetGroupDimMemberIds(nDim, aGIIds); for (sal_Int32 id : aGIIds) { const ScDPItemData* pGIData = rCache.GetItemDataById(nDim, id); if (pGIData->GetType() == ScDPItemData::GroupValue) { auto aGroupVal = pGIData->GetGroupValue(); aDataToSort.push_back({ aGroupVal.mnValue, pGIData }); } } std::sort(aDataToSort.begin(), aDataToSort.end(), [](const ItemData& a, const ItemData& b) { return a.nVal < b.nVal; }); std::vector aSortedResult; for (const auto& el : aDataToSort) { aSortedResult.push_back(rCache.GetFormattedString(nDim, *el.pData, false)); } return aSortedResult; } } // namespace void XclExpXmlPivotCaches::SavePivotCacheXml( XclExpXmlStream& rStrm, const Entry& rEntry, sal_Int32 nCounter ) { assert(rEntry.mpCache); const ScDPCache& rCache = *rEntry.mpCache; sax_fastparser::FSHelperPtr& pDefStrm = rStrm.GetCurrentStream(); OUString aRelId; sax_fastparser::FSHelperPtr pRecStrm = rStrm.CreateOutputStream( XclXmlUtils::GetStreamName("xl/pivotCache/", "pivotCacheRecords", nCounter), XclXmlUtils::GetStreamName(nullptr, "pivotCacheRecords", nCounter), pDefStrm->getOutputStream(), CREATE_XL_CONTENT_TYPE("pivotCacheRecords"), CREATE_OFFICEDOC_RELATION_TYPE("pivotCacheRecords"), &aRelId); rStrm.PushStream(pRecStrm); savePivotCacheRecordsXml(rStrm, rCache); rStrm.PopStream(); pDefStrm->startElement(XML_pivotCacheDefinition, XML_xmlns, rStrm.getNamespaceURL(OOX_NS(xls)).toUtf8(), FSNS(XML_xmlns, XML_r), rStrm.getNamespaceURL(OOX_NS(officeRel)).toUtf8(), FSNS(XML_r, XML_id), aRelId.toUtf8(), XML_recordCount, OString::number(rEntry.mpCache->GetDataSize()), XML_createdVersion, "3"); // MS Excel 2007, tdf#112936: setting version number makes MSO to handle the pivot table differently pDefStrm->startElement(XML_cacheSource, XML_type, "worksheet"); OUString aSheetName; GetDoc().GetName(rEntry.maSrcRange.aStart.Tab(), aSheetName); pDefStrm->singleElement(XML_worksheetSource, XML_ref, XclXmlUtils::ToOString(rStrm.GetRoot().GetDoc(), rEntry.maSrcRange), XML_sheet, aSheetName.toUtf8()); pDefStrm->endElement(XML_cacheSource); size_t nCount = rCache.GetFieldCount(); const size_t nGroupFieldCount = rCache.GetGroupFieldCount(); pDefStrm->startElement(XML_cacheFields, XML_count, OString::number(static_cast(nCount + nGroupFieldCount))); auto WriteFieldGroup = [this, &rCache, pDefStrm](size_t i, size_t base) { const sal_Int32 nDatePart = rCache.GetGroupType(i); if (!nDatePart) return; OString sGroupBy; switch (nDatePart) { case sheet::DataPilotFieldGroupBy::SECONDS: sGroupBy = "seconds"; break; case sheet::DataPilotFieldGroupBy::MINUTES: sGroupBy = "minutes"; break; case sheet::DataPilotFieldGroupBy::HOURS: sGroupBy = "hours"; break; case sheet::DataPilotFieldGroupBy::DAYS: sGroupBy = "days"; break; case sheet::DataPilotFieldGroupBy::MONTHS: sGroupBy = "months"; break; case sheet::DataPilotFieldGroupBy::QUARTERS: sGroupBy = "quarters"; break; case sheet::DataPilotFieldGroupBy::YEARS: sGroupBy = "years"; break; } // fieldGroup element pDefStrm->startElement(XML_fieldGroup, XML_base, OString::number(base)); SvNumberFormatter& rFormatter = GetFormatter(); // rangePr element const ScDPNumGroupInfo* pGI = rCache.GetNumGroupInfo(i); auto pGroupAttList = sax_fastparser::FastSerializerHelper::createAttrList(); pGroupAttList->add(XML_groupBy, sGroupBy); // Possible TODO: find out when to write autoStart attribute for years grouping pGroupAttList->add(XML_startDate, GetExcelFormattedDate(pGI->mfStart, rFormatter).toUtf8()); pGroupAttList->add(XML_endDate, GetExcelFormattedDate(pGI->mfEnd, rFormatter).toUtf8()); if (pGI->mfStep) pGroupAttList->add(XML_groupInterval, OString::number(pGI->mfStep)); pDefStrm->singleElement(XML_rangePr, pGroupAttList); // groupItems element auto aElemVec = SortGroupItems(rCache, i); pDefStrm->startElement(XML_groupItems, XML_count, OString::number(aElemVec.size())); for (const auto& sElem : aElemVec) { pDefStrm->singleElement(XML_s, XML_v, sElem.toUtf8()); } pDefStrm->endElement(XML_groupItems); pDefStrm->endElement(XML_fieldGroup); }; for (size_t i = 0; i < nCount; ++i) { OUString aName = rCache.GetDimensionName(i); pDefStrm->startElement(XML_cacheField, XML_name, aName.toUtf8(), XML_numFmtId, OString::number(0)); const ScDPCache::ScDPItemDataVec& rFieldItems = rCache.GetDimMemberValues(i); std::set aDPTypes; double fMin = std::numeric_limits::infinity(), fMax = -std::numeric_limits::infinity(); bool isValueInteger = true; bool isContainsDate = rCache.IsDateDimension(i); bool isLongText = false; for (const auto& rFieldItem : rFieldItems) { ScDPItemData::Type eType = rFieldItem.GetType(); // tdf#123939 : error and string are same for cache; if both are present, keep only one if (eType == ScDPItemData::Error) eType = ScDPItemData::String; aDPTypes.insert(eType); if (eType == ScDPItemData::Value) { double fVal = rFieldItem.GetValue(); fMin = std::min(fMin, fVal); fMax = std::max(fMax, fVal); // Check if all values are integers if (isValueInteger && (modf(fVal, &o3tl::temporary(double())) != 0.0)) { isValueInteger = false; } } else if (eType == ScDPItemData::String && !isLongText) { isLongText = rFieldItem.GetString().getLength() > 255; } } auto pAttList = sax_fastparser::FastSerializerHelper::createAttrList(); // TODO In same cases, disable listing of items, as it is done in MS Excel. // Exporting savePivotCacheRecordsXml method needs to be updated accordingly //bool bListItems = true; std::set aDPTypesWithoutBlank = aDPTypes; aDPTypesWithoutBlank.erase(ScDPItemData::Empty); const bool isContainsString = aDPTypesWithoutBlank.count(ScDPItemData::String) > 0; const bool isContainsBlank = aDPTypes.count(ScDPItemData::Empty) > 0; const bool isContainsNumber = !isContainsDate && aDPTypesWithoutBlank.count(ScDPItemData::Value) > 0; bool isContainsNonDate = !(isContainsDate && aDPTypesWithoutBlank.size() <= 1); // XML_containsSemiMixedTypes possible values: // 1 - (Default) at least one text value, or can also contain a mix of other data types and blank values, // or blank values only // 0 - the field does not have a mix of text and other values if (!(isContainsString || (aDPTypes.size() > 1) || (isContainsBlank && aDPTypesWithoutBlank.empty()))) pAttList->add(XML_containsSemiMixedTypes, ToPsz10(false)); if (!isContainsNonDate) pAttList->add(XML_containsNonDate, ToPsz10(false)); if (isContainsDate) pAttList->add(XML_containsDate, ToPsz10(true)); // default for containsString field is true, so we are writing only when is false if (!isContainsString) pAttList->add(XML_containsString, ToPsz10(false)); if (isContainsBlank) pAttList->add(XML_containsBlank, ToPsz10(true)); // XML_containsMixedType possible values: // 1 - field contains more than one data type // 0 - (Default) only one data type. The field can still contain blank values (that's why we are using aDPTypesWithoutBlank) if (aDPTypesWithoutBlank.size() > 1) pAttList->add(XML_containsMixedTypes, ToPsz10(true)); // If field contain mixed types (Date and Numbers), MS Excel is saving only "minDate" and "maxDate" and not "minValue" and "maxValue" // Example how Excel is saving mixed Date and Numbers: // // Example how Excel is saving Dates only: // if (isContainsNumber) pAttList->add(XML_containsNumber, ToPsz10(true)); if (isValueInteger && isContainsNumber) pAttList->add(XML_containsInteger, ToPsz10(true)); // Number type fields could be mixed with blank types, and it shouldn't be treated as listed items. // Example: // // // if (isContainsNumber) { pAttList->add(XML_minValue, OString::number(fMin)); pAttList->add(XML_maxValue, OString::number(fMax)); } if (isContainsDate) { pAttList->add(XML_minDate, GetExcelFormattedDate(fMin, GetFormatter()).toUtf8()); pAttList->add(XML_maxDate, GetExcelFormattedDate(fMax, GetFormatter()).toUtf8()); } //if (bListItems) // see TODO above { pAttList->add(XML_count, OString::number(static_cast(rFieldItems.size()))); } if (isLongText) { pAttList->add(XML_longText, ToPsz10(true)); } sax_fastparser::XFastAttributeListRef xAttributeList(pAttList); pDefStrm->startElement(XML_sharedItems, xAttributeList); //if (bListItems) // see TODO above { for (const ScDPItemData& rItem : rFieldItems) { switch (rItem.GetType()) { case ScDPItemData::String: pDefStrm->singleElement(XML_s, XML_v, rItem.GetString().toUtf8()); break; case ScDPItemData::Value: if (isContainsDate) { pDefStrm->singleElement(XML_d, XML_v, GetExcelFormattedDate(rItem.GetValue(), GetFormatter()).toUtf8()); } else pDefStrm->singleElement(XML_n, XML_v, OString::number(rItem.GetValue())); break; case ScDPItemData::Empty: pDefStrm->singleElement(XML_m); break; case ScDPItemData::Error: pDefStrm->singleElement(XML_e, XML_v, rItem.GetString().toUtf8()); break; case ScDPItemData::GroupValue: // Should not happen here! case ScDPItemData::RangeStart: // TODO : What do we do with these types? pDefStrm->singleElement(XML_m); break; default: ; } } } pDefStrm->endElement(XML_sharedItems); WriteFieldGroup(i, i); pDefStrm->endElement(XML_cacheField); } ScDPObject* pDPObject = rCache.GetAllReferences().empty() ? nullptr : *rCache.GetAllReferences().begin(); for (size_t i = nCount; pDPObject && i < nCount + nGroupFieldCount; ++i) { const OUString aName = pDPObject->GetDimName(i, o3tl::temporary(bool())); // tdf#126748: DPObject might not reference all group fields, when there are several // DPObjects referencing this cache. Trying to get a dimension data for a field not used // in a given DPObject will give nullptr, and dereferencing it then will crash. To avoid // the crash, until there's a correct method to find the names of group fields in cache, // just skip the fields, creating bad cache data, which is of course a temporary hack. // TODO: reimplement the whole block to get the names from another source, not from first // cache reference. if (aName.isEmpty()) break; ScDPSaveData* pSaveData = pDPObject->GetSaveData(); assert(pSaveData); const ScDPSaveGroupDimension* pDim = pSaveData->GetDimensionData()->GetNamedGroupDim(aName); assert(pDim); const SCCOL nBase = rCache.GetDimensionIndex(pDim->GetSourceDimName()); assert(nBase >= 0); pDefStrm->startElement(XML_cacheField, XML_name, aName.toUtf8(), XML_numFmtId, OString::number(0), XML_databaseField, ToPsz10(false)); WriteFieldGroup(i, nBase); pDefStrm->endElement(XML_cacheField); } pDefStrm->endElement(XML_cacheFields); pDefStrm->endElement(XML_pivotCacheDefinition); } XclExpXmlPivotTableManager::XclExpXmlPivotTableManager( const XclExpRoot& rRoot ) : XclExpRoot(rRoot), maCaches(rRoot) {} void XclExpXmlPivotTableManager::Initialize() { ScDocument& rDoc = GetDoc(); if (!rDoc.HasPivotTable()) // No pivot table to export. return; ScDPCollection* pDPColl = rDoc.GetDPCollection(); if (!pDPColl) return; // Update caches from DPObject for (size_t i = 0; i < pDPColl->GetCount(); ++i) { ScDPObject& rDPObj = (*pDPColl)[i]; rDPObj.SyncAllDimensionMembers(); (void)rDPObj.GetOutputRangeByType(sheet::DataPilotOutputRangeType::TABLE); } // Go through the caches first. std::vector aCaches; const ScDPCollection::SheetCaches& rSheetCaches = pDPColl->GetSheetCaches(); const std::vector& rRanges = rSheetCaches.getAllRanges(); for (const auto & rRange : rRanges) { const ScDPCache* pCache = rSheetCaches.getExistingCache(rRange); if (!pCache) continue; // Get all pivot objects that reference this cache, and set up an // object to cache ID mapping. const ScDPCache::ScDPObjectSet& rRefs = pCache->GetAllReferences(); for (const auto& rRef : rRefs) maCacheIdMap.emplace(rRef, aCaches.size()+1); XclExpXmlPivotCaches::Entry aEntry; aEntry.mpCache = pCache; aEntry.maSrcRange = rRange; aCaches.push_back(aEntry); // Cache ID equals position + 1. } // TODO : Handle name and database caches as well. for (size_t i = 0, n = pDPColl->GetCount(); i < n; ++i) { const ScDPObject& rDPObj = (*pDPColl)[i]; // Get the cache ID for this pivot table. CacheIdMapType::iterator itCache = maCacheIdMap.find(&rDPObj); if (itCache == maCacheIdMap.end()) // No cache ID found. Something is wrong here... continue; sal_Int32 nCacheId = itCache->second; SCTAB nTab = rDPObj.GetOutRange().aStart.Tab(); TablesType::iterator it = m_Tables.find(nTab); if (it == m_Tables.end()) { // Insert a new instance for this sheet index. std::pair r = m_Tables.insert(std::make_pair(nTab, std::make_unique(GetRoot(), maCaches))); it = r.first; } XclExpXmlPivotTables *const p = it->second.get(); p->AppendTable(&rDPObj, nCacheId, i+1); } maCaches.SetCaches(aCaches); } XclExpXmlPivotCaches& XclExpXmlPivotTableManager::GetCaches() { return maCaches; } XclExpXmlPivotTables* XclExpXmlPivotTableManager::GetTablesBySheet( SCTAB nTab ) { TablesType::iterator const it = m_Tables.find(nTab); return it == m_Tables.end() ? nullptr : it->second.get(); } XclExpXmlPivotTables::Entry::Entry( const ScDPObject* pTable, sal_Int32 nCacheId, sal_Int32 nPivotId ) : mpTable(pTable), mnCacheId(nCacheId), mnPivotId(nPivotId) {} XclExpXmlPivotTables::XclExpXmlPivotTables( const XclExpRoot& rRoot, const XclExpXmlPivotCaches& rCaches ) : XclExpRoot(rRoot), mrCaches(rCaches) {} void XclExpXmlPivotTables::SaveXml( XclExpXmlStream& rStrm ) { sax_fastparser::FSHelperPtr& pWSStrm = rStrm.GetCurrentStream(); // worksheet stream for (const auto& rTable : maTables) { const ScDPObject& rObj = *rTable.mpTable; sal_Int32 nCacheId = rTable.mnCacheId; sal_Int32 nPivotId = rTable.mnPivotId; sax_fastparser::FSHelperPtr pPivotStrm = rStrm.CreateOutputStream( XclXmlUtils::GetStreamName("xl/pivotTables/", "pivotTable", nPivotId), XclXmlUtils::GetStreamName(nullptr, "../pivotTables/pivotTable", nPivotId), pWSStrm->getOutputStream(), CREATE_XL_CONTENT_TYPE("pivotTable"), CREATE_OFFICEDOC_RELATION_TYPE("pivotTable")); rStrm.PushStream(pPivotStrm); SavePivotTableXml(rStrm, rObj, nCacheId); rStrm.PopStream(); } } namespace { struct DataField { long mnPos; // field index in pivot cache. const ScDPSaveDimension* mpDim; DataField( long nPos, const ScDPSaveDimension* pDim ) : mnPos(nPos), mpDim(pDim) {} }; /** Returns an OOXML subtotal function name string. See ECMA-376-1:2016 18.18.43 */ OString GetSubtotalFuncName(ScGeneralFunction eFunc) { switch (eFunc) { case ScGeneralFunction::SUM: return "sum"; case ScGeneralFunction::COUNT: return "count"; case ScGeneralFunction::AVERAGE: return "avg"; case ScGeneralFunction::MAX: return "max"; case ScGeneralFunction::MIN: return "min"; case ScGeneralFunction::PRODUCT: return "product"; case ScGeneralFunction::COUNTNUMS: return "countA"; case ScGeneralFunction::STDEV: return "stdDev"; case ScGeneralFunction::STDEVP: return "stdDevP"; case ScGeneralFunction::VAR: return "var"; case ScGeneralFunction::VARP: return "varP"; default:; } return "default"; } sal_Int32 GetSubtotalAttrToken(ScGeneralFunction eFunc) { switch (eFunc) { case ScGeneralFunction::SUM: return XML_sumSubtotal; case ScGeneralFunction::COUNT: return XML_countSubtotal; case ScGeneralFunction::AVERAGE: return XML_avgSubtotal; case ScGeneralFunction::MAX: return XML_maxSubtotal; case ScGeneralFunction::MIN: return XML_minSubtotal; case ScGeneralFunction::PRODUCT: return XML_productSubtotal; case ScGeneralFunction::COUNTNUMS: return XML_countASubtotal; case ScGeneralFunction::STDEV: return XML_stdDevSubtotal; case ScGeneralFunction::STDEVP: return XML_stdDevPSubtotal; case ScGeneralFunction::VAR: return XML_varSubtotal; case ScGeneralFunction::VARP: return XML_varPSubtotal; default:; } return XML_defaultSubtotal; } // An item is expected to contain sequences of css::xml::FastAttribute and css::xml::Attribute void WriteGrabBagItemToStream(XclExpXmlStream& rStrm, sal_Int32 tokenId, const css::uno::Any& rItem) { css::uno::Sequence aSeqs; if(!(rItem >>= aSeqs)) return; auto& pStrm = rStrm.GetCurrentStream(); pStrm->write("<")->writeId(tokenId); css::uno::Sequence aFastSeq; css::uno::Sequence aUnkSeq; for (const auto& a : std::as_const(aSeqs)) { if (a >>= aFastSeq) { for (const auto& rAttr : std::as_const(aFastSeq)) rStrm.WriteAttributes(rAttr.Token, rAttr.Value); } else if (a >>= aUnkSeq) { for (const auto& rAttr : std::as_const(aUnkSeq)) pStrm->write(" ") ->write(rAttr.Name) ->write("=\"") ->writeEscaped(rAttr.Value) ->write("\""); } } pStrm->write("/>"); } } void XclExpXmlPivotTables::SavePivotTableXml( XclExpXmlStream& rStrm, const ScDPObject& rDPObj, sal_Int32 nCacheId ) { typedef std::unordered_map NameToIdMapType; const XclExpXmlPivotCaches::Entry* pCacheEntry = mrCaches.GetCache(nCacheId); if (!pCacheEntry) // Something is horribly wrong. Check your logic. return; const ScDPCache& rCache = *pCacheEntry->mpCache; const ScDPSaveData& rSaveData = *rDPObj.GetSaveData(); size_t nFieldCount = rCache.GetFieldCount() + rCache.GetGroupFieldCount(); std::vector aCachedDims; NameToIdMapType aNameToIdMap; aCachedDims.reserve(nFieldCount); for (size_t i = 0; i < nFieldCount; ++i) { OUString aName = const_cast(rDPObj).GetDimName(i, o3tl::temporary(bool())); aNameToIdMap.emplace(aName, aCachedDims.size()); const ScDPSaveDimension* pDim = rSaveData.GetExistingDimensionByName(aName); aCachedDims.push_back(pDim); } std::vector aRowFields; std::vector aColFields; std::vector aPageFields; std::vector aDataFields; long nDataDimCount = rSaveData.GetDataDimensionCount(); // Use dimensions in the save data to get their correct ordering. // Dimension order here is significant as they specify the order of // appearance in each axis. const ScDPSaveData::DimsType& rDims = rSaveData.GetDimensions(); bool bTabularMode = false; for (const auto & i : rDims) { const ScDPSaveDimension& rDim = *i; long nPos = -1; // position in cache if (rDim.IsDataLayout()) nPos = -2; // Excel uses an index of -2 to indicate a data layout field. else { OUString aSrcName = ScDPUtil::getSourceDimensionName(rDim.GetName()); NameToIdMapType::iterator it = aNameToIdMap.find(aSrcName); if (it != aNameToIdMap.end()) nPos = it->second; if (nPos == -1) continue; if (!aCachedDims[nPos]) continue; } sheet::DataPilotFieldOrientation eOrient = rDim.GetOrientation(); switch (eOrient) { case sheet::DataPilotFieldOrientation_COLUMN: if (nPos == -2 && nDataDimCount <= 1) break; aColFields.push_back(nPos); break; case sheet::DataPilotFieldOrientation_ROW: aRowFields.push_back(nPos); break; case sheet::DataPilotFieldOrientation_PAGE: aPageFields.push_back(nPos); break; case sheet::DataPilotFieldOrientation_DATA: aDataFields.emplace_back(nPos, &rDim); break; case sheet::DataPilotFieldOrientation_HIDDEN: default: ; } if(rDim.GetLayoutInfo()) bTabularMode |= (rDim.GetLayoutInfo()->LayoutMode == sheet::DataPilotFieldLayoutMode::TABULAR_LAYOUT); } sax_fastparser::FSHelperPtr& pPivotStrm = rStrm.GetCurrentStream(); pPivotStrm->startElement(XML_pivotTableDefinition, XML_xmlns, rStrm.getNamespaceURL(OOX_NS(xls)).toUtf8(), XML_name, rDPObj.GetName().toUtf8(), XML_cacheId, OString::number(nCacheId), XML_applyNumberFormats, ToPsz10(false), XML_applyBorderFormats, ToPsz10(false), XML_applyFontFormats, ToPsz10(false), XML_applyPatternFormats, ToPsz10(false), XML_applyAlignmentFormats, ToPsz10(false), XML_applyWidthHeightFormats, ToPsz10(false), XML_dataCaption, "Values", XML_useAutoFormatting, ToPsz10(false), XML_itemPrintTitles, ToPsz10(true), XML_indent, ToPsz10(false), XML_outline, ToPsz10(!bTabularMode), XML_outlineData, ToPsz10(!bTabularMode), XML_compact, ToPsz10(false), XML_compactData, ToPsz10(false)); // NB: Excel's range does not include page field area (if any). ScRange aOutRange = rDPObj.GetOutputRangeByType(sheet::DataPilotOutputRangeType::TABLE); sal_Int32 nFirstHeaderRow = rDPObj.GetHeaderLayout() ? 2 : 1; sal_Int32 nFirstDataRow = 2; sal_Int32 nFirstDataCol = 1; ScRange aResRange = rDPObj.GetOutputRangeByType(sheet::DataPilotOutputRangeType::RESULT); if (!aOutRange.IsValid()) aOutRange = rDPObj.GetOutRange(); if (aOutRange.IsValid() && aResRange.IsValid()) { nFirstDataRow = aResRange.aStart.Row() - aOutRange.aStart.Row(); nFirstDataCol = aResRange.aStart.Col() - aOutRange.aStart.Col(); } pPivotStrm->write("<")->writeId(XML_location); rStrm.WriteAttributes(XML_ref, XclXmlUtils::ToOString(rStrm.GetRoot().GetDoc(), aOutRange), XML_firstHeaderRow, OUString::number(nFirstHeaderRow), XML_firstDataRow, OUString::number(nFirstDataRow), XML_firstDataCol, OUString::number(nFirstDataCol)); if (!aPageFields.empty()) { rStrm.WriteAttributes(XML_rowPageCount, OUString::number(static_cast(aPageFields.size()))); rStrm.WriteAttributes(XML_colPageCount, OUString::number(1)); } pPivotStrm->write("/>"); // - It must contain all fields in the pivot cache even if // only some of them are used in the pivot table. The order must be as // they appear in the cache. pPivotStrm->startElement(XML_pivotFields, XML_count, OString::number(static_cast(aCachedDims.size()))); for (size_t i = 0; i < nFieldCount; ++i) { const ScDPSaveDimension* pDim = aCachedDims[i]; if (!pDim) { pPivotStrm->singleElement(XML_pivotField, XML_compact, ToPsz10(false), XML_showAll, ToPsz10(false)); continue; } bool bDimInTabularMode = false; if(pDim->GetLayoutInfo()) bDimInTabularMode = (pDim->GetLayoutInfo()->LayoutMode == sheet::DataPilotFieldLayoutMode::TABULAR_LAYOUT); sheet::DataPilotFieldOrientation eOrient = pDim->GetOrientation(); if (eOrient == sheet::DataPilotFieldOrientation_HIDDEN) { if(bDimInTabularMode) { pPivotStrm->singleElement(XML_pivotField, XML_compact, ToPsz10(false), XML_showAll, ToPsz10(false), XML_outline, ToPsz10(false)); } else { pPivotStrm->singleElement(XML_pivotField, XML_compact, ToPsz10(false), XML_showAll, ToPsz10(false)); } continue; } if (eOrient == sheet::DataPilotFieldOrientation_DATA) { if(bDimInTabularMode) { pPivotStrm->singleElement(XML_pivotField, XML_dataField, ToPsz10(true), XML_compact, ToPsz10(false), XML_showAll, ToPsz10(false), XML_outline, ToPsz10(false)); } else { pPivotStrm->singleElement(XML_pivotField, XML_dataField, ToPsz10(true), XML_compact, ToPsz10(false), XML_showAll, ToPsz10(false)); } continue; } // Dump field items. std::vector aMembers; { // We need to get the members in actual order, getting which requires non-const reference here auto& dpo = const_cast(rDPObj); dpo.GetMembers(i, dpo.GetUsedHierarchy(i), aMembers); } std::vector aCacheFieldItems; if (i < rCache.GetFieldCount() && !rCache.GetGroupType(i)) { for (const auto& it : rCache.GetDimMemberValues(i)) { OUString sFormattedName; if (it.HasStringData() || it.IsEmpty()) sFormattedName = it.GetString(); else sFormattedName = const_cast(rDPObj).GetFormattedString( pDim->GetName(), it.GetValue()); aCacheFieldItems.push_back(sFormattedName); } } else { aCacheFieldItems = SortGroupItems(rCache, i); } // The pair contains the member index in cache and if it is hidden std::vector< std::pair > aMemberSequence; std::set aUsedCachePositions; for (const auto & rMember : aMembers) { auto it = std::find(aCacheFieldItems.begin(), aCacheFieldItems.end(), rMember.maName); if (it != aCacheFieldItems.end()) { size_t nCachePos = static_cast(std::distance(aCacheFieldItems.begin(), it)); auto aInserted = aUsedCachePositions.insert(nCachePos); if (aInserted.second) aMemberSequence.emplace_back(std::make_pair(nCachePos, !rMember.mbVisible)); } } // Now add all remaining cache items as hidden for (size_t nItem = 0; nItem < aCacheFieldItems.size(); ++nItem) { if (aUsedCachePositions.find(nItem) == aUsedCachePositions.end()) aMemberSequence.emplace_back(nItem, true); } // tdf#125086: check if this field *also* appears in Data region bool bAppearsInData = false; { OUString aSrcName = ScDPUtil::getSourceDimensionName(pDim->GetName()); const auto it = std::find_if( aDataFields.begin(), aDataFields.end(), [&aSrcName](const DataField& rDataField) { OUString aThisName = ScDPUtil::getSourceDimensionName(rDataField.mpDim->GetName()); return aThisName == aSrcName; }); if (it != aDataFields.end()) bAppearsInData = true; } auto pAttList = sax_fastparser::FastSerializerHelper::createAttrList(); pAttList->add(XML_axis, toOOXMLAxisType(eOrient)); if (bAppearsInData) pAttList->add(XML_dataField, ToPsz10(true)); pAttList->add(XML_compact, ToPsz10(false)); pAttList->add(XML_showAll, ToPsz10(false)); long nSubTotalCount = pDim->GetSubTotalsCount(); std::vector aSubtotalSequence; bool bHasDefaultSubtotal = false; for (long nSubTotal = 0; nSubTotal < nSubTotalCount; ++nSubTotal) { ScGeneralFunction eFunc = pDim->GetSubTotalFunc(nSubTotal); aSubtotalSequence.push_back(GetSubtotalFuncName(eFunc)); sal_Int32 nAttToken = GetSubtotalAttrToken(eFunc); if (nAttToken == XML_defaultSubtotal) bHasDefaultSubtotal = true; else if (!pAttList->hasAttribute(nAttToken)) pAttList->add(nAttToken, ToPsz10(true)); } // XML_defaultSubtotal is true by default; only write it if it's false if (!bHasDefaultSubtotal) pAttList->add(XML_defaultSubtotal, ToPsz10(false)); if(bDimInTabularMode) pAttList->add( XML_outline, ToPsz10(false)); sax_fastparser::XFastAttributeListRef xAttributeList(pAttList); pPivotStrm->startElement(XML_pivotField, xAttributeList); pPivotStrm->startElement(XML_items, XML_count, OString::number(static_cast(aMemberSequence.size() + aSubtotalSequence.size()))); for (const auto & nMember : aMemberSequence) { auto pItemAttList = sax_fastparser::FastSerializerHelper::createAttrList(); if (nMember.second) pItemAttList->add(XML_h, ToPsz10(true)); pItemAttList->add(XML_x, OString::number(static_cast(nMember.first))); sax_fastparser::XFastAttributeListRef xItemAttributeList(pItemAttList); pPivotStrm->singleElement(XML_item, xItemAttributeList); } for (const OString& sSubtotal : aSubtotalSequence) { pPivotStrm->singleElement(XML_item, XML_t, sSubtotal); } pPivotStrm->endElement(XML_items); pPivotStrm->endElement(XML_pivotField); } pPivotStrm->endElement(XML_pivotFields); // if (!aRowFields.empty()) { pPivotStrm->startElement(XML_rowFields, XML_count, OString::number(static_cast(aRowFields.size()))); for (const auto& rRowField : aRowFields) { pPivotStrm->singleElement(XML_field, XML_x, OString::number(rRowField)); } pPivotStrm->endElement(XML_rowFields); } // // if (!aColFields.empty()) { pPivotStrm->startElement(XML_colFields, XML_count, OString::number(static_cast(aColFields.size()))); for (const auto& rColField : aColFields) { pPivotStrm->singleElement(XML_field, XML_x, OString::number(rColField)); } pPivotStrm->endElement(XML_colFields); } // // if (!aPageFields.empty()) { pPivotStrm->startElement(XML_pageFields, XML_count, OString::number(static_cast(aPageFields.size()))); for (const auto& rPageField : aPageFields) { pPivotStrm->singleElement(XML_pageField, XML_fld, OString::number(rPageField), XML_hier, OString::number(-1)); // TODO : handle this correctly. } pPivotStrm->endElement(XML_pageFields); } // if (!aDataFields.empty()) { css::uno::Reference xDimsByName; if (auto xDimSupplier = const_cast(rDPObj).GetSource()) xDimsByName = xDimSupplier->getDimensions(); pPivotStrm->startElement(XML_dataFields, XML_count, OString::number(static_cast(aDataFields.size()))); for (const auto& rDataField : aDataFields) { long nDimIdx = rDataField.mnPos; assert(aCachedDims[nDimIdx]); // the loop above should have screened for NULL's. const ScDPSaveDimension& rDim = *rDataField.mpDim; std::optional pName = rDim.GetLayoutName(); // tdf#124651: despite being optional in CT_DataField according to ECMA-376 Part 1, // Excel (at least 2016) seems to insist on the presence of "name" attribute in // dataField element. // tdf#124881: try to create a meaningful name; don't use empty string. if (!pName) pName = ScDPUtil::getDisplayedMeasureName( rDim.GetName(), ScDPUtil::toSubTotalFunc(rDim.GetFunction())); auto pItemAttList = sax_fastparser::FastSerializerHelper::createAttrList(); pItemAttList->add(XML_name, pName->toUtf8()); pItemAttList->add(XML_fld, OString::number(nDimIdx)); const char* pSubtotal = toOOXMLSubtotalType(rDim.GetFunction()); if (pSubtotal) pItemAttList->add(XML_subtotal, pSubtotal); if (xDimsByName) { try { css::uno::Reference xDimProps( xDimsByName->getByName(rDim.GetName()), uno::UNO_QUERY_THROW); css::uno::Any aVal = xDimProps->getPropertyValue(SC_UNONAME_NUMFMT); sal_uInt32 nScNumFmt = aVal.get(); sal_uInt16 nXclNumFmt = GetRoot().GetNumFmtBuffer().Insert(nScNumFmt); pItemAttList->add(XML_numFmtId, OString::number(nXclNumFmt)); } catch (uno::Exception&) { SAL_WARN("sc.filter", "Couldn't get number format for data field " << rDim.GetName()); // Just skip exporting number format } } sax_fastparser::XFastAttributeListRef xItemAttributeList(pItemAttList); pPivotStrm->singleElement(XML_dataField, xItemAttributeList); } pPivotStrm->endElement(XML_dataFields); } // Now add style info (use grab bag, or just a set which is default on Excel 2007 through 2016) if (const auto [bHas, aVal] = rDPObj.GetInteropGrabBagValue("pivotTableStyleInfo"); bHas) WriteGrabBagItemToStream(rStrm, XML_pivotTableStyleInfo, aVal); else pPivotStrm->singleElement(XML_pivotTableStyleInfo, XML_name, "PivotStyleLight16", XML_showRowHeaders, "1", XML_showColHeaders, "1", XML_showRowStripes, "0", XML_showColStripes, "0", XML_showLastColumn, "1"); OUString aBuf = "../pivotCache/pivotCacheDefinition" + OUString::number(nCacheId) + ".xml"; rStrm.addRelation( pPivotStrm->getOutputStream(), CREATE_OFFICEDOC_RELATION_TYPE("pivotCacheDefinition"), aBuf); pPivotStrm->endElement(XML_pivotTableDefinition); } void XclExpXmlPivotTables::AppendTable( const ScDPObject* pTable, sal_Int32 nCacheId, sal_Int32 nPivotId ) { maTables.emplace_back(pTable, nCacheId, nPivotId); } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */