diff options
Diffstat (limited to 'sc/source/filter/excel/xetable.cxx')
-rw-r--r-- | sc/source/filter/excel/xetable.cxx | 2841 |
1 files changed, 2841 insertions, 0 deletions
diff --git a/sc/source/filter/excel/xetable.cxx b/sc/source/filter/excel/xetable.cxx new file mode 100644 index 000000000..728ea2339 --- /dev/null +++ b/sc/source/filter/excel/xetable.cxx @@ -0,0 +1,2841 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <xetable.hxx> + +#include <map> +#include <numeric> +#include <com/sun/star/i18n/ScriptType.hpp> +#include <scitems.hxx> +#include <svl/intitem.hxx> +#include <svl/numformat.hxx> +#include <svl/stritem.hxx> +#include <tools/UnitConversion.hxx> +#include <editeng/flditem.hxx> +#include <document.hxx> +#include <dociter.hxx> +#include <olinetab.hxx> +#include <formulacell.hxx> +#include <patattr.hxx> +#include <attrib.hxx> +#include <xehelper.hxx> +#include <xecontent.hxx> +#include <xeescher.hxx> +#include <xeextlst.hxx> +#include <xeformula.hxx> +#include <xlcontent.hxx> +#include <xltools.hxx> +#include <tokenarray.hxx> +#include <formula/errorcodes.hxx> +#include <comphelper/threadpool.hxx> +#include <oox/token/tokens.hxx> +#include <oox/export/utils.hxx> + +using namespace ::oox; + +namespace ApiScriptType = ::com::sun::star::i18n::ScriptType; + +// Helper records for cell records + +XclExpStringRec::XclExpStringRec( const XclExpRoot& rRoot, const OUString& rResult ) : + XclExpRecord( EXC_ID3_STRING ), + mxResult( XclExpStringHelper::CreateString( rRoot, rResult ) ) +{ + OSL_ENSURE( (rRoot.GetBiff() <= EXC_BIFF5) || (mxResult->Len() > 0), + "XclExpStringRec::XclExpStringRec - empty result not allowed in BIFF8+" ); + SetRecSize( mxResult->GetSize() ); +} + +void XclExpStringRec::WriteBody( XclExpStream& rStrm ) +{ + rStrm << *mxResult; +} + +// Additional records for special formula ranges ============================== + +XclExpRangeFmlaBase::XclExpRangeFmlaBase( + sal_uInt16 nRecId, sal_uInt32 nRecSize, const ScAddress& rScPos ) : + XclExpRecord( nRecId, nRecSize ), + maXclRange( ScAddress::UNINITIALIZED ), + maBaseXclPos( ScAddress::UNINITIALIZED ) +{ + maBaseXclPos.Set( static_cast< sal_uInt16 >( rScPos.Col() ), static_cast< sal_uInt16 >( rScPos.Row() ) ); + maXclRange.maFirst = maXclRange.maLast = maBaseXclPos; +} + +XclExpRangeFmlaBase::XclExpRangeFmlaBase( + sal_uInt16 nRecId, sal_uInt32 nRecSize, const ScRange& rScRange ) : + XclExpRecord( nRecId, nRecSize ), + maXclRange( ScAddress::UNINITIALIZED ), + maBaseXclPos( ScAddress::UNINITIALIZED ) +{ + maXclRange.Set( + static_cast< sal_uInt16 >( rScRange.aStart.Col() ), + static_cast< sal_uInt16 >( rScRange.aStart.Row() ), + static_cast< sal_uInt16 >( rScRange.aEnd.Col() ), + static_cast< sal_uInt16 >( rScRange.aEnd.Row() ) ); + maBaseXclPos = maXclRange.maFirst; +} + +bool XclExpRangeFmlaBase::IsBasePos( sal_uInt16 nXclCol, sal_uInt32 nXclRow ) const +{ + return (maBaseXclPos.mnCol == nXclCol) && (maBaseXclPos.mnRow == nXclRow); +} + +void XclExpRangeFmlaBase::Extend( const ScAddress& rScPos ) +{ + sal_uInt16 nXclCol = static_cast< sal_uInt16 >( rScPos.Col() ); + sal_uInt32 nXclRow = static_cast< sal_uInt32 >( rScPos.Row() ); + maXclRange.maFirst.mnCol = ::std::min( maXclRange.maFirst.mnCol, nXclCol ); + maXclRange.maFirst.mnRow = ::std::min( maXclRange.maFirst.mnRow, nXclRow ); + maXclRange.maLast.mnCol = ::std::max( maXclRange.maLast.mnCol, nXclCol ); + maXclRange.maLast.mnRow = ::std::max( maXclRange.maLast.mnRow, nXclRow ); +} + +void XclExpRangeFmlaBase::WriteRangeAddress( XclExpStream& rStrm ) const +{ + maXclRange.Write( rStrm, false ); +} + +// Array formulas ============================================================= + +XclExpArray::XclExpArray( const XclTokenArrayRef& xTokArr, const ScRange& rScRange ) : + XclExpRangeFmlaBase( EXC_ID3_ARRAY, 14 + xTokArr->GetSize(), rScRange ), + mxTokArr( xTokArr ) +{ +} + +XclTokenArrayRef XclExpArray::CreateCellTokenArray( const XclExpRoot& rRoot ) const +{ + return rRoot.GetFormulaCompiler().CreateSpecialRefFormula( EXC_TOKID_EXP, maBaseXclPos ); +} + +bool XclExpArray::IsVolatile() const +{ + return mxTokArr->IsVolatile(); +} + +void XclExpArray::WriteBody( XclExpStream& rStrm ) +{ + WriteRangeAddress( rStrm ); + sal_uInt16 nFlags = EXC_ARRAY_DEFAULTFLAGS; + ::set_flag( nFlags, EXC_ARRAY_RECALC_ALWAYS, IsVolatile() ); + rStrm << nFlags << sal_uInt32( 0 ) << *mxTokArr; +} + +XclExpArrayBuffer::XclExpArrayBuffer( const XclExpRoot& rRoot ) : + XclExpRoot( rRoot ) +{ +} + +XclExpArrayRef XclExpArrayBuffer::CreateArray( const ScTokenArray& rScTokArr, const ScRange& rScRange ) +{ + const ScAddress& rScPos = rScRange.aStart; + XclTokenArrayRef xTokArr = GetFormulaCompiler().CreateFormula( EXC_FMLATYPE_MATRIX, rScTokArr, &rScPos ); + + OSL_ENSURE( maRecMap.find( rScPos ) == maRecMap.end(), "XclExpArrayBuffer::CreateArray - array exists already" ); + XclExpArrayRef& rxRec = maRecMap[ rScPos ]; + rxRec = new XclExpArray( xTokArr, rScRange ); + return rxRec; +} + +XclExpArrayRef XclExpArrayBuffer::FindArray( const ScTokenArray& rScTokArr, const ScAddress& rBasePos ) const +{ + XclExpArrayRef xRec; + // try to extract a matrix reference token + if (rScTokArr.GetLen() != 1) + // Must consist of a single reference token. + return xRec; + + const formula::FormulaToken* pToken = rScTokArr.GetArray()[0]; + if (!pToken || pToken->GetOpCode() != ocMatRef) + // not a matrix reference token. + return xRec; + + const ScSingleRefData& rRef = *pToken->GetSingleRef(); + ScAddress aAbsPos = rRef.toAbs(GetRoot().GetDoc(), rBasePos); + XclExpArrayMap::const_iterator it = maRecMap.find(aAbsPos); + + if (it != maRecMap.end()) + xRec = it->second; + return xRec; +} + +// Shared formulas ============================================================ + +XclExpShrfmla::XclExpShrfmla( const XclTokenArrayRef& xTokArr, const ScAddress& rScPos ) : + XclExpRangeFmlaBase( EXC_ID_SHRFMLA, 10 + xTokArr->GetSize(), rScPos ), + mxTokArr( xTokArr ), + mnUsedCount( 1 ) +{ +} + +void XclExpShrfmla::ExtendRange( const ScAddress& rScPos ) +{ + Extend( rScPos ); + ++mnUsedCount; +} + +XclTokenArrayRef XclExpShrfmla::CreateCellTokenArray( const XclExpRoot& rRoot ) const +{ + return rRoot.GetFormulaCompiler().CreateSpecialRefFormula( EXC_TOKID_EXP, maBaseXclPos ); +} + +bool XclExpShrfmla::IsVolatile() const +{ + return mxTokArr->IsVolatile(); +} + +void XclExpShrfmla::WriteBody( XclExpStream& rStrm ) +{ + WriteRangeAddress( rStrm ); + rStrm << sal_uInt8( 0 ) << mnUsedCount << *mxTokArr; +} + +XclExpShrfmlaBuffer::XclExpShrfmlaBuffer( const XclExpRoot& rRoot ) : + XclExpRoot( rRoot ) +{ +} + +bool XclExpShrfmlaBuffer::IsValidTokenArray( const ScTokenArray& rArray ) const +{ + using namespace formula; + + FormulaToken** pTokens = rArray.GetArray(); + sal_uInt16 nLen = rArray.GetLen(); + for (sal_uInt16 i = 0; i < nLen; ++i) + { + const FormulaToken* p = pTokens[i]; + switch (p->GetType()) + { + case svSingleRef: + { + const ScSingleRefData& rRefData = *p->GetSingleRef(); + if (!GetFormulaCompiler().IsRef2D(rRefData)) + // Excel's shared formula cannot include 3D reference. + return false; + } + break; + case svDoubleRef: + { + const ScComplexRefData& rRefData = *p->GetDoubleRef(); + if (!GetFormulaCompiler().IsRef2D(rRefData)) + // Excel's shared formula cannot include 3D reference. + return false; + } + break; + case svExternalSingleRef: + case svExternalDoubleRef: + case svExternalName: + // External references aren't allowed. + return false; + default: + ; + } + } + return true; +} + +XclExpShrfmlaRef XclExpShrfmlaBuffer::CreateOrExtendShrfmla( + const ScFormulaCell& rScCell, const ScAddress& rScPos ) +{ + XclExpShrfmlaRef xRec; + const ScTokenArray* pShrdScTokArr = rScCell.GetSharedCode(); + if (!pShrdScTokArr) + // This formula cell is not shared formula cell. + return xRec; + + // Check to see if this shared formula contains any tokens that Excel's shared formula cannot handle. + if (maBadTokens.count(pShrdScTokArr) > 0) + // Already on the black list. Skip it. + return xRec; + + if (!IsValidTokenArray(*pShrdScTokArr)) + { + // We can't export this as shared formula. + maBadTokens.insert(pShrdScTokArr); + return xRec; + } + + TokensType::iterator aIt = maRecMap.find(pShrdScTokArr); + if( aIt == maRecMap.end() ) + { + // create a new record + XclTokenArrayRef xTokArr = GetFormulaCompiler().CreateFormula( EXC_FMLATYPE_SHARED, *pShrdScTokArr, &rScPos ); + xRec = new XclExpShrfmla( xTokArr, rScPos ); + maRecMap[ pShrdScTokArr ] = xRec; + } + else + { + // extend existing record + OSL_ENSURE( aIt->second, "XclExpShrfmlaBuffer::CreateOrExtendShrfmla - missing record" ); + xRec = aIt->second; + xRec->ExtendRange( rScPos ); + } + + return xRec; +} + +// Multiple operations ======================================================== + +XclExpTableop::XclExpTableop( const ScAddress& rScPos, + const XclMultipleOpRefs& rRefs, sal_uInt8 nScMode ) : + XclExpRangeFmlaBase( EXC_ID3_TABLEOP, 16, rScPos ), + mnLastAppXclCol( static_cast< sal_uInt16 >( rScPos.Col() ) ), + mnColInpXclCol( static_cast< sal_uInt16 >( rRefs.maColFirstScPos.Col() ) ), + mnColInpXclRow( static_cast< sal_uInt16 >( rRefs.maColFirstScPos.Row() ) ), + mnRowInpXclCol( static_cast< sal_uInt16 >( rRefs.maRowFirstScPos.Col() ) ), + mnRowInpXclRow( static_cast< sal_uInt16 >( rRefs.maRowFirstScPos.Row() ) ), + mnScMode( nScMode ), + mbValid( false ) +{ +} + +bool XclExpTableop::TryExtend( const ScAddress& rScPos, const XclMultipleOpRefs& rRefs ) +{ + sal_uInt16 nXclCol = static_cast< sal_uInt16 >( rScPos.Col() ); + sal_uInt16 nXclRow = static_cast< sal_uInt16 >( rScPos.Row() ); + + bool bOk = IsAppendable( nXclCol, nXclRow ); + if( bOk ) + { + SCCOL nFirstScCol = static_cast< SCCOL >( maXclRange.maFirst.mnCol ); + SCROW nFirstScRow = static_cast< SCROW >( maXclRange.maFirst.mnRow ); + SCCOL nColInpScCol = static_cast< SCCOL >( mnColInpXclCol ); + SCROW nColInpScRow = static_cast< SCROW >( mnColInpXclRow ); + SCCOL nRowInpScCol = static_cast< SCCOL >( mnRowInpXclCol ); + SCROW nRowInpScRow = static_cast< SCROW >( mnRowInpXclRow ); + + bOk = ((mnScMode == 2) == rRefs.mbDblRefMode) && + (rScPos.Tab() == rRefs.maFmlaScPos.Tab()) && + (nColInpScCol == rRefs.maColFirstScPos.Col()) && + (nColInpScRow == rRefs.maColFirstScPos.Row()) && + (rScPos.Tab() == rRefs.maColFirstScPos.Tab()) && + (rScPos.Tab() == rRefs.maColRelScPos.Tab()); + + if( bOk ) switch( mnScMode ) + { + case 0: + bOk = (rScPos.Col() == rRefs.maFmlaScPos.Col()) && + (nFirstScRow == rRefs.maFmlaScPos.Row() + 1) && + (nFirstScCol == rRefs.maColRelScPos.Col() + 1) && + (rScPos.Row() == rRefs.maColRelScPos.Row()); + break; + case 1: + bOk = (nFirstScCol == rRefs.maFmlaScPos.Col() + 1) && + (rScPos.Row() == rRefs.maFmlaScPos.Row()) && + (rScPos.Col() == rRefs.maColRelScPos.Col()) && + (nFirstScRow == rRefs.maColRelScPos.Row() + 1); + break; + case 2: + bOk = (nFirstScCol == rRefs.maFmlaScPos.Col() + 1) && + (nFirstScRow == rRefs.maFmlaScPos.Row() + 1) && + (nFirstScCol == rRefs.maColRelScPos.Col() + 1) && + (rScPos.Row() == rRefs.maColRelScPos.Row()) && + (nRowInpScCol == rRefs.maRowFirstScPos.Col()) && + (nRowInpScRow == rRefs.maRowFirstScPos.Row()) && + (rScPos.Tab() == rRefs.maRowFirstScPos.Tab()) && + (rScPos.Col() == rRefs.maRowRelScPos.Col()) && + (nFirstScRow == rRefs.maRowRelScPos.Row() + 1) && + (rScPos.Tab() == rRefs.maRowRelScPos.Tab()); + break; + default: + bOk = false; + } + + if( bOk ) + { + // extend the cell range + OSL_ENSURE( IsAppendable( nXclCol, nXclRow ), "XclExpTableop::TryExtend - wrong cell address" ); + Extend( rScPos ); + mnLastAppXclCol = nXclCol; + } + } + + return bOk; +} + +void XclExpTableop::Finalize() +{ + // is the range complete? (last appended cell is in last column) + mbValid = maXclRange.maLast.mnCol == mnLastAppXclCol; + // if last row is incomplete, try to shorten the used range + if( !mbValid && (maXclRange.maFirst.mnRow < maXclRange.maLast.mnRow) ) + { + --maXclRange.maLast.mnRow; + mbValid = true; + } + + // check if referred cells are outside of own range + if( !mbValid ) + return; + + switch( mnScMode ) + { + case 0: + mbValid = (mnColInpXclCol + 1 < maXclRange.maFirst.mnCol) || (mnColInpXclCol > maXclRange.maLast.mnCol) || + (mnColInpXclRow < maXclRange.maFirst.mnRow) || (mnColInpXclRow > maXclRange.maLast.mnRow); + break; + case 1: + mbValid = (mnColInpXclCol < maXclRange.maFirst.mnCol) || (mnColInpXclCol > maXclRange.maLast.mnCol) || + (mnColInpXclRow + 1 < maXclRange.maFirst.mnRow) || (mnColInpXclRow > maXclRange.maLast.mnRow); + break; + case 2: + mbValid = ((mnColInpXclCol + 1 < maXclRange.maFirst.mnCol) || (mnColInpXclCol > maXclRange.maLast.mnCol) || + (mnColInpXclRow + 1 < maXclRange.maFirst.mnRow) || (mnColInpXclRow > maXclRange.maLast.mnRow)) && + ((mnRowInpXclCol + 1 < maXclRange.maFirst.mnCol) || (mnRowInpXclCol > maXclRange.maLast.mnCol) || + (mnRowInpXclRow + 1 < maXclRange.maFirst.mnRow) || (mnRowInpXclRow > maXclRange.maLast.mnRow)); + break; + } +} + +XclTokenArrayRef XclExpTableop::CreateCellTokenArray( const XclExpRoot& rRoot ) const +{ + XclExpFormulaCompiler& rFmlaComp = rRoot.GetFormulaCompiler(); + return mbValid ? + rFmlaComp.CreateSpecialRefFormula( EXC_TOKID_TBL, maBaseXclPos ) : + rFmlaComp.CreateErrorFormula( EXC_ERR_NA ); +} + +bool XclExpTableop::IsVolatile() const +{ + return true; +} + +void XclExpTableop::Save( XclExpStream& rStrm ) +{ + if( mbValid ) + XclExpRangeFmlaBase::Save( rStrm ); +} + +bool XclExpTableop::IsAppendable( sal_uInt16 nXclCol, sal_uInt16 nXclRow ) const +{ + return ((nXclCol == mnLastAppXclCol + 1) && (nXclRow == maXclRange.maFirst.mnRow)) || + ((nXclCol == mnLastAppXclCol + 1) && (nXclCol <= maXclRange.maLast.mnCol) && (nXclRow == maXclRange.maLast.mnRow)) || + ((mnLastAppXclCol == maXclRange.maLast.mnCol) && (nXclCol == maXclRange.maFirst.mnCol) && (nXclRow == maXclRange.maLast.mnRow + 1)); +} + +void XclExpTableop::WriteBody( XclExpStream& rStrm ) +{ + sal_uInt16 nFlags = EXC_TABLEOP_DEFAULTFLAGS; + ::set_flag( nFlags, EXC_TABLEOP_RECALC_ALWAYS, IsVolatile() ); + switch( mnScMode ) + { + case 1: ::set_flag( nFlags, EXC_TABLEOP_ROW ); break; + case 2: ::set_flag( nFlags, EXC_TABLEOP_BOTH ); break; + } + + WriteRangeAddress( rStrm ); + rStrm << nFlags; + if( mnScMode == 2 ) + rStrm << mnRowInpXclRow << mnRowInpXclCol << mnColInpXclRow << mnColInpXclCol; + else + rStrm << mnColInpXclRow << mnColInpXclCol << sal_uInt32( 0 ); +} + +XclExpTableopBuffer::XclExpTableopBuffer( const XclExpRoot& rRoot ) : + XclExpRoot( rRoot ) +{ +} + +XclExpTableopRef XclExpTableopBuffer::CreateOrExtendTableop( + const ScTokenArray& rScTokArr, const ScAddress& rScPos ) +{ + XclExpTableopRef xRec; + + // try to extract cell references of a multiple operations formula + XclMultipleOpRefs aRefs; + if (XclTokenArrayHelper::GetMultipleOpRefs(GetDoc(), aRefs, rScTokArr, rScPos)) + { + // try to find an existing TABLEOP record for this cell position + for( size_t nPos = 0, nSize = maTableopList.GetSize(); !xRec && (nPos < nSize); ++nPos ) + { + XclExpTableop* xTempRec = maTableopList.GetRecord( nPos ); + if( xTempRec->TryExtend( rScPos, aRefs ) ) + xRec = xTempRec; + } + + // no record found, or found record not extensible + if( !xRec ) + xRec = TryCreate( rScPos, aRefs ); + } + + return xRec; +} + +void XclExpTableopBuffer::Finalize() +{ + for( size_t nPos = 0, nSize = maTableopList.GetSize(); nPos < nSize; ++nPos ) + maTableopList.GetRecord( nPos )->Finalize(); +} + +XclExpTableopRef XclExpTableopBuffer::TryCreate( const ScAddress& rScPos, const XclMultipleOpRefs& rRefs ) +{ + sal_uInt8 nScMode = 0; + bool bOk = (rScPos.Tab() == rRefs.maFmlaScPos.Tab()) && + (rScPos.Tab() == rRefs.maColFirstScPos.Tab()) && + (rScPos.Tab() == rRefs.maColRelScPos.Tab()); + + if( bOk ) + { + if( rRefs.mbDblRefMode ) + { + nScMode = 2; + bOk = (rScPos.Col() == rRefs.maFmlaScPos.Col() + 1) && + (rScPos.Row() == rRefs.maFmlaScPos.Row() + 1) && + (rScPos.Col() == rRefs.maColRelScPos.Col() + 1) && + (rScPos.Row() == rRefs.maColRelScPos.Row()) && + (rScPos.Tab() == rRefs.maRowFirstScPos.Tab()) && + (rScPos.Col() == rRefs.maRowRelScPos.Col()) && + (rScPos.Row() == rRefs.maRowRelScPos.Row() + 1) && + (rScPos.Tab() == rRefs.maRowRelScPos.Tab()); + } + else if( (rScPos.Col() == rRefs.maFmlaScPos.Col()) && + (rScPos.Row() == rRefs.maFmlaScPos.Row() + 1) && + (rScPos.Col() == rRefs.maColRelScPos.Col() + 1) && + (rScPos.Row() == rRefs.maColRelScPos.Row()) ) + { + nScMode = 0; + } + else if( (rScPos.Col() == rRefs.maFmlaScPos.Col() + 1) && + (rScPos.Row() == rRefs.maFmlaScPos.Row()) && + (rScPos.Col() == rRefs.maColRelScPos.Col()) && + (rScPos.Row() == rRefs.maColRelScPos.Row() + 1) ) + { + nScMode = 1; + } + else + { + bOk = false; + } + } + + XclExpTableopRef xRec; + if( bOk ) + { + xRec = new XclExpTableop( rScPos, rRefs, nScMode ); + maTableopList.AppendRecord( xRec ); + } + + return xRec; +} + +// Cell records + +XclExpCellBase::XclExpCellBase( + sal_uInt16 nRecId, std::size_t nContSize, const XclAddress& rXclPos ) : + XclExpRecord( nRecId, nContSize + 4 ), + maXclPos( rXclPos ) +{ +} + +bool XclExpCellBase::IsMultiLineText() const +{ + return false; +} + +bool XclExpCellBase::TryMerge( const XclExpCellBase& /*rCell*/ ) +{ + return false; +} + +void XclExpCellBase::GetBlankXFIndexes( ScfUInt16Vec& /*rXFIndexes*/ ) const +{ + // default: do nothing +} + +void XclExpCellBase::RemoveUnusedBlankCells( const ScfUInt16Vec& /*rXFIndexes*/, size_t /*nStartAllNotFound*/ ) +{ + // default: do nothing +} + +// Single cell records ======================================================== + +XclExpSingleCellBase::XclExpSingleCellBase( + sal_uInt16 nRecId, std::size_t nContSize, const XclAddress& rXclPos, sal_uInt32 nXFId ) : + XclExpCellBase( nRecId, 2, rXclPos ), + maXFId( nXFId ), + mnContSize( nContSize ) +{ +} + +XclExpSingleCellBase::XclExpSingleCellBase( const XclExpRoot& rRoot, + sal_uInt16 nRecId, std::size_t nContSize, const XclAddress& rXclPos, + const ScPatternAttr* pPattern, sal_Int16 nScript, sal_uInt32 nForcedXFId ) : + XclExpCellBase( nRecId, 2, rXclPos ), + maXFId( nForcedXFId ), + mnContSize( nContSize ) +{ + if( GetXFId() == EXC_XFID_NOTFOUND ) + SetXFId( rRoot.GetXFBuffer().Insert( pPattern, nScript ) ); +} + +sal_uInt16 XclExpSingleCellBase::GetLastXclCol() const +{ + return GetXclCol(); +} + +sal_uInt32 XclExpSingleCellBase::GetFirstXFId() const +{ + return GetXFId(); +} + +bool XclExpSingleCellBase::IsEmpty() const +{ + return false; +} + +void XclExpSingleCellBase::ConvertXFIndexes( const XclExpRoot& rRoot ) +{ + maXFId.ConvertXFIndex( rRoot ); +} + +void XclExpSingleCellBase::Save( XclExpStream& rStrm ) +{ + OSL_ENSURE_BIFF( rStrm.GetRoot().GetBiff() >= EXC_BIFF3 ); + AddRecSize( mnContSize ); + XclExpCellBase::Save( rStrm ); +} + +void XclExpSingleCellBase::WriteBody( XclExpStream& rStrm ) +{ + rStrm << static_cast<sal_uInt16> (GetXclRow()) << GetXclCol() << maXFId.mnXFIndex; + WriteContents( rStrm ); +} + +XclExpNumberCell::XclExpNumberCell( + const XclExpRoot& rRoot, const XclAddress& rXclPos, + const ScPatternAttr* pPattern, sal_uInt32 nForcedXFId, double fValue ) : + // #i41210# always use latin script for number cells - may look wrong for special number formats... + XclExpSingleCellBase( rRoot, EXC_ID3_NUMBER, 8, rXclPos, pPattern, ApiScriptType::LATIN, nForcedXFId ), + mfValue( fValue ) +{ +} + +static OString lcl_GetStyleId( const XclExpXmlStream& rStrm, sal_uInt32 nXFIndex ) +{ + return OString::number( rStrm.GetRoot().GetXFBuffer() + .GetXmlCellIndex( nXFIndex ) ); +} + +static OString lcl_GetStyleId( const XclExpXmlStream& rStrm, const XclExpCellBase& rCell ) +{ + sal_uInt32 nXFId = rCell.GetFirstXFId(); + sal_uInt16 nXFIndex = rStrm.GetRoot().GetXFBuffer().GetXFIndex( nXFId ); + return lcl_GetStyleId( rStrm, nXFIndex ); +} + +void XclExpNumberCell::SaveXml( XclExpXmlStream& rStrm ) +{ + sax_fastparser::FSHelperPtr& rWorksheet = rStrm.GetCurrentStream(); + rWorksheet->startElement( XML_c, + XML_r, XclXmlUtils::ToOString(rStrm.GetRoot().GetStringBuf(), GetXclPos()).getStr(), + XML_s, lcl_GetStyleId(rStrm, *this), + XML_t, "n" + // OOXTODO: XML_cm, XML_vm, XML_ph + ); + rWorksheet->startElement(XML_v); + rWorksheet->write( mfValue ); + rWorksheet->endElement( XML_v ); + rWorksheet->endElement( XML_c ); +} + +void XclExpNumberCell::WriteContents( XclExpStream& rStrm ) +{ + rStrm << mfValue; +} + +XclExpBooleanCell::XclExpBooleanCell( + const XclExpRoot& rRoot, const XclAddress& rXclPos, + const ScPatternAttr* pPattern, sal_uInt32 nForcedXFId, bool bValue ) : + // #i41210# always use latin script for boolean cells + XclExpSingleCellBase( rRoot, EXC_ID3_BOOLERR, 2, rXclPos, pPattern, ApiScriptType::LATIN, nForcedXFId ), + mbValue( bValue ) +{ +} + +void XclExpBooleanCell::SaveXml( XclExpXmlStream& rStrm ) +{ + sax_fastparser::FSHelperPtr& rWorksheet = rStrm.GetCurrentStream(); + rWorksheet->startElement( XML_c, + XML_r, XclXmlUtils::ToOString(rStrm.GetRoot().GetStringBuf(), GetXclPos()).getStr(), + XML_s, lcl_GetStyleId(rStrm, *this), + XML_t, "b" + // OOXTODO: XML_cm, XML_vm, XML_ph + ); + rWorksheet->startElement( XML_v ); + rWorksheet->write( mbValue ? "1" : "0" ); + rWorksheet->endElement( XML_v ); + rWorksheet->endElement( XML_c ); +} + +void XclExpBooleanCell::WriteContents( XclExpStream& rStrm ) +{ + rStrm << sal_uInt16( mbValue ? 1 : 0 ) << EXC_BOOLERR_BOOL; +} + +XclExpLabelCell::XclExpLabelCell( + const XclExpRoot& rRoot, const XclAddress& rXclPos, + const ScPatternAttr* pPattern, sal_uInt32 nForcedXFId, const OUString& rStr ) : + XclExpSingleCellBase( EXC_ID3_LABEL, 0, rXclPos, nForcedXFId ) +{ + sal_uInt16 nMaxLen = (rRoot.GetBiff() == EXC_BIFF8) ? EXC_STR_MAXLEN : EXC_LABEL_MAXLEN; + XclExpStringRef xText = XclExpStringHelper::CreateCellString( + rRoot, rStr, pPattern, XclStrFlags::NONE, nMaxLen); + Init( rRoot, pPattern, xText ); +} + +XclExpLabelCell::XclExpLabelCell( + const XclExpRoot& rRoot, const XclAddress& rXclPos, + const ScPatternAttr* pPattern, sal_uInt32 nForcedXFId, + const EditTextObject* pEditText, XclExpHyperlinkHelper& rLinkHelper ) : + XclExpSingleCellBase( EXC_ID3_LABEL, 0, rXclPos, nForcedXFId ) +{ + sal_uInt16 nMaxLen = (rRoot.GetBiff() == EXC_BIFF8) ? EXC_STR_MAXLEN : EXC_LABEL_MAXLEN; + + XclExpStringRef xText; + if (pEditText) + xText = XclExpStringHelper::CreateCellString( + rRoot, *pEditText, pPattern, rLinkHelper, XclStrFlags::NONE, nMaxLen); + else + xText = XclExpStringHelper::CreateCellString( + rRoot, OUString(), pPattern, XclStrFlags::NONE, nMaxLen); + + Init( rRoot, pPattern, xText ); +} + +bool XclExpLabelCell::IsMultiLineText() const +{ + return mbLineBreak || mxText->IsWrapped(); +} + +void XclExpLabelCell::Init( const XclExpRoot& rRoot, + const ScPatternAttr* pPattern, XclExpStringRef const & xText ) +{ + OSL_ENSURE( xText && xText->Len(), "XclExpLabelCell::XclExpLabelCell - empty string passed" ); + mxText = xText; + mnSstIndex = 0; + + const XclFormatRunVec& rFormats = mxText->GetFormats(); + // remove formatting of the leading run if the entire string + // is equally formatted + sal_uInt16 nXclFont = EXC_FONT_NOTFOUND; + if( rFormats.size() == 1 ) + nXclFont = mxText->RemoveLeadingFont(); + else + nXclFont = mxText->GetLeadingFont(); + + // create cell format + if( GetXFId() == EXC_XFID_NOTFOUND ) + { + OSL_ENSURE( nXclFont != EXC_FONT_NOTFOUND, "XclExpLabelCell::Init - leading font not found" ); + bool bForceLineBreak = mxText->IsWrapped(); + SetXFId( rRoot.GetXFBuffer().InsertWithFont( pPattern, ApiScriptType::WEAK, nXclFont, bForceLineBreak ) ); + } + + // get auto-wrap attribute from cell format + const XclExpXF* pXF = rRoot.GetXFBuffer().GetXFById( GetXFId() ); + mbLineBreak = pXF && pXF->GetAlignmentData().mbLineBreak; + + // initialize the record contents + switch( rRoot.GetBiff() ) + { + case EXC_BIFF5: + // BIFF5-BIFF7: create a LABEL or RSTRING record + OSL_ENSURE( mxText->Len() <= EXC_LABEL_MAXLEN, "XclExpLabelCell::XclExpLabelCell - string too long" ); + SetContSize( mxText->GetSize() ); + // formatted string is exported in an RSTRING record + if( mxText->IsRich() ) + { + OSL_ENSURE( mxText->GetFormatsCount() <= EXC_LABEL_MAXLEN, "XclExpLabelCell::WriteContents - too many formats" ); + mxText->LimitFormatCount( EXC_LABEL_MAXLEN ); + SetRecId( EXC_ID_RSTRING ); + SetContSize( GetContSize() + 1 + 2 * mxText->GetFormatsCount() ); + } + break; + case EXC_BIFF8: + // BIFF8+: create a LABELSST record + mnSstIndex = rRoot.GetSst().Insert( xText ); + SetRecId( EXC_ID_LABELSST ); + SetContSize( 4 ); + break; + default: DBG_ERROR_BIFF(); + } +} + +void XclExpLabelCell::SaveXml( XclExpXmlStream& rStrm ) +{ + sax_fastparser::FSHelperPtr& rWorksheet = rStrm.GetCurrentStream(); + rWorksheet->startElement( XML_c, + XML_r, XclXmlUtils::ToOString(rStrm.GetRoot().GetStringBuf(), GetXclPos()).getStr(), + XML_s, lcl_GetStyleId(rStrm, *this), + XML_t, "s" + // OOXTODO: XML_cm, XML_vm, XML_ph + ); + rWorksheet->startElement( XML_v ); + rWorksheet->write( static_cast<sal_Int32>(mnSstIndex) ); + rWorksheet->endElement( XML_v ); + rWorksheet->endElement( XML_c ); +} + +void XclExpLabelCell::WriteContents( XclExpStream& rStrm ) +{ + switch( rStrm.GetRoot().GetBiff() ) + { + case EXC_BIFF5: + rStrm << *mxText; + if( mxText->IsRich() ) + { + rStrm << static_cast< sal_uInt8 >( mxText->GetFormatsCount() ); + mxText->WriteFormats( rStrm ); + } + break; + case EXC_BIFF8: + rStrm << mnSstIndex; + break; + default: DBG_ERROR_BIFF(); + } +} + +XclExpFormulaCell::XclExpFormulaCell( + const XclExpRoot& rRoot, const XclAddress& rXclPos, + const ScPatternAttr* pPattern, sal_uInt32 nForcedXFId, + const ScFormulaCell& rScFmlaCell, + XclExpArrayBuffer& rArrayBfr, + XclExpShrfmlaBuffer& rShrfmlaBfr, + XclExpTableopBuffer& rTableopBfr ) : + XclExpSingleCellBase( EXC_ID2_FORMULA, 0, rXclPos, nForcedXFId ), + mrScFmlaCell( const_cast< ScFormulaCell& >( rScFmlaCell ) ) +{ + // *** Find result number format overwriting cell number format *** ------- + + if( GetXFId() == EXC_XFID_NOTFOUND ) + { + SvNumberFormatter& rFormatter = rRoot.GetFormatter(); + XclExpNumFmtBuffer& rNumFmtBfr = rRoot.GetNumFmtBuffer(); + + // current cell number format + sal_uInt32 nScNumFmt = pPattern ? + pPattern->GetItemSet().Get( ATTR_VALUE_FORMAT ).GetValue() : + rNumFmtBfr.GetStandardFormat(); + + // alternative number format passed to XF buffer + sal_uInt32 nAltScNumFmt = NUMBERFORMAT_ENTRY_NOT_FOUND; + /* Xcl doesn't know Boolean number formats, we write + "TRUE";"FALSE" (language dependent). Don't do it for automatic + formula formats, because Excel gets them right. */ + /* #i8640# Don't set text format, if we have string results. */ + SvNumFormatType nFormatType = mrScFmlaCell.GetFormatType(); + if( ((nScNumFmt % SV_COUNTRY_LANGUAGE_OFFSET) == 0) && + (nFormatType != SvNumFormatType::LOGICAL) && + (nFormatType != SvNumFormatType::TEXT) ) + nAltScNumFmt = nScNumFmt; + /* If cell number format is Boolean and automatic formula + format is Boolean don't write that ugly special format. */ + else if( (nFormatType == SvNumFormatType::LOGICAL) && + (rFormatter.GetType( nScNumFmt ) == SvNumFormatType::LOGICAL) ) + nAltScNumFmt = rNumFmtBfr.GetStandardFormat(); + + // #i41420# find script type according to result type (always latin for numeric results) + sal_Int16 nScript = ApiScriptType::LATIN; + bool bForceLineBreak = false; + if( nFormatType == SvNumFormatType::TEXT ) + { + OUString aResult = mrScFmlaCell.GetString().getString(); + bForceLineBreak = mrScFmlaCell.IsMultilineResult(); + nScript = XclExpStringHelper::GetLeadingScriptType( rRoot, aResult ); + } + SetXFId( rRoot.GetXFBuffer().InsertWithNumFmt( pPattern, nScript, nAltScNumFmt, bForceLineBreak ) ); + } + + // *** Convert the formula token array *** -------------------------------- + + ScAddress aScPos( static_cast< SCCOL >( rXclPos.mnCol ), static_cast< SCROW >( rXclPos.mnRow ), rRoot.GetCurrScTab() ); + const ScTokenArray& rScTokArr = *mrScFmlaCell.GetCode(); + + // first try to create multiple operations + mxAddRec = rTableopBfr.CreateOrExtendTableop( rScTokArr, aScPos ); + + // no multiple operation found - try to create matrix formula + if( !mxAddRec ) + switch( mrScFmlaCell.GetMatrixFlag() ) + { + case ScMatrixMode::Formula: + { + // origin of the matrix - find the used matrix range + SCCOL nMatWidth; + SCROW nMatHeight; + mrScFmlaCell.GetMatColsRows( nMatWidth, nMatHeight ); + OSL_ENSURE( nMatWidth && nMatHeight, "XclExpFormulaCell::XclExpFormulaCell - empty matrix" ); + ScRange aMatScRange( aScPos ); + ScAddress& rMatEnd = aMatScRange.aEnd; + rMatEnd.IncCol( static_cast< SCCOL >( nMatWidth - 1 ) ); + rMatEnd.IncRow( static_cast< SCROW >( nMatHeight - 1 ) ); + // reduce to valid range (range keeps valid, because start position IS valid) + rRoot.GetAddressConverter().ValidateRange( aMatScRange, true ); + // create the ARRAY record + mxAddRec = rArrayBfr.CreateArray( rScTokArr, aMatScRange ); + } + break; + case ScMatrixMode::Reference: + { + // other formula cell covered by a matrix - find the ARRAY record + mxAddRec = rArrayBfr.FindArray(rScTokArr, aScPos); + // should always be found, if Calc document is not broken + OSL_ENSURE( mxAddRec, "XclExpFormulaCell::XclExpFormulaCell - no matrix found" ); + } + break; + default:; + } + + // no matrix found - try to create shared formula + if( !mxAddRec ) + mxAddRec = rShrfmlaBfr.CreateOrExtendShrfmla(mrScFmlaCell, aScPos); + + // no shared formula found - create a simple cell formula + if( !mxAddRec ) + mxTokArr = rRoot.GetFormulaCompiler().CreateFormula( EXC_FMLATYPE_CELL, rScTokArr, &aScPos ); +} + +void XclExpFormulaCell::Save( XclExpStream& rStrm ) +{ + // create token array for FORMULA cells with additional record + if( mxAddRec ) + mxTokArr = mxAddRec->CreateCellTokenArray( rStrm.GetRoot() ); + + // FORMULA record itself + OSL_ENSURE( mxTokArr, "XclExpFormulaCell::Save - missing token array" ); + if( !mxTokArr ) + mxTokArr = rStrm.GetRoot().GetFormulaCompiler().CreateErrorFormula( EXC_ERR_NA ); + SetContSize( 16 + mxTokArr->GetSize() ); + XclExpSingleCellBase::Save( rStrm ); + + // additional record (ARRAY, SHRFMLA, or TABLEOP), only for first FORMULA record + if( mxAddRec && mxAddRec->IsBasePos( GetXclCol(), GetXclRow() ) ) + mxAddRec->Save( rStrm ); + + // STRING record for string result + if( mxStringRec ) + mxStringRec->Save( rStrm ); +} + +void XclExpFormulaCell::SaveXml( XclExpXmlStream& rStrm ) +{ + const char* sType = nullptr; + OUString sValue; + XclXmlUtils::GetFormulaTypeAndValue( mrScFmlaCell, sType, sValue ); + sax_fastparser::FSHelperPtr& rWorksheet = rStrm.GetCurrentStream(); + rWorksheet->startElement( XML_c, + XML_r, XclXmlUtils::ToOString(rStrm.GetRoot().GetStringBuf(), GetXclPos()).getStr(), + XML_s, lcl_GetStyleId(rStrm, *this), + XML_t, sType + // OOXTODO: XML_cm, XML_vm, XML_ph + ); + + bool bWriteFormula = true; + bool bTagStarted = false; + ScAddress aScPos( static_cast< SCCOL >( GetXclPos().mnCol ), + static_cast< SCROW >( GetXclPos().mnRow ), rStrm.GetRoot().GetCurrScTab() ); + + switch (mrScFmlaCell.GetMatrixFlag()) + { + case ScMatrixMode::NONE: + break; + case ScMatrixMode::Reference: + bWriteFormula = false; + break; + case ScMatrixMode::Formula: + { + // origin of the matrix - find the used matrix range + SCCOL nMatWidth; + SCROW nMatHeight; + mrScFmlaCell.GetMatColsRows( nMatWidth, nMatHeight ); + OSL_ENSURE( nMatWidth && nMatHeight, "XclExpFormulaCell::XclExpFormulaCell - empty matrix" ); + ScRange aMatScRange( aScPos ); + ScAddress& rMatEnd = aMatScRange.aEnd; + rMatEnd.IncCol( static_cast< SCCOL >( nMatWidth - 1 ) ); + rMatEnd.IncRow( static_cast< SCROW >( nMatHeight - 1 ) ); + // reduce to valid range (range keeps valid, because start position IS valid + rStrm.GetRoot().GetAddressConverter().ValidateRange( aMatScRange, true ); + + OStringBuffer sFmlaCellRange; + if (rStrm.GetRoot().GetDoc().ValidRange(aMatScRange)) + { + // calculate the cell range. + sFmlaCellRange.append( XclXmlUtils::ToOString( + rStrm.GetRoot().GetStringBuf(), aMatScRange.aStart ).getStr()); + sFmlaCellRange.append(":"); + sFmlaCellRange.append( XclXmlUtils::ToOString( + rStrm.GetRoot().GetStringBuf(), aMatScRange.aEnd ).getStr()); + } + + if ( aMatScRange.aStart.Col() == GetXclPos().mnCol && + aMatScRange.aStart.Row() == static_cast<SCROW>(GetXclPos().mnRow)) + { + rWorksheet->startElement( XML_f, + XML_aca, ToPsz( (mxTokArr && mxTokArr->IsVolatile()) || + (mxAddRec && mxAddRec->IsVolatile())), + XML_t, mxAddRec ? "array" : nullptr, + XML_ref, !sFmlaCellRange.isEmpty()? sFmlaCellRange.getStr() : nullptr + // OOXTODO: XML_dt2D, bool + // OOXTODO: XML_dtr, bool + // OOXTODO: XML_del1, bool + // OOXTODO: XML_del2, bool + // OOXTODO: XML_r1, ST_CellRef + // OOXTODO: XML_r2, ST_CellRef + // OOXTODO: XML_ca, bool + // OOXTODO: XML_si, uint + // OOXTODO: XML_bx bool + ); + bTagStarted = true; + } + } + break; + } + + if (bWriteFormula) + { + if (!bTagStarted) + { + rWorksheet->startElement( XML_f, + XML_aca, ToPsz( (mxTokArr && mxTokArr->IsVolatile()) || + (mxAddRec && mxAddRec->IsVolatile()) ) ); + } + rWorksheet->writeEscaped( XclXmlUtils::ToOUString( + rStrm.GetRoot().GetCompileFormulaContext(), mrScFmlaCell.aPos, mrScFmlaCell.GetCode(), + mrScFmlaCell.GetErrCode())); + rWorksheet->endElement( XML_f ); + } + + if( strcmp( sType, "inlineStr" ) == 0 ) + { + rWorksheet->startElement(XML_is); + rWorksheet->startElement(XML_t); + rWorksheet->writeEscaped( sValue ); + rWorksheet->endElement( XML_t ); + rWorksheet->endElement( XML_is ); + } + else + { + rWorksheet->startElement(XML_v); + rWorksheet->writeEscaped( sValue ); + rWorksheet->endElement( XML_v ); + } + rWorksheet->endElement( XML_c ); +} + +void XclExpFormulaCell::WriteContents( XclExpStream& rStrm ) +{ + FormulaError nScErrCode = mrScFmlaCell.GetErrCode(); + if( nScErrCode != FormulaError::NONE ) + { + rStrm << EXC_FORMULA_RES_ERROR << sal_uInt8( 0 ) + << XclTools::GetXclErrorCode( nScErrCode ) + << sal_uInt8( 0 ) << sal_uInt16( 0 ) + << sal_uInt16( 0xFFFF ); + } + else + { + // result of the formula + switch( mrScFmlaCell.GetFormatType() ) + { + case SvNumFormatType::NUMBER: + { + // either value or error code + rStrm << mrScFmlaCell.GetValue(); + } + break; + + case SvNumFormatType::TEXT: + { + OUString aResult = mrScFmlaCell.GetString().getString(); + if( !aResult.isEmpty() || (rStrm.GetRoot().GetBiff() <= EXC_BIFF5) ) + { + rStrm << EXC_FORMULA_RES_STRING; + mxStringRec = new XclExpStringRec( rStrm.GetRoot(), aResult ); + } + else + rStrm << EXC_FORMULA_RES_EMPTY; // BIFF8 only + rStrm << sal_uInt8( 0 ) << sal_uInt32( 0 ) << sal_uInt16( 0xFFFF ); + } + break; + + case SvNumFormatType::LOGICAL: + { + sal_uInt8 nXclValue = (mrScFmlaCell.GetValue() == 0.0) ? 0 : 1; + rStrm << EXC_FORMULA_RES_BOOL << sal_uInt8( 0 ) + << nXclValue << sal_uInt8( 0 ) << sal_uInt16( 0 ) + << sal_uInt16( 0xFFFF ); + } + break; + + default: + rStrm << mrScFmlaCell.GetValue(); + } + } + + // flags and formula token array + sal_uInt16 nFlags = EXC_FORMULA_DEFAULTFLAGS; + ::set_flag( nFlags, EXC_FORMULA_RECALC_ALWAYS, mxTokArr->IsVolatile() || (mxAddRec && mxAddRec->IsVolatile()) ); + ::set_flag( nFlags, EXC_FORMULA_SHARED, mxAddRec && (mxAddRec->GetRecId() == EXC_ID_SHRFMLA) ); + rStrm << nFlags << sal_uInt32( 0 ) << *mxTokArr; +} + +// Multiple cell records ====================================================== + +XclExpMultiCellBase::XclExpMultiCellBase( + sal_uInt16 nRecId, sal_uInt16 nMulRecId, std::size_t nContSize, const XclAddress& rXclPos ) : + XclExpCellBase( nRecId, 0, rXclPos ), + mnMulRecId( nMulRecId ), + mnContSize( nContSize ) +{ +} + +sal_uInt16 XclExpMultiCellBase::GetLastXclCol() const +{ + return GetXclCol() + GetCellCount() - 1; +} + +sal_uInt32 XclExpMultiCellBase::GetFirstXFId() const +{ + return maXFIds.empty() ? XclExpXFBuffer::GetDefCellXFId() : maXFIds.front().mnXFId; +} + +bool XclExpMultiCellBase::IsEmpty() const +{ + return maXFIds.empty(); +} + +void XclExpMultiCellBase::ConvertXFIndexes( const XclExpRoot& rRoot ) +{ + for( auto& rXFId : maXFIds ) + rXFId.ConvertXFIndex( rRoot ); +} + +void XclExpMultiCellBase::Save( XclExpStream& rStrm ) +{ + OSL_ENSURE_BIFF( rStrm.GetRoot().GetBiff() >= EXC_BIFF3 ); + + XclExpMultiXFIdDeq::const_iterator aEnd = maXFIds.end(); + XclExpMultiXFIdDeq::const_iterator aRangeBeg = maXFIds.begin(); + XclExpMultiXFIdDeq::const_iterator aRangeEnd = aRangeBeg; + sal_uInt16 nBegXclCol = GetXclCol(); + sal_uInt16 nEndXclCol = nBegXclCol; + + while( aRangeEnd != aEnd ) + { + // find begin of next used XF range + aRangeBeg = aRangeEnd; + nBegXclCol = nEndXclCol; + while( (aRangeBeg != aEnd) && (aRangeBeg->mnXFIndex == EXC_XF_NOTFOUND) ) + { + nBegXclCol = nBegXclCol + aRangeBeg->mnCount; + ++aRangeBeg; + } + // find end of next used XF range + aRangeEnd = aRangeBeg; + nEndXclCol = nBegXclCol; + while( (aRangeEnd != aEnd) && (aRangeEnd->mnXFIndex != EXC_XF_NOTFOUND) ) + { + nEndXclCol = nEndXclCol + aRangeEnd->mnCount; + ++aRangeEnd; + } + + // export this range as a record + if( aRangeBeg != aRangeEnd ) + { + sal_uInt16 nCount = nEndXclCol - nBegXclCol; + bool bIsMulti = nCount > 1; + std::size_t nTotalSize = GetRecSize() + (2 + mnContSize) * nCount; + if( bIsMulti ) nTotalSize += 2; + + rStrm.StartRecord( bIsMulti ? mnMulRecId : GetRecId(), nTotalSize ); + rStrm << static_cast<sal_uInt16> (GetXclRow()) << nBegXclCol; + + sal_uInt16 nRelCol = nBegXclCol - GetXclCol(); + for( XclExpMultiXFIdDeq::const_iterator aIt = aRangeBeg; aIt != aRangeEnd; ++aIt ) + { + for( sal_uInt16 nIdx = 0; nIdx < aIt->mnCount; ++nIdx ) + { + rStrm << aIt->mnXFIndex; + WriteContents( rStrm, nRelCol ); + ++nRelCol; + } + } + if( bIsMulti ) + rStrm << static_cast< sal_uInt16 >( nEndXclCol - 1 ); + rStrm.EndRecord(); + } + } +} + +void XclExpMultiCellBase::SaveXml( XclExpXmlStream& rStrm ) +{ + XclExpMultiXFIdDeq::const_iterator aEnd = maXFIds.end(); + XclExpMultiXFIdDeq::const_iterator aRangeBeg = maXFIds.begin(); + XclExpMultiXFIdDeq::const_iterator aRangeEnd = aRangeBeg; + sal_uInt16 nBegXclCol = GetXclCol(); + sal_uInt16 nEndXclCol = nBegXclCol; + + while( aRangeEnd != aEnd ) + { + // find begin of next used XF range + aRangeBeg = aRangeEnd; + nBegXclCol = nEndXclCol; + while( (aRangeBeg != aEnd) && (aRangeBeg->mnXFIndex == EXC_XF_NOTFOUND) ) + { + nBegXclCol = nBegXclCol + aRangeBeg->mnCount; + ++aRangeBeg; + } + // find end of next used XF range + aRangeEnd = aRangeBeg; + nEndXclCol = nBegXclCol; + while( (aRangeEnd != aEnd) && (aRangeEnd->mnXFIndex != EXC_XF_NOTFOUND) ) + { + nEndXclCol = nEndXclCol + aRangeEnd->mnCount; + ++aRangeEnd; + } + + // export this range as a record + if( aRangeBeg != aRangeEnd ) + { + sal_uInt16 nRelColIdx = nBegXclCol - GetXclCol(); + sal_Int32 nRelCol = 0; + for( XclExpMultiXFIdDeq::const_iterator aIt = aRangeBeg; aIt != aRangeEnd; ++aIt ) + { + for( sal_uInt16 nIdx = 0; nIdx < aIt->mnCount; ++nIdx ) + { + WriteXmlContents( + rStrm, + XclAddress( static_cast<sal_uInt16>(nBegXclCol + nRelCol), GetXclRow() ), + aIt->mnXFIndex, + nRelColIdx ); + ++nRelCol; + ++nRelColIdx; + } + } + } + } +} + +sal_uInt16 XclExpMultiCellBase::GetCellCount() const +{ + return std::accumulate(maXFIds.begin(), maXFIds.end(), sal_uInt16(0), + [](const sal_uInt16& rSum, const XclExpMultiXFId& rXFId) { return rSum + rXFId.mnCount; }); +} + +void XclExpMultiCellBase::AppendXFId( const XclExpMultiXFId& rXFId ) +{ + if( maXFIds.empty() || (maXFIds.back().mnXFId != rXFId.mnXFId) ) + maXFIds.push_back( rXFId ); + else + maXFIds.back().mnCount += rXFId.mnCount; +} + +void XclExpMultiCellBase::AppendXFId( const XclExpRoot& rRoot, + const ScPatternAttr* pPattern, sal_uInt16 nScript, sal_uInt32 nForcedXFId, sal_uInt16 nCount ) +{ + sal_uInt32 nXFId = (nForcedXFId == EXC_XFID_NOTFOUND) ? + rRoot.GetXFBuffer().Insert( pPattern, nScript ) : nForcedXFId; + AppendXFId( XclExpMultiXFId( nXFId, nCount ) ); +} + +bool XclExpMultiCellBase::TryMergeXFIds( const XclExpMultiCellBase& rCell ) +{ + if( GetLastXclCol() + 1 == rCell.GetXclCol() ) + { + maXFIds.insert( maXFIds.end(), rCell.maXFIds.begin(), rCell.maXFIds.end() ); + return true; + } + return false; +} + +void XclExpMultiCellBase::GetXFIndexes( ScfUInt16Vec& rXFIndexes ) const +{ + OSL_ENSURE( GetLastXclCol() < rXFIndexes.size(), "XclExpMultiCellBase::GetXFIndexes - vector too small" ); + ScfUInt16Vec::iterator aDestIt = rXFIndexes.begin() + GetXclCol(); + for( const auto& rXFId : maXFIds ) + { + ::std::fill( aDestIt, aDestIt + rXFId.mnCount, rXFId.mnXFIndex ); + aDestIt += rXFId.mnCount; + } +} + +void XclExpMultiCellBase::RemoveUnusedXFIndexes( const ScfUInt16Vec& rXFIndexes, size_t nStartAllNotFound ) +{ + // save last column before calling maXFIds.clear() + sal_uInt16 nLastXclCol = GetLastXclCol(); + OSL_ENSURE( nLastXclCol < rXFIndexes.size(), "XclExpMultiCellBase::RemoveUnusedXFIndexes - XF index vector too small" ); + + // build new XF index vector, containing passed XF indexes + maXFIds.clear(); + // Process only all that possibly are not EXC_XF_NOTFOUND. + size_t nEnd = std::min<size_t>(nLastXclCol + 1, nStartAllNotFound); + for( size_t i = GetXclCol(); i < nEnd; ++i ) + { + XclExpMultiXFId aXFId( 0 ); + // AppendXFId() tests XclExpXFIndex::mnXFId, set it too + aXFId.mnXFId = aXFId.mnXFIndex = rXFIndexes[ i ]; + AppendXFId( aXFId ); + } + + // remove leading and trailing unused XF indexes + if( !maXFIds.empty() && (maXFIds.front().mnXFIndex == EXC_XF_NOTFOUND) ) + { + SetXclCol( GetXclCol() + maXFIds.front().mnCount ); + maXFIds.erase(maXFIds.begin(), maXFIds.begin() + 1); + } + if( !maXFIds.empty() && (maXFIds.back().mnXFIndex == EXC_XF_NOTFOUND) ) + maXFIds.pop_back(); + + // The Save() function will skip all XF indexes equal to EXC_XF_NOTFOUND. +} + +sal_uInt16 XclExpMultiCellBase::GetStartColAllDefaultCell() const +{ + sal_uInt16 col = GetXclCol(); + sal_uInt16 nMaxNonDefCol = col; + for( const auto& rXFId : maXFIds ) + { + col += rXFId.mnCount; + if (rXFId.mnXFIndex != EXC_XF_DEFAULTCELL) + nMaxNonDefCol = col; + } + return nMaxNonDefCol; +} + +XclExpBlankCell::XclExpBlankCell( const XclAddress& rXclPos, const XclExpMultiXFId& rXFId ) : + XclExpMultiCellBase( EXC_ID3_BLANK, EXC_ID_MULBLANK, 0, rXclPos ) +{ + OSL_ENSURE( rXFId.mnCount > 0, "XclExpBlankCell::XclExpBlankCell - invalid count" ); + AppendXFId( rXFId ); +} + +XclExpBlankCell::XclExpBlankCell( + const XclExpRoot& rRoot, const XclAddress& rXclPos, sal_uInt16 nLastXclCol, + const ScPatternAttr* pPattern, sal_uInt32 nForcedXFId ) : + XclExpMultiCellBase( EXC_ID3_BLANK, EXC_ID_MULBLANK, 0, rXclPos ) +{ + OSL_ENSURE( rXclPos.mnCol <= nLastXclCol, "XclExpBlankCell::XclExpBlankCell - invalid column range" ); + // #i46627# use default script type instead of ApiScriptType::WEAK + AppendXFId( rRoot, pPattern, rRoot.GetDefApiScript(), nForcedXFId, nLastXclCol - rXclPos.mnCol + 1 ); +} + +bool XclExpBlankCell::TryMerge( const XclExpCellBase& rCell ) +{ + const XclExpBlankCell* pBlankCell = dynamic_cast< const XclExpBlankCell* >( &rCell ); + return pBlankCell && TryMergeXFIds( *pBlankCell ); +} + +void XclExpBlankCell::GetBlankXFIndexes( ScfUInt16Vec& rXFIndexes ) const +{ + GetXFIndexes( rXFIndexes ); +} + +void XclExpBlankCell::RemoveUnusedBlankCells( const ScfUInt16Vec& rXFIndexes, size_t nStartAllNotFound ) +{ + RemoveUnusedXFIndexes( rXFIndexes, nStartAllNotFound ); +} + +void XclExpBlankCell::WriteContents( XclExpStream& /*rStrm*/, sal_uInt16 /*nRelCol*/ ) +{ +} + +void XclExpBlankCell::WriteXmlContents( XclExpXmlStream& rStrm, const XclAddress& rAddress, sal_uInt32 nXFId, sal_uInt16 /* nRelCol */ ) +{ + sax_fastparser::FSHelperPtr& rWorksheet = rStrm.GetCurrentStream(); + rWorksheet->singleElement( XML_c, + XML_r, XclXmlUtils::ToOString(rStrm.GetRoot().GetStringBuf(), rAddress).getStr(), + XML_s, lcl_GetStyleId(rStrm, nXFId) ); +} + +XclExpRkCell::XclExpRkCell( + const XclExpRoot& rRoot, const XclAddress& rXclPos, + const ScPatternAttr* pPattern, sal_uInt32 nForcedXFId, sal_Int32 nRkValue ) : + XclExpMultiCellBase( EXC_ID_RK, EXC_ID_MULRK, 4, rXclPos ) +{ + // #i41210# always use latin script for number cells - may look wrong for special number formats... + AppendXFId( rRoot, pPattern, ApiScriptType::LATIN, nForcedXFId ); + maRkValues.push_back( nRkValue ); +} + +bool XclExpRkCell::TryMerge( const XclExpCellBase& rCell ) +{ + const XclExpRkCell* pRkCell = dynamic_cast< const XclExpRkCell* >( &rCell ); + if( pRkCell && TryMergeXFIds( *pRkCell ) ) + { + maRkValues.insert( maRkValues.end(), pRkCell->maRkValues.begin(), pRkCell->maRkValues.end() ); + return true; + } + return false; +} + +void XclExpRkCell::WriteXmlContents( XclExpXmlStream& rStrm, const XclAddress& rAddress, sal_uInt32 nXFId, sal_uInt16 nRelCol ) +{ + sax_fastparser::FSHelperPtr& rWorksheet = rStrm.GetCurrentStream(); + rWorksheet->startElement( XML_c, + XML_r, XclXmlUtils::ToOString(rStrm.GetRoot().GetStringBuf(), rAddress).getStr(), + XML_s, lcl_GetStyleId(rStrm, nXFId), + XML_t, "n" + // OOXTODO: XML_cm, XML_vm, XML_ph + ); + rWorksheet->startElement( XML_v ); + rWorksheet->write( XclTools::GetDoubleFromRK( maRkValues[ nRelCol ] ) ); + rWorksheet->endElement( XML_v ); + rWorksheet->endElement( XML_c ); +} + +void XclExpRkCell::WriteContents( XclExpStream& rStrm, sal_uInt16 nRelCol ) +{ + OSL_ENSURE( nRelCol < maRkValues.size(), "XclExpRkCell::WriteContents - overflow error" ); + rStrm << maRkValues[ nRelCol ]; +} + +// Rows and Columns + +XclExpOutlineBuffer::XclExpOutlineBuffer( const XclExpRoot& rRoot, bool bRows ) : + mpScOLArray( nullptr ), + maLevelInfos( SC_OL_MAXDEPTH ), + mnCurrLevel( 0 ), + mbCurrCollapse( false ) +{ + if( const ScOutlineTable* pOutlineTable = rRoot.GetDoc().GetOutlineTable( rRoot.GetCurrScTab() ) ) + mpScOLArray = &(bRows ? pOutlineTable->GetRowArray() : pOutlineTable->GetColArray()); + + if( mpScOLArray ) + for( size_t nLevel = 0; nLevel < SC_OL_MAXDEPTH; ++nLevel ) + if( const ScOutlineEntry* pEntry = mpScOLArray->GetEntryByPos( nLevel, 0 ) ) + maLevelInfos[ nLevel ].mnScEndPos = pEntry->GetEnd(); +} + +void XclExpOutlineBuffer::UpdateColRow( SCCOLROW nScPos ) +{ + if( !mpScOLArray ) + return; + + // find open level index for passed position + size_t nNewOpenScLevel = 0; // new open level (0-based Calc index) + sal_uInt8 nNewLevel = 0; // new open level (1-based Excel index) + + if( mpScOLArray->FindTouchedLevel( nScPos, nScPos, nNewOpenScLevel ) ) + nNewLevel = static_cast< sal_uInt8 >( nNewOpenScLevel + 1 ); + // else nNewLevel keeps 0 to show that there are no groups + + mbCurrCollapse = false; + if( nNewLevel >= mnCurrLevel ) + { + // new level(s) opened, or no level closed - update all level infos + for( size_t nScLevel = 0; nScLevel <= nNewOpenScLevel; ++nScLevel ) + { + /* In each level: check if a new group is started (there may be + neighbored groups without gap - therefore check ALL levels). */ + if( maLevelInfos[ nScLevel ].mnScEndPos < nScPos ) + { + if( const ScOutlineEntry* pEntry = mpScOLArray->GetEntryByPos( nScLevel, nScPos ) ) + { + maLevelInfos[ nScLevel ].mnScEndPos = pEntry->GetEnd(); + maLevelInfos[ nScLevel ].mbHidden = pEntry->IsHidden(); + } + } + } + } + else + { + // level(s) closed - check if any of the closed levels are collapsed + // Calc uses 0-based level indexes + sal_uInt16 nOldOpenScLevel = mnCurrLevel - 1; + for( sal_uInt16 nScLevel = nNewOpenScLevel + 1; !mbCurrCollapse && (nScLevel <= nOldOpenScLevel); ++nScLevel ) + mbCurrCollapse = maLevelInfos[ nScLevel ].mbHidden; + } + + // cache new opened level + mnCurrLevel = nNewLevel; +} + +XclExpGuts::XclExpGuts( const XclExpRoot& rRoot ) : + XclExpRecord( EXC_ID_GUTS, 8 ), + mnColLevels( 0 ), + mnColWidth( 0 ), + mnRowLevels( 0 ), + mnRowWidth( 0 ) +{ + const ScOutlineTable* pOutlineTable = rRoot.GetDoc().GetOutlineTable( rRoot.GetCurrScTab() ); + if(!pOutlineTable) + return; + + // column outline groups + const ScOutlineArray& rColArray = pOutlineTable->GetColArray(); + mnColLevels = ulimit_cast< sal_uInt16 >( rColArray.GetDepth(), EXC_OUTLINE_MAX ); + if( mnColLevels ) + { + ++mnColLevels; + mnColWidth = 12 * mnColLevels + 5; + } + + // row outline groups + const ScOutlineArray& rRowArray = pOutlineTable->GetRowArray(); + mnRowLevels = ulimit_cast< sal_uInt16 >( rRowArray.GetDepth(), EXC_OUTLINE_MAX ); + if( mnRowLevels ) + { + ++mnRowLevels; + mnRowWidth = 12 * mnRowLevels + 5; + } +} + +void XclExpGuts::WriteBody( XclExpStream& rStrm ) +{ + rStrm << mnRowWidth << mnColWidth << mnRowLevels << mnColLevels; +} + +XclExpDimensions::XclExpDimensions( const XclExpRoot& rRoot ) : + mrRoot(rRoot), + mnFirstUsedXclRow( 0 ), + mnFirstFreeXclRow( 0 ), + mnFirstUsedXclCol( 0 ), + mnFirstFreeXclCol( 0 ) +{ + switch( rRoot.GetBiff() ) + { + case EXC_BIFF2: SetRecHeader( EXC_ID2_DIMENSIONS, 8 ); break; + case EXC_BIFF3: + case EXC_BIFF4: + case EXC_BIFF5: SetRecHeader( EXC_ID3_DIMENSIONS, 10 ); break; + case EXC_BIFF8: SetRecHeader( EXC_ID3_DIMENSIONS, 14 ); break; + default: DBG_ERROR_BIFF(); + } +} + +void XclExpDimensions::SetDimensions( + sal_uInt16 nFirstUsedXclCol, sal_uInt32 nFirstUsedXclRow, + sal_uInt16 nFirstFreeXclCol, sal_uInt32 nFirstFreeXclRow ) +{ + mnFirstUsedXclRow = nFirstUsedXclRow; + mnFirstFreeXclRow = nFirstFreeXclRow; + mnFirstUsedXclCol = nFirstUsedXclCol; + mnFirstFreeXclCol = nFirstFreeXclCol; +} + +void XclExpDimensions::SaveXml( XclExpXmlStream& rStrm ) +{ + ScRange aRange; + aRange.aStart.SetRow( static_cast<SCROW>(mnFirstUsedXclRow) ); + aRange.aStart.SetCol( static_cast<SCCOL>(mnFirstUsedXclCol) ); + + if( mnFirstFreeXclRow != mnFirstUsedXclRow && mnFirstFreeXclCol != mnFirstUsedXclCol ) + { + aRange.aEnd.SetRow( static_cast<SCROW>(mnFirstFreeXclRow-1) ); + aRange.aEnd.SetCol( static_cast<SCCOL>(mnFirstFreeXclCol-1) ); + } + + aRange.PutInOrder(); + rStrm.GetCurrentStream()->singleElement( XML_dimension, + // To be compatible with MS Office 2007, + // we need full address notation format + // e.g. "A1:AMJ177" and not partial like: "1:177". + XML_ref, XclXmlUtils::ToOString(mrRoot.GetDoc(), aRange, true) ); +} + +void XclExpDimensions::WriteBody( XclExpStream& rStrm ) +{ + XclBiff eBiff = rStrm.GetRoot().GetBiff(); + if( eBiff == EXC_BIFF8 ) + rStrm << mnFirstUsedXclRow << mnFirstFreeXclRow; + else + rStrm << static_cast< sal_uInt16 >( mnFirstUsedXclRow ) << static_cast< sal_uInt16 >( mnFirstFreeXclRow ); + rStrm << mnFirstUsedXclCol << mnFirstFreeXclCol; + if( eBiff >= EXC_BIFF3 ) + rStrm << sal_uInt16( 0 ); +} + +namespace { + +double lclGetCChCorrection(const XclExpRoot& rRoot) +{ + // Convert the correction from 1/256ths of a character size to count of chars + // TODO: make to fit ECMA-376-1:2016 18.3.1.81 sheetFormatPr (Sheet Format Properties): + // 5 pixels are added to the base width: 2 for margin padding on each side, plus 1 for gridline + // So this should depend on rRoot.GetCharWidth(), not on font height + + tools::Long nFontHt = rRoot.GetFontBuffer().GetAppFontData().mnHeight; + return XclTools::GetXclDefColWidthCorrection(nFontHt) / 256.0; +} + +} // namespace + +XclExpDefcolwidth::XclExpDefcolwidth( const XclExpRoot& rRoot ) : + XclExpDoubleRecord(EXC_ID_DEFCOLWIDTH, EXC_DEFCOLWIDTH_DEF + lclGetCChCorrection(rRoot)), + XclExpRoot( rRoot ) +{ +} + +bool XclExpDefcolwidth::IsDefWidth( sal_uInt16 nXclColWidth ) const +{ + // This formula is taking number of characters with GetValue() + // and it is translating it into default column width. + // https://msdn.microsoft.com/en-us/library/documentformat.openxml.spreadsheet.column.aspx + double defaultColumnWidth = 256.0 * GetValue(); + + // exactly matched, if difference is less than 1/16 of a character to the left or to the right + return std::abs(defaultColumnWidth - nXclColWidth) < 256.0 * 1.0 / 16.0; +} + +void XclExpDefcolwidth::SetDefWidth( sal_uInt16 nXclColWidth, bool bXLS ) +{ + double fCCh = nXclColWidth / 256.0; + if (bXLS) + { + const double fCorrection = lclGetCChCorrection(GetRoot()); + const double fCorrectedCCh = fCCh - fCorrection; + // Now get the value which would be stored in XLS DefColWidth struct + double fCChRound = std::round(fCorrectedCCh); + // If default width was set to a value that is not representable as integral CCh between 0 + // and 255, then just ignore that value, and use an arbitrary default. That way, the stored + // default might not represent the most used column width (or any used column width), but + // that's OK, and it just means that those columns will explicitly store their width in + // 1/256ths of char, and have fUserSet in their ColInfo records. + if (fCChRound < 0.0 || fCChRound > 255.0 || std::abs(fCChRound - fCorrectedCCh) > 1.0 / 512) + fCChRound = 8.0; + fCCh = fCChRound + fCorrection; + } + + SetValue(fCCh); +} + +void XclExpDefcolwidth::Save(XclExpStream& rStrm) +{ + double fCorrectedCCh = GetValue() - lclGetCChCorrection(GetRoot()); + // Convert double to sal_uInt16 + XclExpUInt16Record aUInt16Rec(GetRecId(), + static_cast<sal_uInt16>(std::round(fCorrectedCCh))); + aUInt16Rec.Save(rStrm); +} + +XclExpColinfo::XclExpColinfo( const XclExpRoot& rRoot, + SCCOL nScCol, SCROW nLastScRow, XclExpColOutlineBuffer& rOutlineBfr ) : + XclExpRecord( EXC_ID_COLINFO, 12 ), + XclExpRoot( rRoot ), + mbCustomWidth( false ), + mnWidth( 0 ), + mnScWidth( 0 ), + mnFlags( 0 ), + mnOutlineLevel( 0 ), + mnFirstXclCol( static_cast< sal_uInt16 >( nScCol ) ), + mnLastXclCol( static_cast< sal_uInt16 >( nScCol ) ) +{ + ScDocument& rDoc = GetDoc(); + SCTAB nScTab = GetCurrScTab(); + + // column default format + maXFId.mnXFId = GetXFBuffer().Insert( + rDoc.GetMostUsedPattern( nScCol, 0, nLastScRow, nScTab ), GetDefApiScript() ); + + // column width. If column is hidden then we should return real value (not zero) + sal_uInt16 nScWidth = rDoc.GetColWidth( nScCol, nScTab, false ); + mnWidth = XclTools::GetXclColumnWidth( nScWidth, GetCharWidth() ); + mnScWidth = convertTwipToMm100(nScWidth); + + // column flags + ::set_flag( mnFlags, EXC_COLINFO_HIDDEN, rDoc.ColHidden(nScCol, nScTab) ); + + // outline data + rOutlineBfr.Update( nScCol ); + ::set_flag( mnFlags, EXC_COLINFO_COLLAPSED, rOutlineBfr.IsCollapsed() ); + ::insert_value( mnFlags, rOutlineBfr.GetLevel(), 8, 3 ); + mnOutlineLevel = rOutlineBfr.GetLevel(); +} + +void XclExpColinfo::ConvertXFIndexes() +{ + maXFId.ConvertXFIndex( GetRoot() ); +} + +bool XclExpColinfo::IsDefault( const XclExpDefcolwidth& rDefColWidth ) +{ + mbCustomWidth = !rDefColWidth.IsDefWidth(mnWidth); + return (maXFId.mnXFIndex == EXC_XF_DEFAULTCELL) && + (mnFlags == 0) && + (mnOutlineLevel == 0) && + !mbCustomWidth; +} + +bool XclExpColinfo::TryMerge( const XclExpColinfo& rColInfo ) +{ + if( (maXFId.mnXFIndex == rColInfo.maXFId.mnXFIndex) && + (mnWidth == rColInfo.mnWidth) && + (mnFlags == rColInfo.mnFlags) && + (mnOutlineLevel == rColInfo.mnOutlineLevel) && + (mnLastXclCol + 1 == rColInfo.mnFirstXclCol) ) + { + mnLastXclCol = rColInfo.mnLastXclCol; + return true; + } + return false; +} + +void XclExpColinfo::WriteBody( XclExpStream& rStrm ) +{ + // if last column is equal to last possible column, Excel adds one more + sal_uInt16 nLastXclCol = mnLastXclCol; + if( nLastXclCol == static_cast< sal_uInt16 >( rStrm.GetRoot().GetMaxPos().Col() ) ) + ++nLastXclCol; + + rStrm << mnFirstXclCol + << nLastXclCol + << mnWidth + << maXFId.mnXFIndex + << mnFlags + << sal_uInt16( 0 ); +} + +void XclExpColinfo::SaveXml( XclExpXmlStream& rStrm ) +{ + const double nExcelColumnWidth = mnScWidth / convertTwipToMm100<double>(GetCharWidth()); + + // tdf#101363 In MS specification the output value is set with double precision after delimiter: + // =Truncate(({width in pixels} - 5)/{Maximum Digit Width} * 100 + 0.5)/100 + // Explanation of magic numbers: + // 5 number - are 4 pixels of margin padding (two on each side), plus 1 pixel padding for the gridlines. + // It is unknown if it should be applied during LibreOffice export + // 100 number - used to limit precision to 0.01 with formula =Truncate( {value}*100+0.5 ) / 100 + // 0.5 number (0.005 to output value) - used to increase value before truncating, + // to avoid situation when 2.997 will be truncated to 2.99 and not to 3.00 + const double nTruncatedExcelColumnWidth = std::trunc( nExcelColumnWidth * 100.0 + 0.5 ) / 100.0; + rStrm.GetCurrentStream()->singleElement( XML_col, + // OOXTODO: XML_bestFit, + XML_collapsed, ToPsz( ::get_flag( mnFlags, EXC_COLINFO_COLLAPSED ) ), + XML_customWidth, ToPsz( mbCustomWidth ), + XML_hidden, ToPsz( ::get_flag( mnFlags, EXC_COLINFO_HIDDEN ) ), + XML_outlineLevel, OString::number(mnOutlineLevel), + XML_max, OString::number(mnLastXclCol + 1), + XML_min, OString::number(mnFirstXclCol + 1), + // OOXTODO: XML_phonetic, + XML_style, lcl_GetStyleId(rStrm, maXFId.mnXFIndex), + XML_width, OString::number(nTruncatedExcelColumnWidth) ); +} + +XclExpColinfoBuffer::XclExpColinfoBuffer( const XclExpRoot& rRoot ) : + XclExpRoot( rRoot ), + maDefcolwidth( rRoot ), + maOutlineBfr( rRoot ), + mnHighestOutlineLevel( 0 ) +{ +} + +void XclExpColinfoBuffer::Initialize( SCROW nLastScRow ) +{ + + for( sal_uInt16 nScCol = 0, nLastScCol = GetMaxPos().Col(); nScCol <= nLastScCol; ++nScCol ) + { + maColInfos.AppendNewRecord( new XclExpColinfo( GetRoot(), nScCol, nLastScRow, maOutlineBfr ) ); + if( maOutlineBfr.GetLevel() > mnHighestOutlineLevel ) + { + mnHighestOutlineLevel = maOutlineBfr.GetLevel(); + } + } +} + +void XclExpColinfoBuffer::Finalize( ScfUInt16Vec& rXFIndexes, bool bXLS ) +{ + rXFIndexes.clear(); + rXFIndexes.reserve( maColInfos.GetSize() ); + + if( !maColInfos.IsEmpty()) + { + XclExpColinfo* xPrevRec = maColInfos.GetRecord( 0 ); + xPrevRec->ConvertXFIndexes(); + for( size_t nPos = 1; nPos < maColInfos.GetSize(); ++nPos ) + { + XclExpColinfo* xRec = maColInfos.GetRecord( nPos ); + xRec->ConvertXFIndexes(); + + // try to merge with previous record + if( xPrevRec->TryMerge( *xRec ) ) + maColInfos.InvalidateRecord( nPos ); + else + xPrevRec = xRec; + } + maColInfos.RemoveInvalidatedRecords(); + } + + // put XF indexes into passed vector, collect use count of all different widths + std::unordered_map< sal_uInt16, sal_uInt16 > aWidthMap; + sal_uInt16 nMaxColCount = 0; + sal_uInt16 nMaxUsedWidth = 0; + for( size_t nPos = 0; nPos < maColInfos.GetSize(); ++nPos ) + { + const XclExpColinfo* xRec = maColInfos.GetRecord( nPos ); + sal_uInt16 nColCount = xRec->GetColCount(); + + // add XF index to passed vector + rXFIndexes.resize( rXFIndexes.size() + nColCount, xRec->GetXFIndex() ); + + // collect use count of column width + sal_uInt16 nWidth = xRec->GetColWidth(); + sal_uInt16& rnMapCount = aWidthMap[ nWidth ]; + rnMapCount = rnMapCount + nColCount; + if( rnMapCount > nMaxColCount ) + { + nMaxColCount = rnMapCount; + nMaxUsedWidth = nWidth; + } + } + maDefcolwidth.SetDefWidth( nMaxUsedWidth, bXLS ); + + // remove all default COLINFO records + for( size_t nPos = 0; nPos < maColInfos.GetSize(); ++nPos ) + { + XclExpColinfo* xRec = maColInfos.GetRecord( nPos ); + if( xRec->IsDefault( maDefcolwidth ) ) + maColInfos.InvalidateRecord( nPos ); + } + maColInfos.RemoveInvalidatedRecords(); +} + +void XclExpColinfoBuffer::Save( XclExpStream& rStrm ) +{ + // DEFCOLWIDTH + maDefcolwidth.Save( rStrm ); + // COLINFO records + maColInfos.Save( rStrm ); +} + +void XclExpColinfoBuffer::SaveXml( XclExpXmlStream& rStrm ) +{ + if( maColInfos.IsEmpty() ) + return; + + sax_fastparser::FSHelperPtr& rWorksheet = rStrm.GetCurrentStream(); + rWorksheet->startElement(XML_cols); + maColInfos.SaveXml( rStrm ); + rWorksheet->endElement( XML_cols ); +} + +XclExpDefaultRowData::XclExpDefaultRowData() : + mnFlags( EXC_DEFROW_DEFAULTFLAGS ), + mnHeight( EXC_DEFROW_DEFAULTHEIGHT ) +{ +} + +XclExpDefaultRowData::XclExpDefaultRowData( const XclExpRow& rRow ) : + mnFlags( EXC_DEFROW_DEFAULTFLAGS ), + mnHeight( rRow.GetHeight() ) +{ + ::set_flag( mnFlags, EXC_DEFROW_HIDDEN, rRow.IsHidden() ); + ::set_flag( mnFlags, EXC_DEFROW_UNSYNCED, rRow.IsUnsynced() ); +} + +static bool operator<( const XclExpDefaultRowData& rLeft, const XclExpDefaultRowData& rRight ) +{ + return (rLeft.mnHeight < rRight.mnHeight) || + ((rLeft.mnHeight == rRight.mnHeight) && (rLeft.mnFlags < rRight.mnFlags)); +} + +XclExpDefrowheight::XclExpDefrowheight() : + XclExpRecord( EXC_ID3_DEFROWHEIGHT, 4 ) +{ +} + +void XclExpDefrowheight::SetDefaultData( const XclExpDefaultRowData& rDefData ) +{ + maDefData = rDefData; +} + +void XclExpDefrowheight::WriteBody( XclExpStream& rStrm ) +{ + OSL_ENSURE_BIFF( rStrm.GetRoot().GetBiff() >= EXC_BIFF3 ); + rStrm << maDefData.mnFlags << maDefData.mnHeight; +} + +XclExpRow::XclExpRow( const XclExpRoot& rRoot, sal_uInt32 nXclRow, + XclExpRowOutlineBuffer& rOutlineBfr, bool bAlwaysEmpty, bool bHidden, sal_uInt16 nHeight ) : + XclExpRecord( EXC_ID3_ROW, 16 ), + XclExpRoot( rRoot ), + mnXclRow( nXclRow ), + mnHeight( nHeight ), + mnFlags( EXC_ROW_DEFAULTFLAGS ), + mnXFIndex( EXC_XF_DEFAULTCELL ), + mnOutlineLevel( 0 ), + mnXclRowRpt( 1 ), + mnCurrentRow( nXclRow ), + mbAlwaysEmpty( bAlwaysEmpty ), + mbEnabled( true ) +{ + SCTAB nScTab = GetCurrScTab(); + SCROW nScRow = static_cast< SCROW >( mnXclRow ); + + // *** Row flags *** ------------------------------------------------------ + + CRFlags nRowFlags = GetDoc().GetRowFlags( nScRow, nScTab ); + bool bUserHeight( nRowFlags & CRFlags::ManualSize ); + ::set_flag( mnFlags, EXC_ROW_UNSYNCED, bUserHeight ); + ::set_flag( mnFlags, EXC_ROW_HIDDEN, bHidden ); + + // *** Outline data *** --------------------------------------------------- + + rOutlineBfr.Update( nScRow ); + ::set_flag( mnFlags, EXC_ROW_COLLAPSED, rOutlineBfr.IsCollapsed() ); + ::insert_value( mnFlags, rOutlineBfr.GetLevel(), 0, 3 ); + mnOutlineLevel = rOutlineBfr.GetLevel(); + + // *** Progress bar *** --------------------------------------------------- + + XclExpProgressBar& rProgress = GetProgressBar(); + rProgress.IncRowRecordCount(); + rProgress.Progress(); +} + +static size_t findFirstAllSameUntilEnd( const ScfUInt16Vec& rIndexes, sal_uInt16 value, + size_t searchStart = std::numeric_limits<size_t>::max()) +{ + for( size_t i = std::min(rIndexes.size(), searchStart); i >= 1; --i ) + { + if( rIndexes[ i - 1 ] != value ) + return i; + } + return 0; +} + +void XclExpRow::AppendCell( XclExpCellRef const & xCell, bool bIsMergedBase ) +{ + OSL_ENSURE( !mbAlwaysEmpty, "XclExpRow::AppendCell - row is marked to be always empty" ); + // try to merge with last existing cell + InsertCell( xCell, maCellList.GetSize(), bIsMergedBase ); +} + +void XclExpRow::Finalize( const ScfUInt16Vec& rColXFIndexes, ScfUInt16Vec& aXFIndexes, size_t nStartColAllDefault, bool bProgress ) +{ + size_t nPos, nSize; + + // *** Convert XF identifiers *** ----------------------------------------- + + // additionally collect the blank XF indexes + size_t nColCount = GetMaxPos().Col() + 1; + OSL_ENSURE( rColXFIndexes.size() == nColCount, "XclExpRow::Finalize - wrong column XF index count" ); + + // The vector should be preset to all items being EXC_XF_NOTFOUND, to avoid repeated allocations + // and clearing. + assert( aXFIndexes.size() == nColCount ); + assert( aXFIndexes.front() == EXC_XF_NOTFOUND ); + assert( aXFIndexes.back() == EXC_XF_NOTFOUND ); + for( nPos = 0, nSize = maCellList.GetSize(); nPos < nSize; ++nPos ) + { + XclExpCellBase* pCell = maCellList.GetRecord( nPos ); + pCell->ConvertXFIndexes( GetRoot() ); + pCell->GetBlankXFIndexes( aXFIndexes ); + } + + // *** Fill gaps with BLANK/MULBLANK cell records *** --------------------- + + /* This is needed because nonexistent cells in Calc are not formatted at all, + but in Excel they would have the column default format. Blank cells that + are equal to the respective column default are removed later in this function. */ + if( !mbAlwaysEmpty ) + { + // XF identifier representing default cell XF + XclExpMultiXFId aXFId( XclExpXFBuffer::GetDefCellXFId() ); + aXFId.ConvertXFIndex( GetRoot() ); + + nPos = 0; + while( nPos <= maCellList.GetSize() ) // don't cache list size, may change in the loop + { + // get column index that follows previous cell + sal_uInt16 nFirstFreeXclCol = (nPos > 0) ? (maCellList.GetRecord( nPos - 1 )->GetLastXclCol() + 1) : 0; + // get own column index + sal_uInt16 nNextUsedXclCol = (nPos < maCellList.GetSize()) ? maCellList.GetRecord( nPos )->GetXclCol() : (GetMaxPos().Col() + 1); + + // is there a gap? + if( nFirstFreeXclCol < nNextUsedXclCol ) + { + aXFId.mnCount = nNextUsedXclCol - nFirstFreeXclCol; + XclExpCellRef xNewCell = new XclExpBlankCell( XclAddress( nFirstFreeXclCol, mnXclRow ), aXFId ); + // insert the cell, InsertCell() may merge it with existing BLANK records + InsertCell( xNewCell, nPos, false ); + // insert default XF indexes into aXFIndexes + for( size_t i = nFirstFreeXclCol; i < nNextUsedXclCol; ++i ) + aXFIndexes[ i ] = aXFId.mnXFIndex; + // don't step forward with nPos, InsertCell() may remove records + } + else + ++nPos; + } + } + + // *** Find default row format *** ---------------------------------------- + + // Often there will be many EXC_XF_DEFAULTCELL at the end, optimize by ignoring them. + size_t nStartSearchAllDefault = aXFIndexes.size(); + if( !maCellList.IsEmpty() && dynamic_cast< const XclExpBlankCell* >( maCellList.GetLastRecord())) + { + const XclExpBlankCell* pLastBlank = static_cast< const XclExpBlankCell* >( maCellList.GetLastRecord()); + assert(pLastBlank->GetLastXclCol() == aXFIndexes.size() - 1); + nStartSearchAllDefault = pLastBlank->GetStartColAllDefaultCell(); + } + size_t nStartAllDefault = findFirstAllSameUntilEnd( aXFIndexes, EXC_XF_DEFAULTCELL, nStartSearchAllDefault); + + // find most used XF index in the row + sal_uInt16 nRowXFIndex = EXC_XF_DEFAULTCELL; + const size_t nHalfIndexes = aXFIndexes.size() / 2; + if( nStartAllDefault > nHalfIndexes ) // Otherwise most are EXC_XF_DEFAULTCELL. + { + // Very likely the most common one is going to be the last one. + nRowXFIndex = aXFIndexes.back(); + size_t nStartLastSame = findFirstAllSameUntilEnd( aXFIndexes, nRowXFIndex ); + if( nStartLastSame > nHalfIndexes ) // No, find out the most used one by counting. + { + std::unordered_map< sal_uInt16, size_t > aIndexMap; + size_t nMaxXFCount = 0; + for( const auto& rXFIndex : aXFIndexes ) + { + if( rXFIndex != EXC_XF_NOTFOUND ) + { + size_t& rnCount = aIndexMap[ rXFIndex ]; + ++rnCount; + if( rnCount > nMaxXFCount ) + { + nRowXFIndex = rXFIndex; + nMaxXFCount = rnCount; + if (nMaxXFCount > nHalfIndexes) + { + // No other XF index can have a greater usage count, we + // don't need to loop through the remaining cells. + // Specifically for the tail of unused default + // cells/columns this makes a difference. + break; // for + } + } + } + } + } + } + + // decide whether to use the row default XF index or column default XF indexes + bool bUseColDefXFs = nRowXFIndex == EXC_XF_DEFAULTCELL; + if( !bUseColDefXFs ) + { + // count needed XF indexes for blank cells with and without row default XF index + size_t nXFCountWithRowDefXF = 0; + size_t nXFCountWithoutRowDefXF = 0; + ScfUInt16Vec::const_iterator aColIt = rColXFIndexes.begin(); + for( const auto& rXFIndex : aXFIndexes ) + { + sal_uInt16 nXFIndex = rXFIndex; + if( nXFIndex != EXC_XF_NOTFOUND ) + { + if( nXFIndex != nRowXFIndex ) + ++nXFCountWithRowDefXF; // with row default XF index + if( nXFIndex != *aColIt ) + ++nXFCountWithoutRowDefXF; // without row default XF index + } + ++aColIt; + } + + // use column XF indexes if this would cause less or equal number of BLANK records + bUseColDefXFs = nXFCountWithoutRowDefXF <= nXFCountWithRowDefXF; + } + + // *** Remove unused BLANK cell records *** ------------------------------- + + size_t maxStartAllNotFound; + if( bUseColDefXFs ) + { + size_t maxStartAllDefault = std::max( nStartAllDefault, nStartColAllDefault ); + // use column default XF indexes + // #i194#: remove cell XF indexes equal to column default XF indexes + for( size_t i = 0; i < maxStartAllDefault; ++i ) + { + if( aXFIndexes[ i ] == rColXFIndexes[ i ] ) + aXFIndexes[ i ] = EXC_XF_NOTFOUND; + } + // They can differ only up to maxNonDefault, in the rest they are the same. + for( size_t i = maxStartAllDefault; i < aXFIndexes.size(); ++i ) + aXFIndexes[ i ] = EXC_XF_NOTFOUND; + maxStartAllNotFound = maxStartAllDefault; + } + else + { + // use row default XF index + mnXFIndex = nRowXFIndex; + ::set_flag( mnFlags, EXC_ROW_USEDEFXF ); + // #98133#, #i194#, #i27407#: remove cell XF indexes equal to row default XF index + for( auto& rXFIndex : aXFIndexes ) + if( rXFIndex == nRowXFIndex ) + rXFIndex = EXC_XF_NOTFOUND; + maxStartAllNotFound = aXFIndexes.size(); + } + + // remove unused parts of BLANK/MULBLANK cell records + size_t nStartAllNotFound = findFirstAllSameUntilEnd( aXFIndexes, EXC_XF_NOTFOUND, maxStartAllNotFound ); + nPos = 0; + while( nPos < maCellList.GetSize() ) // do not cache list size, may change in the loop + { + XclExpCellBase* xCell = maCellList.GetRecord( nPos ); + xCell->RemoveUnusedBlankCells( aXFIndexes, nStartAllNotFound ); + if( xCell->IsEmpty() ) + maCellList.RemoveRecord( nPos ); + else + ++nPos; + } + // Ensure it's all EXC_XF_NOTFOUND again for next reuse. + for( size_t i = 0; i < nStartAllNotFound; ++i ) + aXFIndexes[ i ] = EXC_XF_NOTFOUND; + + // progress bar includes disabled rows; only update it in the lead thread. + if (bProgress) + GetProgressBar().Progress(); +} +sal_uInt16 XclExpRow::GetFirstUsedXclCol() const +{ + return maCellList.IsEmpty() ? 0 : maCellList.GetFirstRecord()->GetXclCol(); +} + +sal_uInt16 XclExpRow::GetFirstFreeXclCol() const +{ + return maCellList.IsEmpty() ? 0 : (maCellList.GetLastRecord()->GetLastXclCol() + 1); +} + +bool XclExpRow::IsDefaultable() const +{ + const sal_uInt16 nFlagsAlwaysMarkedAsDefault = EXC_ROW_DEFAULTFLAGS | EXC_ROW_HIDDEN | EXC_ROW_UNSYNCED; + return !::get_flag( mnFlags, static_cast< sal_uInt16 >( ~nFlagsAlwaysMarkedAsDefault ) ) && + IsEmpty(); +} + +void XclExpRow::DisableIfDefault( const XclExpDefaultRowData& rDefRowData ) +{ + mbEnabled = !IsDefaultable() || + (mnHeight != rDefRowData.mnHeight) || + (IsHidden() != rDefRowData.IsHidden()) || + (IsUnsynced() != rDefRowData.IsUnsynced()); +} + +void XclExpRow::WriteCellList( XclExpStream& rStrm ) +{ + OSL_ENSURE( mbEnabled || maCellList.IsEmpty(), "XclExpRow::WriteCellList - cells in disabled row" ); + maCellList.Save( rStrm ); +} + +void XclExpRow::Save( XclExpStream& rStrm ) +{ + if( mbEnabled ) + { + mnCurrentRow = mnXclRow; + for ( sal_uInt32 i = 0; i < mnXclRowRpt; ++i, ++mnCurrentRow ) + XclExpRecord::Save( rStrm ); + } +} + +void XclExpRow::InsertCell( XclExpCellRef xCell, size_t nPos, bool bIsMergedBase ) +{ + OSL_ENSURE( xCell, "XclExpRow::InsertCell - missing cell" ); + + /* If we have a multi-line text in a merged cell, and the resulting + row height has not been confirmed, we need to force the EXC_ROW_UNSYNCED + flag to be true to ensure Excel works correctly. */ + if( bIsMergedBase && xCell->IsMultiLineText() ) + ::set_flag( mnFlags, EXC_ROW_UNSYNCED ); + + // try to merge with previous cell, insert the new cell if not successful + XclExpCellBase* xPrevCell = maCellList.GetRecord( nPos - 1 ); + if( xPrevCell && xPrevCell->TryMerge( *xCell ) ) + xCell = xPrevCell; + else + maCellList.InsertRecord( xCell, nPos++ ); + // nPos points now to following cell + + // try to merge with following cell, remove it if successful + XclExpCellRef xNextCell = maCellList.GetRecord( nPos ); + if( xNextCell && xCell->TryMerge( *xNextCell ) ) + maCellList.RemoveRecord( nPos ); +} + +void XclExpRow::WriteBody( XclExpStream& rStrm ) +{ + rStrm << static_cast< sal_uInt16 >(mnCurrentRow) + << GetFirstUsedXclCol() + << GetFirstFreeXclCol() + << mnHeight + << sal_uInt32( 0 ) + << mnFlags + << mnXFIndex; +} + +void XclExpRow::SaveXml( XclExpXmlStream& rStrm ) +{ + if( !mbEnabled ) + return; + sax_fastparser::FSHelperPtr& rWorksheet = rStrm.GetCurrentStream(); + bool haveFormat = ::get_flag( mnFlags, EXC_ROW_USEDEFXF ); + mnCurrentRow = mnXclRow + 1; + for ( sal_uInt32 i=0; i<mnXclRowRpt; ++i ) + { + rWorksheet->startElement( XML_row, + XML_r, OString::number(mnCurrentRow++), + // OOXTODO: XML_spans, optional + XML_s, haveFormat ? lcl_GetStyleId( rStrm, mnXFIndex ).getStr() : nullptr, + XML_customFormat, ToPsz( haveFormat ), + XML_ht, OString::number(static_cast<double>(mnHeight) / 20.0), + XML_hidden, ToPsz( ::get_flag( mnFlags, EXC_ROW_HIDDEN ) ), + XML_customHeight, ToPsz( ::get_flag( mnFlags, EXC_ROW_UNSYNCED ) ), + XML_outlineLevel, OString::number(mnOutlineLevel), + XML_collapsed, ToPsz( ::get_flag( mnFlags, EXC_ROW_COLLAPSED ) ) + // OOXTODO: XML_thickTop, bool + // OOXTODO: XML_thickBot, bool + // OOXTODO: XML_ph, bool + ); + // OOXTODO: XML_extLst + maCellList.SaveXml( rStrm ); + rWorksheet->endElement( XML_row ); + } +} + +XclExpRowBuffer::XclExpRowBuffer( const XclExpRoot& rRoot ) : + XclExpRoot( rRoot ), + maOutlineBfr( rRoot ), + maDimensions( rRoot ), + mnHighestOutlineLevel( 0 ) +{ +} + +void XclExpRowBuffer::AppendCell( XclExpCellRef const & xCell, bool bIsMergedBase ) +{ + OSL_ENSURE( xCell, "XclExpRowBuffer::AppendCell - missing cell" ); + GetOrCreateRow( xCell->GetXclRow(), false ).AppendCell( xCell, bIsMergedBase ); +} + +void XclExpRowBuffer::CreateRows( SCROW nFirstFreeScRow ) +{ + if( nFirstFreeScRow > 0 ) + GetOrCreateRow( ::std::max ( nFirstFreeScRow - 1, GetMaxPos().Row() ), true ); +} + +namespace { + +class RowFinalizeTask : public comphelper::ThreadTask +{ + bool mbProgress; + const ScfUInt16Vec& mrColXFIndexes; + size_t mnStartColAllDefault; + std::vector< XclExpRow * > maRows; +public: + RowFinalizeTask( const std::shared_ptr<comphelper::ThreadTaskTag> & pTag, + const ScfUInt16Vec& rColXFIndexes, + size_t nStartColAllDefault, + bool bProgress ) : + comphelper::ThreadTask( pTag ), + mbProgress( bProgress ), + mrColXFIndexes( rColXFIndexes ), + mnStartColAllDefault( nStartColAllDefault ) + {} + + void push_back( XclExpRow *pRow ) { maRows.push_back( pRow ); } + virtual void doWork() override + { + ScfUInt16Vec aXFIndexes( mrColXFIndexes.size(), EXC_XF_NOTFOUND ); + for (XclExpRow* p : maRows) + p->Finalize( mrColXFIndexes, aXFIndexes, mnStartColAllDefault, mbProgress ); + } +}; + +} + +void XclExpRowBuffer::Finalize( XclExpDefaultRowData& rDefRowData, + const ScfUInt16Vec& rColXFIndexes, + size_t nStartColAllDefault ) +{ + // *** Finalize all rows *** ---------------------------------------------- + + GetProgressBar().ActivateFinalRowsSegment(); + +#if 1 + // This is staggeringly slow, and each element operates only + // on its own data. + const size_t nRows = maRowMap.size(); + const size_t nThreads = nRows < 128 ? 1 : comphelper::ThreadPool::getPreferredConcurrency(); +#else + const size_t nThreads = 1; // globally disable multi-threading for now. +#endif + if (nThreads == 1) + { + ScfUInt16Vec aXFIndexes( rColXFIndexes.size(), EXC_XF_NOTFOUND ); + for (auto& rEntry : maRowMap) + rEntry.second->Finalize( rColXFIndexes, aXFIndexes, nStartColAllDefault, true ); + } + else + { + comphelper::ThreadPool &rPool = comphelper::ThreadPool::getSharedOptimalPool(); + std::shared_ptr<comphelper::ThreadTaskTag> pTag = comphelper::ThreadPool::createThreadTaskTag(); + std::vector<std::unique_ptr<RowFinalizeTask>> aTasks(nThreads); + for ( size_t i = 0; i < nThreads; i++ ) + aTasks[ i ].reset( new RowFinalizeTask( pTag, rColXFIndexes, nStartColAllDefault, i == 0 ) ); + + size_t nIdx = 0; + for ( const auto& rEntry : maRowMap ) + { + aTasks[ nIdx % nThreads ]->push_back( rEntry.second.get() ); + ++nIdx; + } + + for ( size_t i = 1; i < nThreads; i++ ) + rPool.pushTask( std::move(aTasks[ i ]) ); + + // Progress bar updates must be synchronous to avoid deadlock + aTasks[0]->doWork(); + + rPool.waitUntilDone(pTag); + } + + // *** Default row format *** --------------------------------------------- + + std::map< XclExpDefaultRowData, size_t > aDefRowMap; + + XclExpDefaultRowData aMaxDefData; + size_t nMaxDefCount = 0; + // only look for default format in existing rows, if there are more than unused + // if the row is hidden, then row xml must be created even if it not contain cells + XclExpRow* pPrev = nullptr; + std::vector< XclExpRow* > aRepeated; + for (const auto& rEntry : maRowMap) + { + const RowRef& rRow = rEntry.second; + if ( rRow->IsDefaultable() ) + { + XclExpDefaultRowData aDefData( *rRow ); + size_t& rnDefCount = aDefRowMap[ aDefData ]; + ++rnDefCount; + if( rnDefCount > nMaxDefCount ) + { + nMaxDefCount = rnDefCount; + aMaxDefData = aDefData; + } + } + if ( pPrev ) + { + if ( pPrev->IsDefaultable() ) + { + // if the previous row we processed is not + // defaultable then afaict the rows in between are + // not used ( and not repeatable ) + sal_uInt32 nRpt = rRow->GetXclRow() - pPrev->GetXclRow(); + if ( nRpt > 1 ) + aRepeated.push_back( pPrev ); + pPrev->SetXclRowRpt( nRpt ); + XclExpDefaultRowData aDefData( *pPrev ); + size_t& rnDefCount = aDefRowMap[ aDefData ]; + rnDefCount += ( pPrev->GetXclRowRpt() - 1 ); + if( rnDefCount > nMaxDefCount ) + { + nMaxDefCount = rnDefCount; + aMaxDefData = aDefData; + } + } + } + pPrev = rRow.get(); + } + // return the default row format to caller + rDefRowData = aMaxDefData; + + // now disable repeating extra (empty) rows that are equal to the default row + for (auto& rpRow : aRepeated) + { + if ( rpRow->GetXclRowRpt() > 1 + && rpRow->GetHeight() == rDefRowData.mnHeight + && rpRow->IsHidden() == rDefRowData.IsHidden() ) + { + rpRow->SetXclRowRpt( 1 ); + } + } + + // *** Disable unused ROW records, find used area *** --------------------- + + sal_uInt16 nFirstUsedXclCol = SAL_MAX_UINT16; + sal_uInt16 nFirstFreeXclCol = 0; + sal_uInt32 nFirstUsedXclRow = SAL_MAX_UINT32; + sal_uInt32 nFirstFreeXclRow = 0; + + for (const auto& rEntry : maRowMap) + { + const RowRef& rRow = rEntry.second; + // disable unused rows + rRow->DisableIfDefault( aMaxDefData ); + + // find used column range + if( !rRow->IsEmpty() ) // empty rows return (0...0) as used range + { + nFirstUsedXclCol = ::std::min( nFirstUsedXclCol, rRow->GetFirstUsedXclCol() ); + nFirstFreeXclCol = ::std::max( nFirstFreeXclCol, rRow->GetFirstFreeXclCol() ); + } + + // find used row range + if( rRow->IsEnabled() ) + { + sal_uInt32 nXclRow = rRow->GetXclRow(); + nFirstUsedXclRow = ::std::min< sal_uInt32 >( nFirstUsedXclRow, nXclRow ); + nFirstFreeXclRow = ::std::max< sal_uInt32 >( nFirstFreeXclRow, nXclRow + 1 ); + } + } + + // adjust start position, if there are no or only empty/disabled ROW records + nFirstUsedXclCol = ::std::min( nFirstUsedXclCol, nFirstFreeXclCol ); + nFirstUsedXclRow = ::std::min( nFirstUsedXclRow, nFirstFreeXclRow ); + + // initialize the DIMENSIONS record + maDimensions.SetDimensions( + nFirstUsedXclCol, nFirstUsedXclRow, nFirstFreeXclCol, nFirstFreeXclRow ); +} + +void XclExpRowBuffer::Save( XclExpStream& rStrm ) +{ + // DIMENSIONS record + maDimensions.Save( rStrm ); + + // save in blocks of 32 rows, each block contains first all ROWs, then all cells + size_t nSize = maRowMap.size(); + RowMap::iterator itr = maRowMap.begin(), itrEnd = maRowMap.end(); + RowMap::iterator itrBlkStart = maRowMap.begin(), itrBlkEnd = maRowMap.begin(); + sal_uInt16 nStartXclRow = (nSize == 0) ? 0 : itr->second->GetXclRow(); + + for (; itr != itrEnd; ++itr) + { + // find end of row block + itrBlkEnd = std::find_if_not(itrBlkEnd, itrEnd, + [&nStartXclRow](const RowMap::value_type& rRow) { return rRow.second->GetXclRow() - nStartXclRow < EXC_ROW_ROWBLOCKSIZE; }); + + // write the ROW records + std::for_each(itrBlkStart, itrBlkEnd, [&rStrm](const RowMap::value_type& rRow) { rRow.second->Save( rStrm ); }); + + // write the cell records + std::for_each(itrBlkStart, itrBlkEnd, [&rStrm](const RowMap::value_type& rRow) { rRow.second->WriteCellList( rStrm ); }); + + itrBlkStart = (itrBlkEnd == itrEnd) ? itrBlkEnd : itrBlkEnd++; + nStartXclRow += EXC_ROW_ROWBLOCKSIZE; + } +} + +void XclExpRowBuffer::SaveXml( XclExpXmlStream& rStrm ) +{ + if (std::none_of(maRowMap.begin(), maRowMap.end(), [](const RowMap::value_type& rRow) { return rRow.second->IsEnabled(); })) + { + rStrm.GetCurrentStream()->singleElement(XML_sheetData); + return; + } + + sax_fastparser::FSHelperPtr& rWorksheet = rStrm.GetCurrentStream(); + rWorksheet->startElement(XML_sheetData); + for (const auto& rEntry : maRowMap) + rEntry.second->SaveXml(rStrm); + rWorksheet->endElement( XML_sheetData ); +} + +XclExpRow& XclExpRowBuffer::GetOrCreateRow( sal_uInt32 nXclRow, bool bRowAlwaysEmpty ) +{ + // This is called rather often, so optimize for the most common case of saving row by row + // (so the requested row is often the last one in the map or belongs after the last one). + RowMap::iterator itr; + if(maRowMap.empty()) + itr = maRowMap.end(); + else + { + RowMap::reverse_iterator last = maRowMap.rbegin(); + if( last->first == nXclRow ) + return *last->second; + if( nXclRow > last->first ) + itr = maRowMap.end(); + else + itr = maRowMap.lower_bound( nXclRow ); + } + const bool bFound = itr != maRowMap.end(); + // bFoundHigher: nXclRow was identical to the previous entry, so not explicitly created earlier + // coverity[deref_iterator : FALSE] - clearly itr if only derefed if bFound which checks for valid itr + const bool bFoundHigher = bFound && itr->first != nXclRow; + if( bFound && !bFoundHigher ) + return *itr->second; + + size_t nFrom = 0; + RowRef pPrevEntry; + if( itr != maRowMap.begin() ) + { + --itr; + pPrevEntry = itr->second; + if( bFoundHigher ) + nFrom = nXclRow; + else + nFrom = itr->first + 1; + } + + const ScDocument& rDoc = GetRoot().GetDoc(); + const SCTAB nScTab = GetRoot().GetCurrScTab(); + // Do not repeatedly call RowHidden() / GetRowHeight() for same values. + bool bHidden = false; + SCROW lastSameHiddenRow = -1; + sal_uInt16 nHeight = 0; + SCROW lastSameHeightRow = -1; + // create the missing rows first + while( nFrom <= nXclRow ) + { + // only create RowMap entries if it is first row in spreadsheet, + // if it is the desired row, or for rows that differ from previous. + if( static_cast<SCROW>(nFrom) > lastSameHiddenRow ) + bHidden = rDoc.RowHidden(nFrom, nScTab, nullptr, &lastSameHiddenRow); + // Always get the actual row height even if the manual size flag is + // not set, to correctly export the heights of rows with wrapped + // texts. + if( static_cast<SCROW>(nFrom) > lastSameHeightRow ) + nHeight = rDoc.GetRowHeight(nFrom, nScTab, nullptr, &lastSameHeightRow, false); + if ( !pPrevEntry || ( nFrom == nXclRow ) || + ( maOutlineBfr.IsCollapsed() ) || + ( maOutlineBfr.GetLevel() != 0 ) || + ( bRowAlwaysEmpty && !pPrevEntry->IsEmpty() ) || + ( bHidden != pPrevEntry->IsHidden() ) || + ( nHeight != pPrevEntry->GetHeight() ) ) + { + if( maOutlineBfr.GetLevel() > mnHighestOutlineLevel ) + { + mnHighestOutlineLevel = maOutlineBfr.GetLevel(); + } + RowRef p = std::make_shared<XclExpRow>(GetRoot(), nFrom, maOutlineBfr, bRowAlwaysEmpty, bHidden, nHeight); + maRowMap.emplace(nFrom, p); + pPrevEntry = p; + } + ++nFrom; + } + itr = maRowMap.find(nXclRow); + return *itr->second; +} + +// Cell Table + +XclExpCellTable::XclExpCellTable( const XclExpRoot& rRoot ) : + XclExpRoot( rRoot ), + maColInfoBfr( rRoot ), + maRowBfr( rRoot ), + maArrayBfr( rRoot ), + maShrfmlaBfr( rRoot ), + maTableopBfr( rRoot ), + mxDefrowheight( new XclExpDefrowheight() ), + mxGuts( new XclExpGuts( rRoot ) ), + mxNoteList( new XclExpNoteList ), + mxMergedcells( new XclExpMergedcells( rRoot ) ), + mxHyperlinkList( new XclExpHyperlinkList ), + mxDval( new XclExpDval( rRoot ) ), + mxExtLst( new XclExtLst( rRoot ) ) +{ + ScDocument& rDoc = GetDoc(); + SCTAB nScTab = GetCurrScTab(); + SvNumberFormatter& rFormatter = GetFormatter(); + + // maximum sheet limits + SCCOL nMaxScCol = GetMaxPos().Col(); + SCROW nMaxScRow = GetMaxPos().Row(); + + // find used area (non-empty cells) + SCCOL nLastUsedScCol; + SCROW nLastUsedScRow; + rDoc.GetTableArea( nScTab, nLastUsedScCol, nLastUsedScRow ); + + if(nLastUsedScCol > nMaxScCol) + nLastUsedScCol = nMaxScCol; + + // check extra blank rows to avoid of losing their not default settings (workaround for tdf#41425) + nLastUsedScRow += 1000; + + if(nLastUsedScRow > nMaxScRow) + nLastUsedScRow = nMaxScRow; + + ScRange aUsedRange( 0, 0, nScTab, nLastUsedScCol, nLastUsedScRow, nScTab ); + GetAddressConverter().ValidateRange( aUsedRange, true ); + nLastUsedScRow = aUsedRange.aEnd.Row(); + + // first row without any set attributes (height/hidden/...) + SCROW nFirstUnflaggedScRow = rDoc.GetLastFlaggedRow( nScTab ) + 1; + + // find range of outlines + SCROW nFirstUngroupedScRow = 0; + if( const ScOutlineTable* pOutlineTable = rDoc.GetOutlineTable( nScTab ) ) + { + SCCOLROW nScStartPos, nScEndPos; + const ScOutlineArray& rRowArray = pOutlineTable->GetRowArray(); + rRowArray.GetRange( nScStartPos, nScEndPos ); + // +1 because open/close button is in next row in Excel, +1 for "end->first unused" + nFirstUngroupedScRow = static_cast< SCROW >( nScEndPos + 2 ); + } + + // column settings + /* #i30411# Files saved with SO7/OOo1.x with nonstandard default column + formatting cause big Excel files, because all rows from row 1 to row + 32000 are exported. Now, if the used area goes exactly to row 32000, + use this row as default and ignore all rows >32000. + #i59220# Tolerance of +-128 rows for inserted/removed rows. */ + if( (31871 <= nLastUsedScRow) && (nLastUsedScRow <= 32127) && (nFirstUnflaggedScRow < nLastUsedScRow) && (nFirstUngroupedScRow <= nLastUsedScRow) ) + nMaxScRow = nLastUsedScRow; + maColInfoBfr.Initialize( nMaxScRow ); + + // range for cell iterator + SCCOL nLastIterScCol = nMaxScCol; + SCROW nLastIterScRow = ulimit_cast< SCROW >( nLastUsedScRow, nMaxScRow ); + ScUsedAreaIterator aIt( rDoc, nScTab, 0, 0, nLastIterScCol, nLastIterScRow ); + + // activate the correct segment and sub segment at the progress bar + GetProgressBar().ActivateCreateRowsSegment(); + + for( bool bIt = aIt.GetNext(); bIt; bIt = aIt.GetNext() ) + { + SCCOL nScCol = aIt.GetStartCol(); + SCROW nScRow = aIt.GetRow(); + SCCOL nLastScCol = aIt.GetEndCol(); + ScAddress aScPos( nScCol, nScRow, nScTab ); + + XclAddress aXclPos( static_cast< sal_uInt16 >( nScCol ), static_cast< sal_uInt32 >( nScRow ) ); + sal_uInt16 nLastXclCol = static_cast< sal_uInt16 >( nLastScCol ); + + const ScRefCellValue& rScCell = aIt.GetCell(); + XclExpCellRef xCell; + + const ScPatternAttr* pPattern = aIt.GetPattern(); + + // handle overlapped merged cells before creating the cell record + sal_uInt32 nMergeBaseXFId = EXC_XFID_NOTFOUND; + bool bIsMergedBase = false; + if( pPattern ) + { + const SfxItemSet& rItemSet = pPattern->GetItemSet(); + // base cell in a merged range + const ScMergeAttr& rMergeItem = rItemSet.Get( ATTR_MERGE ); + bIsMergedBase = rMergeItem.IsMerged(); + /* overlapped cell in a merged range; in Excel all merged cells + must contain same XF index, for correct border */ + const ScMergeFlagAttr& rMergeFlagItem = rItemSet.Get( ATTR_MERGE_FLAG ); + if( rMergeFlagItem.IsOverlapped() ) + nMergeBaseXFId = mxMergedcells->GetBaseXFId( aScPos ); + } + + OUString aAddNoteText; // additional text to be appended to a note + + switch (rScCell.meType) + { + case CELLTYPE_VALUE: + { + double fValue = rScCell.mfValue; + + if (pPattern) + { + OUString aUrl = pPattern->GetItemSet().Get(ATTR_HYPERLINK).GetValue(); + if (!aUrl.isEmpty()) + { + rtl::Reference<XclExpHyperlink> aLink = + new XclExpHyperlink(GetRoot(), SvxURLField(aUrl, aUrl), aScPos); + mxHyperlinkList->AppendRecord(aLink); + } + } + + // try to create a Boolean cell + if( pPattern && ((fValue == 0.0) || (fValue == 1.0)) ) + { + sal_uInt32 nScNumFmt = pPattern->GetItemSet().Get( ATTR_VALUE_FORMAT ).GetValue(); + if( rFormatter.GetType( nScNumFmt ) == SvNumFormatType::LOGICAL ) + xCell = new XclExpBooleanCell( + GetRoot(), aXclPos, pPattern, nMergeBaseXFId, fValue != 0.0 ); + } + + // try to create an RK value (compressed floating-point number) + sal_Int32 nRkValue; + if( !xCell && XclTools::GetRKFromDouble( nRkValue, fValue ) ) + xCell = new XclExpRkCell( + GetRoot(), aXclPos, pPattern, nMergeBaseXFId, nRkValue ); + + // else: simple floating-point number cell + if( !xCell ) + xCell = new XclExpNumberCell( + GetRoot(), aXclPos, pPattern, nMergeBaseXFId, fValue ); + } + break; + + case CELLTYPE_STRING: + { + xCell = new XclExpLabelCell( + GetRoot(), aXclPos, pPattern, nMergeBaseXFId, rScCell.mpString->getString()); + } + break; + + case CELLTYPE_EDIT: + { + XclExpHyperlinkHelper aLinkHelper( GetRoot(), aScPos ); + xCell = new XclExpLabelCell( + GetRoot(), aXclPos, pPattern, nMergeBaseXFId, rScCell.mpEditText, aLinkHelper); + + // add a single created HLINK record to the record list + if( aLinkHelper.HasLinkRecord() ) + mxHyperlinkList->AppendRecord( aLinkHelper.GetLinkRecord() ); + // add list of multiple URLs to the additional cell note text + if( aLinkHelper.HasMultipleUrls() ) + aAddNoteText = ScGlobal::addToken( aAddNoteText, aLinkHelper.GetUrlList(), '\n', 2 ); + } + break; + + case CELLTYPE_FORMULA: + { + if (pPattern) + { + OUString aUrl = pPattern->GetItemSet().Get(ATTR_HYPERLINK).GetValue(); + if (!aUrl.isEmpty()) + { + rtl::Reference<XclExpHyperlink> aLink = + new XclExpHyperlink(GetRoot(), SvxURLField(aUrl, aUrl), aScPos); + mxHyperlinkList->AppendRecord(aLink); + } + } + + xCell = new XclExpFormulaCell( + GetRoot(), aXclPos, pPattern, nMergeBaseXFId, + *rScCell.mpFormula, maArrayBfr, maShrfmlaBfr, maTableopBfr); + } + break; + + default: + OSL_FAIL( "XclExpCellTable::XclExpCellTable - unknown cell type" ); + [[fallthrough]]; + case CELLTYPE_NONE: + { + xCell = new XclExpBlankCell( + GetRoot(), aXclPos, nLastXclCol, pPattern, nMergeBaseXFId ); + } + break; + } + + assert(xCell && "can only reach here with xCell set"); + + // insert the cell into the current row + maRowBfr.AppendCell( xCell, bIsMergedBase ); + + if ( !aAddNoteText.isEmpty() ) + mxNoteList->AppendNewRecord( new XclExpNote( GetRoot(), aScPos, nullptr, aAddNoteText ) ); + + // other sheet contents + if( pPattern ) + { + const SfxItemSet& rItemSet = pPattern->GetItemSet(); + + // base cell in a merged range + if( bIsMergedBase ) + { + const ScMergeAttr& rMergeItem = rItemSet.Get( ATTR_MERGE ); + ScRange aScRange( aScPos ); + aScRange.aEnd.IncCol( rMergeItem.GetColMerge() - 1 ); + aScRange.aEnd.IncRow( rMergeItem.GetRowMerge() - 1 ); + sal_uInt32 nXFId = xCell->GetFirstXFId(); + // blank cells merged vertically may occur repeatedly + OSL_ENSURE( (aScRange.aStart.Col() == aScRange.aEnd.Col()) || (nScCol == nLastScCol), + "XclExpCellTable::XclExpCellTable - invalid repeated blank merged cell" ); + for( SCCOL nIndex = nScCol; nIndex <= nLastScCol; ++nIndex ) + { + mxMergedcells->AppendRange( aScRange, nXFId ); + aScRange.aStart.IncCol(); + aScRange.aEnd.IncCol(); + } + } + + // data validation + if( ScfTools::CheckItem( rItemSet, ATTR_VALIDDATA, false ) ) + { + sal_uLong nScHandle = rItemSet.Get( ATTR_VALIDDATA ).GetValue(); + ScRange aScRange( aScPos ); + aScRange.aEnd.SetCol( nLastScCol ); + mxDval->InsertCellRange( aScRange, nScHandle ); + } + } + } + + // create missing row settings for rows anyhow flagged or with outlines + maRowBfr.CreateRows( ::std::max( nFirstUnflaggedScRow, nFirstUngroupedScRow ) ); +} + +void XclExpCellTable::Finalize(bool bXLS) +{ + // Finalize multiple operations. + maTableopBfr.Finalize(); + + /* Finalize column buffer. This calculates column default XF indexes from + the XF identifiers and fills a vector with these XF indexes. */ + ScfUInt16Vec aColXFIndexes; + maColInfoBfr.Finalize( aColXFIndexes, bXLS ); + + // Usually many indexes towards the end will be EXC_XF_DEFAULTCELL, find + // the index that starts all EXC_XF_DEFAULTCELL until the end. + size_t nStartColAllDefault = findFirstAllSameUntilEnd( aColXFIndexes, EXC_XF_DEFAULTCELL ); + + /* Finalize row buffer. This calculates all cell XF indexes from the XF + identifiers. Then the XF index vector aColXFIndexes (filled above) is + used to calculate the row default formats. With this, all unneeded blank + cell records (equal to row default or column default) will be removed. + The function returns the (most used) default row format in aDefRowData. */ + XclExpDefaultRowData aDefRowData; + maRowBfr.Finalize( aDefRowData, aColXFIndexes, nStartColAllDefault ); + + // Initialize the DEFROWHEIGHT record. + mxDefrowheight->SetDefaultData( aDefRowData ); +} + +XclExpRecordRef XclExpCellTable::CreateRecord( sal_uInt16 nRecId ) const +{ + XclExpRecordRef xRec; + switch( nRecId ) + { + case EXC_ID3_DIMENSIONS: xRec = new XclExpDelegatingRecord( &const_cast<XclExpRowBuffer*>(&maRowBfr)->GetDimensions() ); break; + case EXC_ID2_DEFROWHEIGHT: xRec = mxDefrowheight; break; + case EXC_ID_GUTS: xRec = mxGuts; break; + case EXC_ID_NOTE: xRec = mxNoteList; break; + case EXC_ID_MERGEDCELLS: xRec = mxMergedcells; break; + case EXC_ID_HLINK: xRec = mxHyperlinkList; break; + case EXC_ID_DVAL: xRec = mxDval; break; + case EXC_ID_EXTLST: xRec = mxExtLst; break; + default: OSL_FAIL( "XclExpCellTable::CreateRecord - unknown record id" ); + } + return xRec; +} + +void XclExpCellTable::Save( XclExpStream& rStrm ) +{ + // DEFCOLWIDTH and COLINFOs + maColInfoBfr.Save( rStrm ); + // ROWs and cell records + maRowBfr.Save( rStrm ); +} + +void XclExpCellTable::SaveXml( XclExpXmlStream& rStrm ) +{ + // DEFAULT row height + XclExpDefaultRowData& rDefData = mxDefrowheight->GetDefaultData(); + sax_fastparser::FSHelperPtr& rWorksheet = rStrm.GetCurrentStream(); + rWorksheet->startElement( XML_sheetFormatPr, + // OOXTODO: XML_baseColWidth + XML_defaultColWidth, OString::number(maColInfoBfr.GetDefColWidth()), + // OOXTODO: XML_customHeight + // OOXTODO: XML_thickTop + // OOXTODO: XML_thickBottom + XML_defaultRowHeight, OString::number(static_cast<double> (rDefData.mnHeight) / 20.0), + XML_zeroHeight, ToPsz( rDefData.IsHidden() ), + XML_outlineLevelRow, OString::number(maRowBfr.GetHighestOutlineLevel()), + XML_outlineLevelCol, OString::number(maColInfoBfr.GetHighestOutlineLevel()) ); + rWorksheet->endElement( XML_sheetFormatPr ); + + maColInfoBfr.SaveXml( rStrm ); + maRowBfr.SaveXml( rStrm ); + mxExtLst->SaveXml( rStrm ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |