tdf#100946 Fix width calculation and add customWidth support (.xlsx)

On some MS Excel version (OS X), the column "width" is
not applied, if "customWidth" key (in "col") is not set to "true".
It means that in case of .xlsx files, exported by LibreOffice,
all columns have default width. To resolve that "customWidth"
key was added during export into .xlsx file format.
During development it appears that Default Column Width is wrongly
calculated, and it was done not according to MS documentation.
This issue was also fixed. After fix default column width is
properly set.
MS documentation:
https://msdn.microsoft.com/en-us/library/documentformat.openxml.spreadsheet.column.aspx

Change-Id: I0d1944081a5ea445d1e4284db62e9b4d504bf1c0
This commit is contained in:
Bartosz Kosiorek
2016-07-19 00:26:54 +02:00
committed by Markus Mohrhard
parent 72e6f08c69
commit 40d892a2db
8 changed files with 122 additions and 8 deletions

Binary file not shown.

View File

@@ -110,6 +110,7 @@ public:
void testCellNoteExportXLS();
void testFormatExportODS();
void testCustomColumnWidthExportXLSX();
void testOutlineExportXLSX();
void testHiddenEmptyRowsXLSX();
void testLandscapeOrientationXLSX();
@@ -197,6 +198,7 @@ public:
CPPUNIT_TEST(testCellNoteExportXLS);
CPPUNIT_TEST(testFormatExportODS);
CPPUNIT_TEST(testCustomColumnWidthExportXLSX);
CPPUNIT_TEST(testOutlineExportXLSX);
CPPUNIT_TEST(testHiddenEmptyRowsXLSX);
CPPUNIT_TEST(testLandscapeOrientationXLSX);
@@ -479,6 +481,100 @@ void ScExportTest::testFormatExportODS()
xDocSh->DoClose();
}
void ScExportTest::testCustomColumnWidthExportXLSX()
{
//tdf#100946 FILESAVE Excel on OS X ignored column widths in XLSX last saved by LO
ScDocShellRef xShell = loadDoc("custom_column_width.", FORMAT_ODS);
CPPUNIT_ASSERT(xShell.Is());
std::shared_ptr<utl::TempFile> pXPathFile = ScBootstrapFixture::exportTo(&(*xShell), FORMAT_XLSX);
xmlDocPtr pSheet = XPathHelper::parseExport(pXPathFile, m_xSFactory, "xl/worksheets/sheet1.xml");
CPPUNIT_ASSERT(pSheet);
// First column, has everything default
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[1]", "hidden", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[1]", "outlineLevel", "0");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[1]", "customWidth", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[1]", "collapsed", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[1]", "min", "1");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[1]", "max", "1");
// Second column, has custom width
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[2]", "hidden", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[2]", "outlineLevel", "0");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[2]", "customWidth", "true");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[2]", "collapsed", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[2]", "min", "2");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[2]", "max", "2");
// Third column, has everything default
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[3]", "hidden", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[3]", "outlineLevel", "0");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[3]", "customWidth", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[3]", "collapsed", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[3]", "min", "3");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[3]", "max", "3");
// Fourth column has custom width. Columns from 4 to 7 are hidden
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[4]", "hidden", "true");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[4]", "outlineLevel", "0");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[4]", "customWidth", "true");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[4]", "collapsed", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[4]", "min", "4");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[4]", "max", "4");
// 5th column has custom width. Columns from 4 to 7 are hidden
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[5]", "hidden", "true");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[5]", "outlineLevel", "0");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[5]", "customWidth", "true");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[5]", "collapsed", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[5]", "min", "5");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[5]", "max", "5");
// 6th and 7th columns has default width and it are hidden
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[6]", "hidden", "true");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[6]", "outlineLevel", "0");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[6]", "customWidth", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[6]", "collapsed", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[6]", "min", "6");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[6]", "max", "7");
// 8th column has everything default
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[7]", "hidden", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[7]", "outlineLevel", "0");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[7]", "customWidth", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[7]", "collapsed", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[7]", "min", "8");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[7]", "max", "8");
// 9th column has custom width
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[8]", "hidden", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[8]", "outlineLevel", "0");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[8]", "customWidth", "true");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[8]", "collapsed", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[8]", "min", "9");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[8]", "max", "9");
// Rest of columns are default
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[9]", "hidden", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[9]", "outlineLevel", "0");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[9]", "customWidth", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[9]", "collapsed", "false");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[9]", "min", "10");
assertXPath(pSheet, "/x:worksheet/x:cols/x:col[9]", "max", "1025");
// We expected that exactly 9 unique Nodes will be produced
assertXPath(pSheet, "/x:worksheet/x:cols/x:col", 9);
assertXPath(pSheet, "/x:worksheet/x:sheetData/x:row[1]", "hidden", "false");
assertXPath(pSheet, "/x:worksheet/x:sheetData/x:row[1]", "outlineLevel", "0");
assertXPath(pSheet, "/x:worksheet/x:sheetData/x:row[1]", "collapsed", "false");
assertXPath(pSheet, "/x:worksheet/x:sheetData/x:row[1]", "customFormat", "false");
assertXPath(pSheet, "/x:worksheet/x:sheetData/x:row[1]", "customHeight", "false");
}
void ScExportTest::testOutlineExportXLSX()
{
//tdf#100347 FILESAVE FILEOPEN after exporting to .xlsx format grouping are lost

View File

@@ -1637,7 +1637,7 @@ void ScFiltersTest::testChartImportXLS()
const SdrOle2Obj* pOleObj = getSingleChartObject(rDoc, 0);
CPPUNIT_ASSERT_MESSAGE("Failed to retrieve a chart object from the 2nd sheet.", pOleObj);
CPPUNIT_ASSERT_EQUAL(11148L, pOleObj->GetLogicRect().getWidth());
CPPUNIT_ASSERT_EQUAL(11137L, pOleObj->GetLogicRect().getWidth());
CPPUNIT_ASSERT(8640L > pOleObj->GetLogicRect().getHeight());
xDocSh->DoClose();

View File

@@ -1561,20 +1561,28 @@ XclExpDefcolwidth::XclExpDefcolwidth( const XclExpRoot& rRoot ) :
bool XclExpDefcolwidth::IsDefWidth( sal_uInt16 nXclColWidth ) const
{
double fNewColWidth = lclGetCorrectedColWidth( GetRoot(), nXclColWidth );
// This formula is taking number of characters with GetValue()
// and it is translating it into default column width. 0.5 means half character.
// https://msdn.microsoft.com/en-us/library/documentformat.openxml.spreadsheet.column.aspx
long defaultColumnWidth = static_cast< long >( 256.0 * ( GetValue() + 0.5 ) );
// exactly matched, if difference is less than 1/16 of a character to the left or to the right
return std::abs( static_cast< long >( GetValue() * 256.0 - fNewColWidth + 0.5 ) ) < 16;
return std::abs( defaultColumnWidth - fNewColWidth ) < 16;
}
void XclExpDefcolwidth::SetDefWidth( sal_uInt16 nXclColWidth )
{
double fNewColWidth = lclGetCorrectedColWidth( GetRoot(), nXclColWidth );
SetValue( limit_cast< sal_uInt16 >( fNewColWidth / 256.0 + 0.5 ) );
// This function is taking width and translate it into number of characters
// Next this number of characters are stored. 0.5 means half character.
SetValue( limit_cast< sal_uInt16 >( fNewColWidth / 256.0 - 0.5 ) );
}
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 ),
@@ -1593,9 +1601,14 @@ XclExpColinfo::XclExpColinfo( const XclExpRoot& rRoot,
sal_uInt16 nScWidth = rDoc.GetColWidth( nScCol, nScTab, false );
mnWidth = XclTools::GetXclColumnWidth( nScWidth, GetCharWidth() );
mnScWidth = sc::TwipsToHMM( nScWidth );
// column flags
::set_flag( mnFlags, EXC_COLINFO_HIDDEN, rDoc.ColHidden(nScCol, nScTab) );
// TODO Do we need to save customWidth information also for .xls (with mnFlags)?
XclExpDefcolwidth defColWidth = XclExpDefcolwidth( rRoot );
mbCustomWidth = !defColWidth.IsDefWidth( mnWidth );
// outline data
rOutlineBfr.Update( nScCol );
::set_flag( mnFlags, EXC_COLINFO_COLLAPSED, rOutlineBfr.IsCollapsed() );
@@ -1655,7 +1668,7 @@ void XclExpColinfo::SaveXml( XclExpXmlStream& rStrm )
rStrm.GetCurrentStream()->singleElement( XML_col,
// OOXTODO: XML_bestFit,
XML_collapsed, XclXmlUtils::ToPsz( ::get_flag( mnFlags, EXC_COLINFO_COLLAPSED ) ),
// OOXTODO: XML_customWidth,
XML_customWidth, XclXmlUtils::ToPsz( mbCustomWidth ),
XML_hidden, XclXmlUtils::ToPsz( ::get_flag( mnFlags, EXC_COLINFO_HIDDEN ) ),
XML_outlineLevel, OString::number( mnOutlineLevel ).getStr(),
XML_max, OString::number( (nLastXclCol + 1) ).getStr(),

View File

@@ -310,13 +310,13 @@ sal_Int32 XclTools::GetHmmFromTwips( sal_Int32 nTwips )
sal_uInt16 XclTools::GetScColumnWidth( sal_uInt16 nXclWidth, long nScCharWidth )
{
double fScWidth = static_cast< double >( nXclWidth ) / 256.0 * nScCharWidth + 0.5;
double fScWidth = static_cast< double >( nXclWidth ) / 256.0 * nScCharWidth - 0.5;
return limit_cast< sal_uInt16 >( fScWidth );
}
sal_uInt16 XclTools::GetXclColumnWidth( sal_uInt16 nScWidth, long nScCharWidth )
{
double fXclWidth = static_cast< double >( nScWidth ) * 256.0 / nScCharWidth + 0.5;
double fXclWidth = ( static_cast< double >( nScWidth ) + 0.5 ) * 256.0 / nScCharWidth;
return limit_cast< sal_uInt16 >( fXclWidth );
}
@@ -627,6 +627,7 @@ bool XclTools::GetBuiltInStyleId( sal_uInt8& rnStyleId, sal_uInt8& rnLevel, cons
return true;
}
}
rnStyleId = EXC_STYLE_USERDEF;
rnLevel = EXC_STYLE_NOLEVEL;
return false;

View File

@@ -683,6 +683,9 @@ private:
if the '0' character fits 10 times into a cell in a column with default
width.
Half of character width is reserved for non character display.
It is margin padding (two on each side) and padding for the gridlines.
The IsDefWidth() function returns true, if the passed width (measured in
1/256 of the width of the '0' character) could be converted exactly to the
default width. If the passed width is rounded up or down to get the default
@@ -740,6 +743,7 @@ private:
private:
XclExpXFId maXFId; /// The XF identifier for column default format.
bool mbCustomWidth; /// True = Column width is different from default width
sal_uInt16 mnWidth; /// Excel width of the column.
sal_uInt16 mnScWidth; /// Calc width of the column.
sal_uInt16 mnFlags; /// Additional column flags.

View File

@@ -143,6 +143,7 @@ public:
static sal_Int32 GetHmmFromTwips( sal_Int32 nTwips );
/** Returns the Calc column width (twips) for the passed Excel width.
* Excel Column width is stored as 1/256th of a character.
@param nScCharWidth Width of the '0' character in Calc (twips). */
static sal_uInt16 GetScColumnWidth( sal_uInt16 nXclWidth, long nScCharWidth );
/** Returns the Excel column width for the passed Calc width (twips).

View File

@@ -1193,11 +1193,10 @@ sal_Int32 getColumnWidth(UnitConverter& rConverter, double nWidth)
long nPixel = aDev->LogicToPixel(Point(nCoeff, 0), MapMode(MAP_100TH_MM)).getX();
// the 1.047 has been experimentally chosen based on measurements with a screen ruler
// TODO: fix the display of cells so that it no longer requires this hack
// algorithm from OOXML spec part1: 18.3.1.13
sal_Int32 nColWidthPixel= std::floor(((256*nWidth + std::floor(128.0/nPixel))/256.0)*nPixel) * 1.047;
sal_Int32 nColWidthPixel= std::floor( ( ( 256 * nWidth + std::floor( 128.0 / nPixel ) ) / 256.0 ) * nPixel ) * 1.047;
return aDev->PixelToLogic(Point(nColWidthPixel, 0), MapMode(MAP_100TH_MM)).getX();
}