editeng/source/editeng/impedit.hxx | 9 - editeng/source/editeng/impedit2.cxx | 4 editeng/source/editeng/impedit3.cxx | 20 +-- include/svx/compatflags.hxx | 3 sd/qa/unit/data/odp/trailing-paragraphs-compat.odp |binary sd/qa/unit/data/odp/trailing-paragraphs.odp |binary sd/qa/unit/data/pptx/trailing-paragraphs.pptx |binary sd/qa/unit/layout-tests.cxx | 126 ++++++++++++++++++++- sd/source/ui/docshell/docshel4.cxx | 4 svx/source/svdraw/svdmodel.cxx | 9 + svx/source/svdraw/svdotextdecomposition.cxx | 12 +- 11 files changed, 167 insertions(+), 20 deletions(-)
New commits: commit 40f13a8fc7676f7ed0741b3b4c46d0c06637ce53 Author: Mike Kaganski <mike.kagan...@collabora.com> AuthorDate: Tue Aug 19 23:33:02 2025 +0500 Commit: Xisco Fauli <xiscofa...@libreoffice.org> CommitDate: Thu Aug 21 00:07:02 2025 +0200 tdf#168010: introduce UseTrailingEmptyLinesInLayout compat option In PowerPoint, autoshrink text size is calculated the same way as in Impress, ignoring the empty lines block in the end. But when the text is positioned, these lines are taken into account, which may change position of texts aligned to bottom. Since this method of positioning text may change text placement very seriously, it can't be used unconditionally, and requires a compat flag, which is introduced here, only enabled for PPTX import, and in ODP documents having the option explicitly set. It is not used for other MS Office document types. I saw that Excel behaves similarly to PowerPoint, so maybe it makes sense to enable it for XLSX, too; on the other hand, MS Word works differently. Also I couldn't prepare a test document in binary PPT to test hehavior. The decisions about these file types should go in separate changes. Change-Id: Ie37f1d2b3393f9c52be89586c73df70b108190a1 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/189935 Reviewed-by: Mike Kaganski <mike.kagan...@collabora.com> Tested-by: Jenkins (cherry picked from commit 425b55efaf1f04e8f91cb049a5820fcf61e678dd) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/189942 Reviewed-by: Xisco Fauli <xiscofa...@libreoffice.org> diff --git a/editeng/source/editeng/impedit.hxx b/editeng/source/editeng/impedit.hxx index e91e2f4a5191..8dd95542981a 100644 --- a/editeng/source/editeng/impedit.hxx +++ b/editeng/source/editeng/impedit.hxx @@ -663,7 +663,7 @@ private: void ParaAttribsChanged( ContentNode const * pNode, bool bIgnoreUndoCheck = false ); void TextModified(); - void CalcHeight(ParaPortion& rParaPortion, bool bIsScaling = false); + void CalcHeight(ParaPortion& rParaPortion); bool isInEmptyClusterAtTheEnd(const ParaPortion& rParaPortion, bool bIsScaling); void InsertUndo( std::unique_ptr<EditUndo> pUndo, bool bTryMerge = false ); @@ -695,14 +695,14 @@ private: void Clear(); EditPaM RemoveText(); - bool createLinesForEmptyParagraph(ParaPortion& rParaPortion, bool bIsScaling = false); + bool createLinesForEmptyParagraph(ParaPortion& rParaPortion); tools::Long calculateMaxLineWidth(tools::Long nStartX, SvxLRSpaceItem const& rLRItem, const SvxFontUnitMetrics& rMetrics); void populateRubyInfo(ParaPortion& rParaPortion, EditLine* pLine); - bool CreateLines(sal_Int32 nPara, sal_uInt32 nStartPosY, bool bIsScaling = false); + bool CreateLines(sal_Int32 nPara, sal_uInt32 nStartPosY); void CreateAndInsertEmptyLine(ParaPortion& rParaPortion); - bool FinishCreateLines(ParaPortion& rParaPortion, bool bIsScaling = false); + bool FinishCreateLines(ParaPortion& rParaPortion); void CreateTextPortions(ParaPortion& rParaPortion, sal_Int32& rStartPos); void RecalcTextPortion(ParaPortion& rParaPortion, sal_Int32 nStartPos, sal_Int32 nNewChars); sal_Int32 SplitTextPortion(ParaPortion& rParaPortion, sal_Int32 nPos, EditLine* pCurLine = nullptr); @@ -993,6 +993,7 @@ public: void SetMinColumnWrapHeight(tools::Long nVal) { mnMinColumnWrapHeight = nVal; } + // Returns the height of the text, excluding empty lines in the end tools::Long FormatParagraphs(o3tl::sorted_vector<sal_Int32>& rRepaintParagraphs, bool bIsScaling); void ScaleContentToFitWindow(o3tl::sorted_vector<sal_Int32>& rRepaintParagraphs); void FormatDoc(); diff --git a/editeng/source/editeng/impedit2.cxx b/editeng/source/editeng/impedit2.cxx index 62262a2f83a5..5ce813b2d8f7 100644 --- a/editeng/source/editeng/impedit2.cxx +++ b/editeng/source/editeng/impedit2.cxx @@ -4665,12 +4665,12 @@ bool ImpEditEngine::isInEmptyClusterAtTheEnd(const ParaPortion& rPortion, bool b return false; } -void ImpEditEngine::CalcHeight(ParaPortion& rPortion, bool bIsScaling) +void ImpEditEngine::CalcHeight(ParaPortion& rPortion) { rPortion.mnHeight = 0; rPortion.mnFirstLineOffset = 0; - if (!rPortion.IsVisible() || isInEmptyClusterAtTheEnd(rPortion, bIsScaling)) + if (!rPortion.IsVisible()) return; OSL_ENSURE(rPortion.GetLines().Count(), "Paragraph with no lines in ParaPortion::CalcHeight"); diff --git a/editeng/source/editeng/impedit3.cxx b/editeng/source/editeng/impedit3.cxx index 8eb2f8ee73b0..c8e47018e5d7 100644 --- a/editeng/source/editeng/impedit3.cxx +++ b/editeng/source/editeng/impedit3.cxx @@ -323,7 +323,7 @@ void ImpEditEngine::FormatFullDoc() tools::Long ImpEditEngine::FormatParagraphs(o3tl::sorted_vector<sal_Int32>& aRepaintParagraphList, bool bIsScaling) { sal_Int32 nParaCount = GetParaPortions().Count(); - tools::Long nY = 0; + tools::Long nY = 0, nResult = 0; bool bGrow = false; for (sal_Int32 nParagraph = 0; nParagraph < nParaCount; nParagraph++) @@ -332,7 +332,7 @@ tools::Long ImpEditEngine::FormatParagraphs(o3tl::sorted_vector<sal_Int32>& aRep if (rParaPortion.MustRepaint() || (rParaPortion.IsInvalid() && rParaPortion.IsVisible())) { // No formatting should be necessary for MustRepaint()! - if (CreateLines(nParagraph, nY, bIsScaling)) + if (CreateLines(nParagraph, nY)) { if (!bGrow && GetTextRanger()) { @@ -361,8 +361,10 @@ tools::Long ImpEditEngine::FormatParagraphs(o3tl::sorted_vector<sal_Int32>& aRep aRepaintParagraphList.insert(nParagraph); } nY += rParaPortion.GetHeight(); + if (!isInEmptyClusterAtTheEnd(rParaPortion, bIsScaling)) + nResult = nY; // The total height excluding trailing blank paragraphs } - return nY; + return nResult; } namespace @@ -595,7 +597,7 @@ tools::Long ImpEditEngine::GetColumnWidth(const Size& rPaperSize) const return (nWidth - mnColumnSpacing * (mnColumns - 1)) / mnColumns; } -bool ImpEditEngine::createLinesForEmptyParagraph(ParaPortion& rParaPortion, bool bIsScaling) +bool ImpEditEngine::createLinesForEmptyParagraph(ParaPortion& rParaPortion) { // fast special treatment... if (rParaPortion.GetTextPortions().Count()) @@ -604,7 +606,7 @@ bool ImpEditEngine::createLinesForEmptyParagraph(ParaPortion& rParaPortion, bool rParaPortion.GetLines().Reset(); CreateAndInsertEmptyLine(rParaPortion); - return FinishCreateLines(rParaPortion, bIsScaling); + return FinishCreateLines(rParaPortion); } tools::Long ImpEditEngine::calculateMaxLineWidth(tools::Long nStartX, SvxLRSpaceItem const& rLRItem, @@ -725,7 +727,7 @@ void ImpEditEngine::populateRubyInfo(ParaPortion& rParaPortion, EditLine* pLine) } } -bool ImpEditEngine::CreateLines( sal_Int32 nPara, sal_uInt32 nStartPosY, bool bIsScaling ) +bool ImpEditEngine::CreateLines( sal_Int32 nPara, sal_uInt32 nStartPosY ) { assert(GetParaPortions().exists(nPara) && "Portion paragraph index is not valid"); ParaPortion& rParaPortion = GetParaPortions().getRef(nPara); @@ -742,7 +744,7 @@ bool ImpEditEngine::CreateLines( sal_Int32 nPara, sal_uInt32 nStartPosY, bool bI // Fast special treatment for empty paragraphs... bool bEmptyParagraph = rParaPortion.GetNode()->Len() == 0 && !GetTextRanger(); if (bEmptyParagraph) - return createLinesForEmptyParagraph(rParaPortion, bIsScaling); + return createLinesForEmptyParagraph(rParaPortion); sal_Int64 nCurrentPosY = nStartPosY; // If we're allowed to skip parts outside and this cannot possibly fit in the given height, @@ -2040,12 +2042,12 @@ void ImpEditEngine::CreateAndInsertEmptyLine(ParaPortion& rParaPortion) } } -bool ImpEditEngine::FinishCreateLines(ParaPortion& rParaPortion, bool bIsScaling) +bool ImpEditEngine::FinishCreateLines(ParaPortion& rParaPortion) { // CalcCharPositions( pParaPortion ); rParaPortion.SetValid(); tools::Long nOldHeight = rParaPortion.GetHeight(); - CalcHeight(rParaPortion, bIsScaling); + CalcHeight(rParaPortion); DBG_ASSERT(rParaPortion.GetTextPortions().Count(), "FinishCreateLines: No Text-Portion?"); bool bRet = rParaPortion.GetHeight() != nOldHeight; diff --git a/include/svx/compatflags.hxx b/include/svx/compatflags.hxx index 4caecf18be55..75b85f79607f 100644 --- a/include/svx/compatflags.hxx +++ b/include/svx/compatflags.hxx @@ -14,7 +14,8 @@ enum class SdrCompatibilityFlag LegacyFontwork, ///< for tdf#148000 false == Fontwork works in PowerPoint compat mode ConnectorUseSnapRect, ///< for tdf#149756 IgnoreBreakAfterMultilineField, ///< for tdf#148966 - LAST = IgnoreBreakAfterMultilineField /// add new items above + UseTrailingEmptyLinesInLayout, ///< for tdf#168010 + LAST = UseTrailingEmptyLinesInLayout /// add new items above }; /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/sd/qa/unit/data/odp/trailing-paragraphs-compat.odp b/sd/qa/unit/data/odp/trailing-paragraphs-compat.odp new file mode 100644 index 000000000000..b7f5798e00d4 Binary files /dev/null and b/sd/qa/unit/data/odp/trailing-paragraphs-compat.odp differ diff --git a/sd/qa/unit/data/odp/trailing-paragraphs.odp b/sd/qa/unit/data/odp/trailing-paragraphs.odp new file mode 100644 index 000000000000..ea87cc8f6001 Binary files /dev/null and b/sd/qa/unit/data/odp/trailing-paragraphs.odp differ diff --git a/sd/qa/unit/data/pptx/trailing-paragraphs.pptx b/sd/qa/unit/data/pptx/trailing-paragraphs.pptx new file mode 100644 index 000000000000..fa8907e543bf Binary files /dev/null and b/sd/qa/unit/data/pptx/trailing-paragraphs.pptx differ diff --git a/sd/qa/unit/layout-tests.cxx b/sd/qa/unit/layout-tests.cxx index 6fae40d135c7..9a593b611da0 100644 --- a/sd/qa/unit/layout-tests.cxx +++ b/sd/qa/unit/layout-tests.cxx @@ -10,6 +10,10 @@ #include <sfx2/objsh.hxx> #include <sfx2/sfxbasemodel.hxx> +#include <svx/compatflags.hxx> + +#include <drawdoc.hxx> +#include <unomodel.hxx> class SdLayoutTest : public UnoApiXmlTest { @@ -19,9 +23,8 @@ public: { } - xmlDocUniquePtr load(const char* pName) + xmlDocUniquePtr parseLayout() const { - loadFromFile(OUString::createFromAscii(pName)); SfxBaseModel* pModel = dynamic_cast<SfxBaseModel*>(mxComponent.get()); CPPUNIT_ASSERT(pModel); SfxObjectShell* pShell = pModel->GetObjectShell(); @@ -33,6 +36,21 @@ public: return pXmlDoc; } + + xmlDocUniquePtr load(const char* pName) + { + loadFromFile(OUString::createFromAscii(pName)); + return parseLayout(); + } + + SdDrawDocument* getDoc() + { + auto* pImpress = dynamic_cast<SdXImpressDocument*>(mxComponent.get()); + CPPUNIT_ASSERT(pImpress); + auto* pDoc = pImpress->GetDoc(); + CPPUNIT_ASSERT(pDoc); + return pDoc; + } }; CPPUNIT_TEST_FIXTURE(SdLayoutTest, testTdf104722) @@ -458,6 +476,110 @@ CPPUNIT_TEST_FIXTURE(SdLayoutTest, testTdf164622) "y", u"892"); } +CPPUNIT_TEST_FIXTURE(SdLayoutTest, testTdf168010) +{ + // Test UseTrailingEmptyLinesInLayout compatibility option. + // The test documents have an auto-shrink text "textbox "; the box itself is positioned + // identically in all cases; the text is aligned to bottom. + // When "UseTrailingEmptyLinesInLayout" is set, "textbox" string is placed higher, than when + // the setting is not set (y value ~6700 vs. ~8100). + + // The existing ODPs have a standard draw:text-box. It produces three textarray elements, + // in order from bottom to top. We need the topmost, third. + + // 1. UseTrailingEmptyLinesInLayout must be enabled in an existing ODP with respective option + // in settings.xml + loadFromFile(u"odp/trailing-paragraphs-compat.odp"); + { + CPPUNIT_ASSERT( + getDoc()->GetCompatibilityFlag(SdrCompatibilityFlag::UseTrailingEmptyLinesInLayout)); + + xmlDocUniquePtr pXml = parseLayout(); + sal_Int32 y = getXPath(pXml, "/metafile['1']/push/push/textarray[3]", "y").toInt32(); + CPPUNIT_ASSERT_DOUBLES_EQUAL(6700, y, 100); // could be 6641 or 6760 + assertXPathContent(pXml, "/metafile['1']/push/push/textarray[3]/text", u"textbox"); + } + + // 2. It must stay enabled after ODP round-trip + saveAndReload(u"impress8"_ustr); + { + CPPUNIT_ASSERT( + getDoc()->GetCompatibilityFlag(SdrCompatibilityFlag::UseTrailingEmptyLinesInLayout)); + + xmlDocUniquePtr pXml = parseLayout(); + sal_Int32 y = getXPath(pXml, "/metafile['2']/push/push/textarray[3]", "y").toInt32(); + CPPUNIT_ASSERT_DOUBLES_EQUAL(6700, y, 100); + assertXPathContent(pXml, "/metafile['2']/push/push/textarray[3]/text", u"textbox"); + } + + // 3. It must be disabled in an existing ODP without that option in settings.xml + loadFromFile(u"odp/trailing-paragraphs.odp"); + { + CPPUNIT_ASSERT( + !getDoc()->GetCompatibilityFlag(SdrCompatibilityFlag::UseTrailingEmptyLinesInLayout)); + + xmlDocUniquePtr pXml = parseLayout(); + sal_Int32 y = getXPath(pXml, "/metafile['3']/push/push/textarray[3]", "y").toInt32(); + CPPUNIT_ASSERT_DOUBLES_EQUAL(8100, y, 100); + assertXPathContent(pXml, "/metafile['3']/push/push/textarray[3]/text", u"textbox"); + } + + // 4. It must stay disabled after ODP round-trip + saveAndReload(u"impress8"_ustr); + { + CPPUNIT_ASSERT( + !getDoc()->GetCompatibilityFlag(SdrCompatibilityFlag::UseTrailingEmptyLinesInLayout)); + + xmlDocUniquePtr pXml = parseLayout(); + sal_Int32 y = getXPath(pXml, "/metafile['4']/push/push/textarray[3]", "y").toInt32(); + CPPUNIT_ASSERT_DOUBLES_EQUAL(8100, y, 100); + assertXPathContent(pXml, "/metafile['4']/push/push/textarray[3]/text", u"textbox"); + } + + // Now test PPTX and its round-trip. The text there imports as draw:custom-shape; it generates + // a single textarray element. + + // 5. It must be enabled for PPTX documents unconditionally + loadFromFile(u"pptx/trailing-paragraphs.pptx"); + { + CPPUNIT_ASSERT( + getDoc()->GetCompatibilityFlag(SdrCompatibilityFlag::UseTrailingEmptyLinesInLayout)); + + xmlDocUniquePtr pXml = parseLayout(); + sal_Int32 y = getXPath(pXml, "/metafile['5']/push/push/textarray", "y").toInt32(); + CPPUNIT_ASSERT_DOUBLES_EQUAL(6700, y, 100); + assertXPathContent(pXml, "/metafile['5']/push/push/textarray/text", u"textbox"); + } + + // 6. Check PPTX round-trip + saveAndReload(u"Impress Office Open XML"_ustr); + { + CPPUNIT_ASSERT( + getDoc()->GetCompatibilityFlag(SdrCompatibilityFlag::UseTrailingEmptyLinesInLayout)); + + xmlDocUniquePtr pXml = parseLayout(); + sal_Int32 y = getXPath(pXml, "/metafile['6']/push/push/textarray", "y").toInt32(); + CPPUNIT_ASSERT_DOUBLES_EQUAL(6700, y, 100); + assertXPathContent(pXml, "/metafile['6']/push/push/textarray/text", u"textbox"); + } + + // For some reason, saving PPTX to ODT in step 7 below produces negative fo:padding-top, which + // fails validation; that is unrelated, so disable validation for now. + skipValidation(); + + // 7. It must round-trip to ODP + saveAndReload(u"impress8"_ustr); + { + CPPUNIT_ASSERT( + getDoc()->GetCompatibilityFlag(SdrCompatibilityFlag::UseTrailingEmptyLinesInLayout)); + + xmlDocUniquePtr pXml = parseLayout(); + sal_Int32 y = getXPath(pXml, "/metafile['7']/push/push/textarray", "y").toInt32(); + CPPUNIT_ASSERT_DOUBLES_EQUAL(6700, y, 100); + assertXPathContent(pXml, "/metafile['7']/push/push/textarray/text", u"textbox"); + } +} + CPPUNIT_PLUGIN_IMPLEMENT(); /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/docshell/docshel4.cxx b/sd/source/ui/docshell/docshel4.cxx index dca885bcd2cd..223b5921cc4c 100644 --- a/sd/source/ui/docshell/docshel4.cxx +++ b/sd/source/ui/docshell/docshel4.cxx @@ -430,6 +430,10 @@ bool DrawDocShell::ImportFrom(SfxMedium &rMedium, // compatibility flag for tdf#148966 mpDoc->SetCompatibilityFlag(SdrCompatibilityFlag::IgnoreBreakAfterMultilineField, true); + + // tdf#168010: PowerPoint ignores empty trailing lines in autoshrink text when scaling font + // (same as Impress), but takes into account in layout: + mpDoc->SetCompatibilityFlag(SdrCompatibilityFlag::UseTrailingEmptyLinesInLayout, true); } if (aFilterName == "Impress MS PowerPoint 2007 XML" || diff --git a/svx/source/svdraw/svdmodel.cxx b/svx/source/svdraw/svdmodel.cxx index b5b9ebb591ad..2884afefaa4e 100644 --- a/svx/source/svdraw/svdmodel.cxx +++ b/svx/source/svdraw/svdmodel.cxx @@ -98,6 +98,7 @@ struct SdrModelImpl false, // tdf#148000 LegacyFontwork false, // tdf#149756 ConnectorUseSnapRect false, // tdf#148966 IgnoreBreakAfterMultilineField + false, // tdf#168010 UseTrailingEmptyLinesInLayout } , mpTheme(new model::Theme(u"Office"_ustr)) {} @@ -1836,6 +1837,11 @@ void SdrModel::ReadUserDataSequenceValue(const beans::PropertyValue* pValue) SetCompatibilityFlag(SdrCompatibilityFlag::IgnoreBreakAfterMultilineField, bBool); } } + else if (pValue->Name == "UseTrailingEmptyLinesInLayout") + { + if (bool bBool; pValue->Value >>= bBool) + SetCompatibilityFlag(SdrCompatibilityFlag::UseTrailingEmptyLinesInLayout, bBool); + } } void SdrModel::WriteUserDataSequence(uno::Sequence <beans::PropertyValue>& rValues) @@ -1845,7 +1851,8 @@ void SdrModel::WriteUserDataSequence(uno::Sequence <beans::PropertyValue>& rValu { "AnchoredTextOverflowLegacy", uno::Any(GetCompatibilityFlag(SdrCompatibilityFlag::AnchoredTextOverflowLegacy)) }, { "LegacySingleLineFontwork", uno::Any(GetCompatibilityFlag(SdrCompatibilityFlag::LegacyFontwork)) }, { "ConnectorUseSnapRect", uno::Any(GetCompatibilityFlag(SdrCompatibilityFlag::ConnectorUseSnapRect)) }, - { "IgnoreBreakAfterMultilineField", uno::Any(GetCompatibilityFlag(SdrCompatibilityFlag::IgnoreBreakAfterMultilineField)) } + { "IgnoreBreakAfterMultilineField", uno::Any(GetCompatibilityFlag(SdrCompatibilityFlag::IgnoreBreakAfterMultilineField)) }, + { "UseTrailingEmptyLinesInLayout", uno::Any(GetCompatibilityFlag(SdrCompatibilityFlag::UseTrailingEmptyLinesInLayout)) }, }; const sal_Int32 nOldLength = rValues.getLength(); diff --git a/svx/source/svdraw/svdotextdecomposition.cxx b/svx/source/svdraw/svdotextdecomposition.cxx index a90694947368..b86d9c20f92a 100644 --- a/svx/source/svdraw/svdotextdecomposition.cxx +++ b/svx/source/svdraw/svdotextdecomposition.cxx @@ -456,7 +456,17 @@ void SdrTextObj::impDecomposeAutoFitTextPrimitive( rOutliner.SetFixedCellHeight(rSdrAutofitTextPrimitive.isFixedCellHeight()); // now get back the layouted text size from outliner - const Size aOutlinerTextSize(rOutliner.GetPaperSize()); + Size aOutlinerTextSize(rOutliner.GetPaperSize()); + if (getSdrModelFromSdrObject().GetCompatibilityFlag( + SdrCompatibilityFlag::UseTrailingEmptyLinesInLayout)) + { + // The height of the text may be larger than the box height, because of the trailing + // empty paragraphs, ignored when scaling, and normally ignored for layout. PowerPoint + // has a different handling: it also ignores the lines when scaling, but uses them for + // positioning of the text. + if (tools::Long h = rOutliner.GetTextHeight(); h > aOutlinerTextSize.Height()) + aOutlinerTextSize.setHeight(h); + } const basegfx::B2DVector aOutlinerScale(aOutlinerTextSize.Width(), aOutlinerTextSize.Height()); basegfx::B2DVector aAdjustTranslate(0.0, 0.0);