From 44c92622d255015baee2feebc08473d80eb2a2ae Mon Sep 17 00:00:00 2001 From: Mike Kaganski Date: Mon, 10 Aug 2026 18:11:03 +0500 Subject: [PATCH] cool#16071 sw: do not lose footnote lines cut away to fit RearrangeFootnotes may cause SetRstHeight to set the rest height to 0 for footnote lines no more fitting the footnote area; but 0 was used as a special value in IsInside, meaning "no rest height was set", which then incorrectly measured the upper instead - still as tall as the lines the frame had before the truncation. Then the frame was considered as having enough space, marked valid, and the lines cut away from it got no follow to move to, so they never appeared anywhere. Give the rest height its own "not given" state instead of spelling that as a number, and honour a height of zero for what it says. A magic negative would not do: SwContentFrame::WouldFit_ passes a negative height when the frames before it overfill the new upper, and that has to keep meaning that no line fits. The footnote computation in the constructor still falls back to the upper when it ends up at or below zero, so only the SetRstHeight path changes behaviour. Signed-off-by: Mike Kaganski Change-Id: If692577f3ee692ad5c0dbe87f9a11a2670f8cf16 Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/8906 Tested-by: Jenkins CPCI Reviewed-by: Miklos Vajna (cherry picked from commit 77634c741bb538853a757c2a41e4c9b919f22815) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/209271 Reviewed-by: Xisco Fauli Tested-by: Jenkins --- ...ol16071_footnoteTailKeptWhenTruncated.fodt | 60 +++++++++++++++++++ sw/qa/extras/layout/layout6.cxx | 33 ++++++++++ sw/source/core/text/frmform.cxx | 2 +- sw/source/core/text/widorp.cxx | 38 ++++++------ sw/source/core/text/widorp.hxx | 12 ++-- 5 files changed, 123 insertions(+), 22 deletions(-) create mode 100644 sw/qa/extras/layout/data/Cool16071_footnoteTailKeptWhenTruncated.fodt diff --git a/sw/qa/extras/layout/data/Cool16071_footnoteTailKeptWhenTruncated.fodt b/sw/qa/extras/layout/data/Cool16071_footnoteTailKeptWhenTruncated.fodt new file mode 100644 index 000000000000..a5ab90290900 --- /dev/null +++ b/sw/qa/extras/layout/data/Cool16071_footnoteTailKeptWhenTruncated.fodt @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Row 11alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima mike november oscar papa quebec romeo sierra tango uniform victor whiskey xray yankee zulu alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima mike november oscar papa quebec romeo sierra tango uniform victor whiskey xray yankee zulu alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima mike november oscar papa quebec romeo sierra tango uniform victor whiskey xray yankee zulu alpha bravo charlie delta echo foxtrot golf hotel india juliet filler + + + Row 2 + + + Row 3 + + + Row 42alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima mike november oscar papa quebec romeo sierra TAILMARK + + + + + diff --git a/sw/qa/extras/layout/layout6.cxx b/sw/qa/extras/layout/layout6.cxx index 5c37ac4240ab..0402916b58bf 100644 --- a/sw/qa/extras/layout/layout6.cxx +++ b/sw/qa/extras/layout/layout6.cxx @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -1610,6 +1611,38 @@ CPPUNIT_TEST_FIXTURE(SwLayoutWriter6, testCool16067_footnoteSpaceFreedBySplit) assertXPath(pXmlDoc, "//page[4]/body/tab/row", 1); } +CPPUNIT_TEST_FIXTURE(SwLayoutWriter6, testCool16071_footnoteTailKeptWhenTruncated) +{ + // The first row's footnote fills the footnote area of the page it ends up on, and the last + // row travels between pages carrying a footnote of three lines. That leaves the lines after + // the first formatted against a footnote area with no room for them. + createSwDoc("Cool16071_footnoteTailKeptWhenTruncated.fodt"); + // A layout dump would format what it reads, and that alone repairs the frame: measure the + // export instead. + save(TestFilter::PDF_WRITER); + std::unique_ptr pPdf = parsePDFExport(); + if (!pPdf) + return; + + // the last word of the last footnote, on its third line + bool bTailExported = false; + for (int nPage = 0; nPage < pPdf->getPageCount() && !bTailExported; ++nPage) + { + std::unique_ptr pPage = pPdf->openPage(nPage); + std::unique_ptr pTextPage = pPage->getTextPage(); + for (int i = 0; i < pPage->getObjectCount() && !bTailExported; ++i) + { + std::unique_ptr pObject = pPage->getObject(i); + if (pObject->getType() == vcl::pdf::PDFPageObjectType::Text) + bTailExported = pObject->getText(pTextPage).indexOf("TAILMARK") >= 0; + } + } + + // Without the fix the footnote kept its first line only: the two lines cut away from it had + // nowhere to go, so their text was missing from the document altogether. + CPPUNIT_ASSERT(bTailExported); +} + CPPUNIT_TEST_FIXTURE(SwLayoutWriter6, testHiddenParagraphFollowFrame) { createSwDoc("hidden-para-follow-frame.fodt"); diff --git a/sw/source/core/text/frmform.cxx b/sw/source/core/text/frmform.cxx index a37e808f8668..f7a1ce8da2b9 100644 --- a/sw/source/core/text/frmform.cxx +++ b/sw/source/core/text/frmform.cxx @@ -1849,7 +1849,7 @@ void SwTextFrame::Format_( SwTextFormatter &rLine, SwTextFormatInfo &rInf, rRepaint.Top( rLine.Y() ); if( 0 >= rRepaint.Width() ) rRepaint.Width(1); - WidowsAndOrphans aFrameBreak( this, rInf.IsTest() ? 1 : 0 ); + WidowsAndOrphans aFrameBreak(this, rInf.IsTest() ? std::optional(0) : std::nullopt); // rLine is now set to the first line which needs formatting. // The bFirst flag makes sure that Next() is not called. diff --git a/sw/source/core/text/widorp.cxx b/sw/source/core/text/widorp.cxx index f8fc286d333d..f7260cde37a7 100644 --- a/sw/source/core/text/widorp.cxx +++ b/sw/source/core/text/widorp.cxx @@ -59,8 +59,9 @@ bool IsNastyFollow( const SwTextFrame *pFrame ) } -SwTextFrameBreak::SwTextFrameBreak( SwTextFrame *pNewFrame, const SwTwips nRst ) - : m_nRstHeight(nRst), m_pFrame(pNewFrame) +SwTextFrameBreak::SwTextFrameBreak(SwTextFrame* pNewFrame, const std::optional& oRst) + : m_oRstHeight(oRst) + , m_pFrame(pNewFrame) { SwSwapIfSwapped swap(m_pFrame); SwRectFnSet aRectFnSet(*m_pFrame); @@ -84,13 +85,13 @@ SwTextFrameBreak::SwTextFrameBreak( SwTextFrame *pNewFrame, const SwTwips nRst ) m_bBreak = false; - if( !m_nRstHeight && !m_pFrame->IsFollow() && m_pFrame->IsInFootnote() && m_pFrame->HasPara() ) + if (!m_oRstHeight && !m_pFrame->IsFollow() && m_pFrame->IsInFootnote() && m_pFrame->HasPara()) { - m_nRstHeight = m_pFrame->GetFootnoteFrameHeight(); - m_nRstHeight += aRectFnSet.GetHeight(m_pFrame->getFramePrintArea()) - - aRectFnSet.GetHeight(m_pFrame->getFrameArea()); - if( m_nRstHeight < 0 ) - m_nRstHeight = 0; + m_oRstHeight = m_pFrame->GetFootnoteFrameHeight() + + aRectFnSet.GetHeight(m_pFrame->getFramePrintArea()) + - aRectFnSet.GetHeight(m_pFrame->getFrameArea()); + if (*m_oRstHeight <= 0) + m_oRstHeight.reset(); } } @@ -144,8 +145,8 @@ bool SwTextFrameBreak::IsInside(SwTextMargin const& rLine, SwResizeLimitReason& && rLine.GetEnd() >= TextFrameIndex(m_pFrame->GetText().getLength())) nLineHeight += m_pFrame->CalcAddLowerSpaceAsLastInTableCell(); - if( m_nRstHeight ) - bFit = m_nRstHeight >= nLineHeight; + if (m_oRstHeight) + bFit = *m_oRstHeight >= nLineHeight; else { // The Frame has a height to fit on the page. @@ -297,22 +298,25 @@ void SwTextFrameBreak::SetRstHeight( const SwTextMargin &rLine ) // Consider bottom margin SwRectFnSet aRectFnSet(*m_pFrame); - m_nRstHeight = aRectFnSet.GetBottomMargin(*m_pFrame); + m_oRstHeight = aRectFnSet.GetBottomMargin(*m_pFrame); if ( aRectFnSet.IsVert() ) { if ( m_pFrame->IsVertLR() ) - m_nRstHeight = aRectFnSet.YDiff( m_pFrame->SwitchHorizontalToVertical( rLine.Y() ) , m_nOrigin ); + m_oRstHeight + = aRectFnSet.YDiff(m_pFrame->SwitchHorizontalToVertical(rLine.Y()), m_nOrigin); else - m_nRstHeight += m_nOrigin - m_pFrame->SwitchHorizontalToVertical( rLine.Y() ); + *m_oRstHeight += m_nOrigin - m_pFrame->SwitchHorizontalToVertical(rLine.Y()); } else - m_nRstHeight += rLine.Y() - m_nOrigin; + *m_oRstHeight += rLine.Y() - m_nOrigin; } -WidowsAndOrphans::WidowsAndOrphans( SwTextFrame *pNewFrame, const SwTwips nRst, - bool bChkKeep ) - : SwTextFrameBreak( pNewFrame, nRst ), m_nWidLines( 0 ), m_nOrphLines( 0 ) +WidowsAndOrphans::WidowsAndOrphans(SwTextFrame* pNewFrame, const std::optional& oRst, + bool bChkKeep) + : SwTextFrameBreak(pNewFrame, oRst) + , m_nWidLines(0) + , m_nOrphLines(0) { SwSwapIfSwapped swap(m_pFrame); diff --git a/sw/source/core/text/widorp.hxx b/sw/source/core/text/widorp.hxx index bf3051df2298..2b5666e90a00 100644 --- a/sw/source/core/text/widorp.hxx +++ b/sw/source/core/text/widorp.hxx @@ -18,6 +18,8 @@ */ #pragma once +#include + #include #include "itrtxt.hxx" @@ -26,14 +28,16 @@ class SwTextFrame; class SwTextFrameBreak { private: - SwTwips m_nRstHeight; + // The height the frame has to fit into. Zero and negative values are considered valid. If not + // set, the whole upper is measured instead. + std::optional m_oRstHeight; SwTwips m_nOrigin; protected: SwTextFrame *m_pFrame; bool m_bBreak; bool m_bKeep; public: - SwTextFrameBreak( SwTextFrame *pFrame, const SwTwips nRst = 0 ); + SwTextFrameBreak(SwTextFrame* pFrame, const std::optional& oRst = std::nullopt); bool IsBreakNow( SwTextMargin &rLine ); bool IsKeepAlways() const { return m_bKeep; } @@ -56,8 +60,8 @@ private: sal_uInt16 m_nWidLines, m_nOrphLines; public: - WidowsAndOrphans( SwTextFrame *pFrame, const SwTwips nRst = 0, - bool bCheckKeep = true ); + WidowsAndOrphans(SwTextFrame* pFrame, const std::optional& oRst = std::nullopt, + bool bCheckKeep = true); bool FindWidows( SwTextFrame *pFrame, SwTextMargin &rLine ); sal_uInt16 GetOrphansLines() const { return m_nOrphLines; }