sc/source/ui/view/output2.cxx | 16 ++- vcl/qa/cppunit/pdfexport/data/tdf160786.fods | 134 +++++++++++++++++++++++++++ vcl/qa/cppunit/pdfexport/pdfexport2.cxx | 49 +++++++++ 3 files changed, 196 insertions(+), 3 deletions(-)
New commits: commit 981c5780da8ec6b6878de58909b7cc7e736ad33a Author: Jonathan Clark <jonat...@libreoffice.org> AuthorDate: Tue Aug 6 06:01:57 2024 -0600 Commit: Jonathan Clark <jonat...@libreoffice.org> CommitDate: Tue Aug 6 21:57:10 2024 +0200 tdf#160786 sc: Fix repeat char format code inaccuracy Number format codes allow arbitrary repeat characters. These repeat characters are inserted while formatting numbers to pad strings to the width of the containing cell. For example, this is used in some default format codes to left-align currency symbols with right-aligned digits. Previously, Calc determined the number of repeat characters to insert based on the assumption that the pixel width of a string containing N of the repeat character would be N times the pixel width of a string containing 1 of them. This is not a safe assumption. This change updates the algorithm to instead look at the average width of a repeat character when used in context as part of a padding string. This approach is still not entirely correct - it doesn't account for ligatures around the padding sequence, for example - but in practice it is a reasonable compromise between cost and accuracy. Change-Id: I240cf38ec0376ee2d2896e7a7da6760a374b3034 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/171548 Tested-by: Jenkins Reviewed-by: Jonathan Clark <jonat...@libreoffice.org> diff --git a/sc/source/ui/view/output2.cxx b/sc/source/ui/view/output2.cxx index 1a8d1356a4a6..cab30c900136 100644 --- a/sc/source/ui/view/output2.cxx +++ b/sc/source/ui/view/output2.cxx @@ -573,8 +573,17 @@ void ScDrawStringsVars::RepeatToFill( tools::Long nColWidth ) if ( nRepeatPos == -1 || nRepeatPos > aString.getLength() ) return; - tools::Long nCharWidth = GetFmtTextWidth(OUString(nRepeatChar)); + // Measuring a string containing a single copy of the repeat char is inaccurate. + // To increase accuracy, start with a representative sample of a padding sequence. + constexpr sal_Int32 nSampleSize = 20; + OUStringBuffer aFill(nSampleSize); + comphelper::string::padToLength(aFill, nSampleSize, nRepeatChar); + tools::Long nSampleWidth = GetFmtTextWidth(aFill.makeStringAndClear()); + double nAvgCharWidth = static_cast<double>(nSampleWidth) / static_cast<double>(nSampleSize); + + // Intentionally truncate to round toward zero + auto nCharWidth = static_cast<tools::Long>(nAvgCharWidth); if ( nCharWidth < 1 || (bPixelToLogic && nCharWidth < pOutput->mpRefDevice->PixelToLogic(Size(1,0)).Width()) ) return; @@ -590,8 +599,9 @@ void ScDrawStringsVars::RepeatToFill( tools::Long nColWidth ) if ( nSpaceToFill <= nCharWidth ) return; - sal_Int32 nCharsToInsert = nSpaceToFill / nCharWidth; - OUStringBuffer aFill(nCharsToInsert); + // Intentionally truncate to round toward zero + auto nCharsToInsert = static_cast<sal_Int32>(static_cast<double>(nSpaceToFill) / nAvgCharWidth); + aFill.ensureCapacity(nCharsToInsert); comphelper::string::padToLength(aFill, nCharsToInsert, nRepeatChar); aString = aString.replaceAt( nRepeatPos, 0, aFill ); TextChanged(); diff --git a/vcl/qa/cppunit/pdfexport/data/tdf160786.fods b/vcl/qa/cppunit/pdfexport/data/tdf160786.fods new file mode 100644 index 000000000000..acb279ee52fc --- /dev/null +++ b/vcl/qa/cppunit/pdfexport/data/tdf160786.fods @@ -0,0 +1,134 @@ +<?xml version='1.0' encoding='UTF-8'?> +<office:document xmlns:presentation="urn:oasis:names:tc:opendocument:xmlns:presentation:1.0" xmlns:css3t="http://www.w3.org/TR/css3-text/" xmlns:grddl="http://www.w3.org/2003/g/data-view#" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xforms="http://www.w3.org/2002/xforms" xmlns:dom="http://www.w3.org/2001/xml-events" xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0" xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0" xmlns:math="http://www.w3.org/1998/Math/MathML" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:ooo="http://openoffice.org/2004/office" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0" xmlns:ooow="http://openoffice.org/2004/writer" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:drawooo="http://openoffice.org/2010/draw" xmlns:oooc="http://o penoffice.org/2004/calc" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2" xmlns:tableooo="http://openoffice.org/2009/table" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0" xmlns:rpt="http://openoffice.org/2005/report" xmlns:formx="urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:field="urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:meta="urn:oasis:name s:tc:opendocument:xmlns:meta:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.spreadsheet"> + <office:meta><meta:creation-date>2024-08-06T05:16:57.794291032</meta:creation-date><meta:editing-duration>P0D</meta:editing-duration><meta:editing-cycles>1</meta:editing-cycles><meta:generator>LibreOfficeDev/25.2.0.0.alpha0$Linux_X86_64 LibreOffice_project/900af9c53788d5d274900f796e3aee14926abf2d</meta:generator><meta:print-date>2024-08-06T05:52:40.231662987</meta:print-date><meta:printed-by>PDF files</meta:printed-by><meta:document-statistic meta:table-count="1" meta:cell-count="2" meta:object-count="0"/></office:meta> + <office:font-face-decls> + <style:font-face style:name="Liberation Sans" svg:font-family="'Liberation Sans'" style:font-family-generic="swiss" style:font-pitch="variable"/> + <style:font-face style:name="Liberation Sans1" svg:font-family="'Liberation Sans'" style:font-family-generic="system" style:font-pitch="variable"/> + <style:font-face style:name="Microsoft YaHei" svg:font-family="'Microsoft YaHei'" style:font-family-generic="system" style:font-pitch="variable"/> + <style:font-face style:name="Noto Serif" svg:font-family="'Noto Serif'" style:font-family-generic="roman" style:font-pitch="variable"/> + </office:font-face-decls> + <office:styles> + <style:default-style style:family="table-cell"> + <style:paragraph-properties style:tab-stop-distance="0.5in"/> + <style:text-properties style:font-name="Liberation Sans" fo:font-size="10pt" fo:language="en" fo:country="US" style:font-name-asian="Microsoft YaHei" style:font-size-asian="10pt" style:language-asian="zh" style:country-asian="CN" style:font-name-complex="Liberation Sans1" style:font-size-complex="10pt" style:language-complex="he" style:country-complex="IL"/> + </style:default-style> + <style:default-style style:family="graphic"> + <style:graphic-properties svg:stroke-color="#3465a4" draw:fill-color="#729fcf" fo:wrap-option="no-wrap" draw:shadow-offset-x="0.1181in" draw:shadow-offset-y="0.1181in" style:writing-mode="page"/> + <style:paragraph-properties style:text-autospace="ideograph-alpha" style:punctuation-wrap="simple" style:line-break="strict" loext:tab-stop-distance="0in" style:writing-mode="page" style:font-independent-line-spacing="false"> + <style:tab-stops/> + </style:paragraph-properties> + <style:text-properties style:use-window-font-color="true" loext:opacity="0%" fo:font-family="'Liberation Serif'" style:font-family-generic="roman" style:font-pitch="variable" fo:font-size="12pt" fo:language="en" fo:country="US" style:letter-kerning="true" style:font-family-asian="'Segoe UI'" style:font-family-generic-asian="system" style:font-pitch-asian="variable" style:font-size-asian="12pt" style:language-asian="zh" style:country-asian="CN" style:font-family-complex="Tahoma" style:font-family-generic-complex="system" style:font-pitch-complex="variable" style:font-size-complex="12pt" style:language-complex="he" style:country-complex="IL"/> + </style:default-style> + <number:number-style style:name="N0"> + <number:number number:min-integer-digits="1"/> + </number:number-style> + <number:currency-style style:name="N132P0" style:volatile="true"> + <number:currency-symbol number:language="en" number:country="US">$</number:currency-symbol> + <number:fill-character>f</number:fill-character> + <number:number number:decimal-places="2" number:min-decimal-places="2" number:min-integer-digits="1" number:grouping="true"/> + </number:currency-style> + <number:currency-style style:name="N132"> + <number:text>-</number:text> + <number:currency-symbol number:language="en" number:country="US">$</number:currency-symbol> + <number:fill-character> </number:fill-character> + <number:number number:decimal-places="2" number:min-decimal-places="2" number:min-integer-digits="1" number:grouping="true"/> + <style:map style:condition="value()>=0" style:apply-style-name="N132P0"/> + </number:currency-style> + <style:style style:name="Default" style:family="table-cell"/> + </office:styles> + <office:automatic-styles> + <style:style style:name="co1" style:family="table-column"> + <style:table-column-properties fo:break-before="auto" style:column-width="0.5071in"/> + </style:style> + <style:style style:name="co2" style:family="table-column"> + <style:table-column-properties fo:break-before="auto" style:column-width="6.1126in"/> + </style:style> + <style:style style:name="co3" style:family="table-column"> + <style:table-column-properties fo:break-before="auto" style:column-width="0.889in"/> + </style:style> + <style:style style:name="ro1" style:family="table-row"> + <style:table-row-properties style:row-height="0.178in" fo:break-before="auto" style:use-optimal-row-height="true"/> + </style:style> + <style:style style:name="ta1" style:family="table" style:master-page-name="Default"> + <style:table-properties table:display="true" style:writing-mode="lr-tb"/> + </style:style> + <number:number-style style:name="N2"> + <number:number number:decimal-places="2" number:min-decimal-places="2" number:min-integer-digits="1"/> + </number:number-style> + <style:style style:name="ce1" style:family="table-cell" style:parent-style-name="Default"> + <style:table-cell-properties style:text-align-source="fix" style:repeat-content="false"/> + <style:paragraph-properties fo:text-align="end" fo:margin-left="0in"/> + <style:text-properties style:font-name="Noto Serif" fo:font-size="10.5pt" style:font-size-asian="10.5pt" style:font-size-complex="10.5pt"/> + </style:style> + <style:style style:name="ce3" style:family="table-cell" style:parent-style-name="Default" style:data-style-name="N132"> + <style:table-cell-properties style:text-align-source="value-type" style:repeat-content="false"/> + <style:paragraph-properties fo:margin-left="0in"/> + <style:text-properties style:font-name="Noto Serif" fo:font-size="10.5pt" style:font-size-asian="10.5pt" style:font-size-complex="10.5pt"/> + </style:style> + <style:style style:name="ce2" style:family="table-cell" style:parent-style-name="Default"> + <style:table-cell-properties style:text-align-source="fix" style:repeat-content="false"/> + <style:paragraph-properties fo:text-align="end" fo:margin-left="0in"/> + </style:style> + <style:style style:name="ce5" style:family="table-cell" style:parent-style-name="Default"> + <style:table-cell-properties style:text-align-source="value-type" style:repeat-content="false"/> + <style:paragraph-properties fo:margin-left="0in"/> + <style:text-properties style:font-name="Liberation Sans"/> + </style:style> + <style:page-layout style:name="pm1"> + <style:page-layout-properties style:writing-mode="lr-tb"/> + <style:header-style> + <style:header-footer-properties fo:min-height="0.2953in" fo:margin-left="0in" fo:margin-right="0in" fo:margin-bottom="0.0984in"/> + </style:header-style> + <style:footer-style> + <style:header-footer-properties fo:min-height="0.2953in" fo:margin-left="0in" fo:margin-right="0in" fo:margin-top="0.0984in"/> + </style:footer-style> + </style:page-layout> + </office:automatic-styles> + <office:master-styles> + <style:master-page style:name="Default" style:page-layout-name="pm1"> + <style:header> + <text:p><text:sheet-name>???</text:sheet-name></text:p> + </style:header> + <style:header-left style:display="false"/> + <style:header-first style:display="false"/> + <style:footer> + <text:p>Page <text:page-number>1</text:page-number></text:p> + </style:footer> + <style:footer-left style:display="false"/> + <style:footer-first style:display="false"/> + </style:master-page> + </office:master-styles> + <office:body> + <office:spreadsheet> + <table:calculation-settings table:automatic-find-labels="false" table:use-regular-expressions="false" table:use-wildcards="true"/> + <table:table table:name="Sheet1" table:style-name="ta1"> + <table:table-column table:style-name="co1" table:default-cell-style-name="Default"/> + <table:table-column table:style-name="co2" table:default-cell-style-name="Default"/> + <table:table-column table:style-name="co3" table:default-cell-style-name="Default"/> + <table:table-column table:style-name="co3" table:default-cell-style-name="ce5"/> + <table:table-row table:style-name="ro1"> + <table:table-cell table:style-name="ce1" office:value-type="string" calcext:value-type="string"> + <text:p>A</text:p> + </table:table-cell> + <table:table-cell table:style-name="ce3" office:value-type="currency" office:currency="USD" office:value="12345.67" calcext:value-type="currency"> + <text:p>$12,345.67</text:p> + </table:table-cell> + <table:table-cell table:style-name="ce2"/> + <table:table-cell table:style-name="Default"/> + </table:table-row> + <table:table-row table:style-name="ro1" table:number-rows-repeated="7"> + <table:table-cell table:number-columns-repeated="2"/> + <table:table-cell table:style-name="ce2"/> + <table:table-cell/> + </table:table-row> + <table:table-row table:style-name="ro1" table:number-rows-repeated="1048567"> + <table:table-cell table:number-columns-repeated="4"/> + </table:table-row> + <table:table-row table:style-name="ro1"> + <table:table-cell table:number-columns-repeated="4"/> + </table:table-row> + </table:table> + <table:named-expressions/> + </office:spreadsheet> + </office:body> +</office:document> \ No newline at end of file diff --git a/vcl/qa/cppunit/pdfexport/pdfexport2.cxx b/vcl/qa/cppunit/pdfexport/pdfexport2.cxx index 702b71db7eff..9b08b1fa047a 100644 --- a/vcl/qa/cppunit/pdfexport/pdfexport2.cxx +++ b/vcl/qa/cppunit/pdfexport/pdfexport2.cxx @@ -5429,6 +5429,55 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf162194SoftHyphen) CPPUNIT_ASSERT_EQUAL(u"fle"_ustr, aText.at(3).trim()); } +// tdf#160786 - Tests that Calc format code with repeat char is measured correctly +CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf160786) +{ + saveAsPDF(u"tdf160786.fods"); + + auto pPdfDocument = parsePDFExport(); + CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount()); + + auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0); + CPPUNIT_ASSERT(pPdfPage); + auto pTextPage = pPdfPage->getTextPage(); + CPPUNIT_ASSERT(pTextPage); + + int nPageObjectCount = pPdfPage->getObjectCount(); + CPPUNIT_ASSERT_EQUAL(5, nPageObjectCount); + + std::vector<OUString> aText; + std::vector<basegfx::B2DRectangle> aRect; + + int nTextObjectCount = 0; + for (int i = 0; i < nPageObjectCount; ++i) + { + auto pPageObject = pPdfPage->getObject(i); + CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr); + if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text) + { + aText.push_back(pPageObject->getText(pTextPage)); + aRect.push_back(pPageObject->getBounds()); + ++nTextObjectCount; + } + } + + CPPUNIT_ASSERT_EQUAL(5, nTextObjectCount); + + CPPUNIT_ASSERT_EQUAL(u"A"_ustr, aText.at(3).trim()); + + // The currency line is padded with an unknown number of 'f' characters. It doesn't matter how + // many are used, as long as the cell is padded to the expected width. Just verify that this + // text object is the expected one. + CPPUNIT_ASSERT(o3tl::trim(aText.at(4)).starts_with(u"$")); + + // The currency cell must not overlap the adjacent cell + CPPUNIT_ASSERT_GREATEREQUAL(aRect.at(3).getMaxX(), aRect.at(4).getMinX()); + + // The currency cell must be padded to occupy its space reasonably well. + // As a heuristic, ensure the free space is no more than the width of "A" + CPPUNIT_ASSERT_LESS(aRect.at(3).getMaxX() + aRect.at(3).getWidth(), aRect.at(4).getMinX()); +} + } // end anonymous namespace CPPUNIT_PLUGIN_IMPLEMENT();