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 425b55efaf1f04e8f91cb049a5820fcf61e678dd Author: Mike Kaganski <mike.kagan...@collabora.com> AuthorDate: Tue Aug 19 23:33:02 2025 +0500 Commit: Mike Kaganski <mike.kagan...@collabora.com> CommitDate: Tue Aug 19 23:05:20 2025 +0200 tdf#164622: 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 diff --git a/editeng/source/editeng/impedit.hxx b/editeng/source/editeng/impedit.hxx index 4fb322ddea46..96089f4299b2 100644 --- a/editeng/source/editeng/impedit.hxx +++ b/editeng/source/editeng/impedit.hxx @@ -658,7 +658,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 ); @@ -690,14 +690,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); @@ -988,6 +988,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 1d60a91d2134..fd27afb17b43 100644 --- a/editeng/source/editeng/impedit2.cxx +++ b/editeng/source/editeng/impedit2.cxx @@ -4658,12 +4658,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 a366c53d9ee4..f28e8bf9e550 100644 --- a/editeng/source/editeng/impedit3.cxx +++ b/editeng/source/editeng/impedit3.cxx @@ -246,7 +246,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++) @@ -255,7 +255,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()) { @@ -284,8 +284,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 @@ -518,7 +520,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()) @@ -527,7 +529,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, @@ -648,7 +650,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); @@ -665,7 +667,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, @@ -1961,12 +1963,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 59571a5c485b..5e00d42bab2b 100644 --- a/sd/source/ui/docshell/docshel4.cxx +++ b/sd/source/ui/docshell/docshel4.cxx @@ -431,6 +431,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 c3d3271a99e7..f0f16e55218e 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 7a6001d628e4..951f93fe0de4 100644 --- a/svx/source/svdraw/svdotextdecomposition.cxx +++ b/svx/source/svdraw/svdotextdecomposition.cxx @@ -239,7 +239,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);