sw/qa/extras/layout/data/tdf163720.fodt | 2 sw/qa/extras/layout/layout3.cxx | 35 ++--- sw/source/core/text/guess.cxx | 13 - sw/source/core/text/guess.hxx | 2 sw/source/core/text/inftxt.cxx | 34 ++++ sw/source/core/text/inftxt.hxx | 7 + sw/source/core/text/portxt.cxx | 165 ++++++++++++++---------- sw/source/writerfilter/dmapper/DomainMapper.cxx | 6 8 files changed, 172 insertions(+), 92 deletions(-)
New commits: commit 529755f0919217a84a12daad0fddfddd1124f0e9 Author: László Németh <nem...@numbertext.org> AuthorDate: Mon Jun 2 19:54:43 2025 +0200 Commit: László Németh <nem...@numbertext.org> CommitDate: Mon Jun 9 23:59:40 2025 +0200 tdf#166113 sw smart justify: adjust algorithm for interoperability Skip space shrinking, if space expansion is closer (with a weight) to the desired (100%) word space. Import DOCX documents with 75% minimum word spacing, 100% desired word spacing and 133% maximum word spacing. These changes cover MSO smart justify algorithm. Hyphenation and text portions may need future adjustments. Modify unit tests according to the changes: – testTdf126154, testTdf126154_minimum_shrinking, testTdf126154_portion: choose maximum word spacing, i.e. expansion instead of shrinking, according to the weighted distance from the desired word space; – testTdf163720: set desired word spacing to the minimum word spacing in the test document to get back the original regression test. Note: the previous algorithm for disabled hyphenation within the minimum and maximum word spacing range is replaced by optimization for the desired word spacing (temporarily). Interoperability analysis and custom more/less hyphenation can change this. Change-Id: Iec0582a497945743f97686743af5ff6609866abe Reviewed-on: https://gerrit.libreoffice.org/c/core/+/186138 Tested-by: Jenkins Reviewed-by: László Németh <nem...@numbertext.org> diff --git a/sw/qa/extras/layout/data/tdf163720.fodt b/sw/qa/extras/layout/data/tdf163720.fodt index 7f0fccef95ef..28c38cf6a0c9 100644 --- a/sw/qa/extras/layout/data/tdf163720.fodt +++ b/sw/qa/extras/layout/data/tdf163720.fodt @@ -178,7 +178,7 @@ <style:paragraph-properties fo:text-align="start" style:justify-single-word="false" text:number-lines="false" text:line-number="0" style:writing-mode="lr-tb"/> </style:style> <style:style style:name="P2" style:family="paragraph" style:parent-style-name="Standard"> - <style:paragraph-properties fo:text-align="justify" style:justify-single-word="false" fo:hyphenation-ladder-count="no-limit" fo:hyphenation-keep="auto" loext:hyphenation-keep-type="column" style:writing-mode="lr-tb"/> + <style:paragraph-properties fo:text-align="justify" style:justify-single-word="false" fo:hyphenation-ladder-count="no-limit" fo:hyphenation-keep="auto" loext:hyphenation-keep-type="column" style:writing-mode="lr-tb" loext:word-spacing-minimum="75%" loext:word-spacing="75%"/> <style:text-properties fo:hyphenate="true" fo:hyphenation-remain-char-count="2" fo:hyphenation-push-char-count="2" loext:hyphenation-no-caps="false" loext:hyphenation-no-last-word="false" loext:hyphenation-word-char-count="5" loext:hyphenation-zone="567" loext:hyphenation-compound-remain-char-count="2"/> </style:style> <style:page-layout style:name="pm1"> diff --git a/sw/qa/extras/layout/layout3.cxx b/sw/qa/extras/layout/layout3.cxx index 15034a6fb8b3..23dd0dc51221 100644 --- a/sw/qa/extras/layout/layout3.cxx +++ b/sw/qa/extras/layout/layout3.cxx @@ -1054,7 +1054,8 @@ CPPUNIT_TEST_FIXTURE(SwLayoutWriter3, testTdf126154) u",,,,,,,, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vesti "); // also minimum word space: 80%, 100%, 100% - // only a single line is hyphenated from the previous ones + // only a single line was hyphenated from the previous ones + // TODO: fix possible interoperability issues, allow optional limitation of hyphenation again assertXPath( pXmlDoc, "/root/page[1]/body/txt[10]/SwParaPortion/SwLineLayout[1]", "portion", u",, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vesti bulum "); @@ -1067,10 +1068,10 @@ CPPUNIT_TEST_FIXTURE(SwLayoutWriter3, testTdf126154) u"Vesti bulum "); assertXPath(pXmlDoc, "/root/page[1]/body/txt[13]/SwParaPortion/SwLineLayout[1]", "portion", u",,,,, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. " - u"Vesti bulum "); + u"Vesti bu"); assertXPath(pXmlDoc, "/root/page[1]/body/txt[14]/SwParaPortion/SwLineLayout[1]", "portion", u",,,,,, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. " - u"Vesti bulum "); + u"Vesti bu"); assertXPath( pXmlDoc, "/root/page[1]/body/txt[15]/SwParaPortion/SwLineLayout[1]", "portion", u",,,,,,, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vesti bu"); @@ -1079,8 +1080,9 @@ CPPUNIT_TEST_FIXTURE(SwLayoutWriter3, testTdf126154) u",,,,,,,, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vesti "); // minimum, desired and maximum word spacing: 80%, 100%, 133% - // no hyphenation in the same text: hyphenation of all the short words are limited + // no hyphenation in the same text: hyphenation of all the short words were limited // by the minimum and maximum word spacing settings + // TODO: fix possible interoperability issues, allow optional limitation of hyphenation again assertXPath( pXmlDoc, "/root/page[1]/body/txt[18]/SwParaPortion/SwLineLayout[1]", "portion", u",, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vesti bulum "); @@ -1093,10 +1095,10 @@ CPPUNIT_TEST_FIXTURE(SwLayoutWriter3, testTdf126154) u"Vesti bulum "); assertXPath(pXmlDoc, "/root/page[1]/body/txt[21]/SwParaPortion/SwLineLayout[1]", "portion", u",,,,, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. " - u"Vesti bulum "); + u"Vesti bu"); assertXPath(pXmlDoc, "/root/page[1]/body/txt[22]/SwParaPortion/SwLineLayout[1]", "portion", u",,,,,, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. " - u"Vesti bulum "); + u"Vesti bu"); assertXPath( pXmlDoc, "/root/page[1]/body/txt[23]/SwParaPortion/SwLineLayout[1]", "portion", u",,,,,,, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vesti "); @@ -1167,7 +1169,8 @@ CPPUNIT_TEST_FIXTURE(SwLayoutWriter3, testTdf126154_minimum_shrinking) u",,,,,,,, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vesti "); // also minimum word space: 80%, 100%, 100% - // only a single line is hyphenated from the previous ones + // only a single line was yphenated from the previous ones + // TODO: fix possible interoperability issues, allow optional limitation of hyphenation again assertXPath( pXmlDoc, "/root/page[1]/body/txt[10]/SwParaPortion/SwLineLayout[1]", "portion", u",, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vesti bulum "); @@ -1179,10 +1182,10 @@ CPPUNIT_TEST_FIXTURE(SwLayoutWriter3, testTdf126154_minimum_shrinking) u"Vesti bulum "); assertXPath(pXmlDoc, "/root/page[1]/body/txt[13]/SwParaPortion/SwLineLayout[1]", "portion", u",,,,, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. " - u"Vesti bulum "); + u"Vesti bu"); assertXPath(pXmlDoc, "/root/page[1]/body/txt[14]/SwParaPortion/SwLineLayout[1]", "portion", u",,,,,, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. " - u"Vesti bulum "); + u"Vesti bu"); assertXPath( pXmlDoc, "/root/page[1]/body/txt[15]/SwParaPortion/SwLineLayout[1]", "portion", u",,,,,,, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vesti bu"); @@ -1191,8 +1194,9 @@ CPPUNIT_TEST_FIXTURE(SwLayoutWriter3, testTdf126154_minimum_shrinking) u",,,,,,,, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vesti "); // minimum, desired and maximum word spacing: 80%, 100%, 133% - // no hyphenation in the same text: hyphenation of all the short words are limited + // no hyphenation in the same text: hyphenation of all the short words were limited // by the minimum and maximum word spacing settings + // TODO: fix possible interoperability issues, allow optional limitation of hyphenation again assertXPath( pXmlDoc, "/root/page[1]/body/txt[18]/SwParaPortion/SwLineLayout[1]", "portion", u",, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vesti bulum "); @@ -1204,10 +1208,10 @@ CPPUNIT_TEST_FIXTURE(SwLayoutWriter3, testTdf126154_minimum_shrinking) u"Vesti bulum "); assertXPath(pXmlDoc, "/root/page[1]/body/txt[21]/SwParaPortion/SwLineLayout[1]", "portion", u",,,,, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. " - u"Vesti bulum "); + u"Vesti bu"); assertXPath(pXmlDoc, "/root/page[1]/body/txt[22]/SwParaPortion/SwLineLayout[1]", "portion", u",,,,,, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. " - u"Vesti bulum "); + u"Vesti bu"); assertXPath( pXmlDoc, "/root/page[1]/body/txt[23]/SwParaPortion/SwLineLayout[1]", "portion", u",,,,,,, , , , , , , , Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vesti "); @@ -1237,11 +1241,12 @@ CPPUNIT_TEST_FIXTURE(SwLayoutWriter3, testTdf126154_portion) u",,,,,,,,,,,,,,,,,,,,, , , , , , , , , , , , , , , , Lorem ipsum dolor sit amet, " u"consectetur adipiscing elit. Vesti "); - // prefer minimum word spacing, not maximum word spacing to disable hyphenation, if it's possible - // between the minimum and maximum values. (minimum: 80%, desired: 100%, maximum word spacing: 133%) + // minimum: 80%, desired: 100%, maximum word spacing: 133% + // prefer maximum word spacing, not minimum word spacing, if the weighted word space + // is nearer to the desired word space assertXPath(pXmlDoc, "/root/page[1]/body/txt[4]/SwParaPortion/SwLineLayout[1]", "portion", u",,,,,,,,,,,,,,,,,,,,, , , , , , , , , , , , , , , , Lorem ipsum dolor sit amet, " - u"consectetur adipiscing elit. Vesti bulum "); + u"consectetur adipiscing elit. Vesti "); // paragraph end zone // This was "... other celes" (not disabled hyphenation because of text portion) diff --git a/sw/source/core/text/guess.cxx b/sw/source/core/text/guess.cxx index d18336432118..694a74361553 100644 --- a/sw/source/core/text/guess.cxx +++ b/sw/source/core/text/guess.cxx @@ -158,7 +158,7 @@ bool SwTextGuess::maybeAdjustPositionsForBlockAdjust(tools::Long& rMaxSizeDiff, // otherwise possible break or hyphenation position is determined bool SwTextGuess::Guess( const SwTextPortion& rPor, SwTextFormatInfo &rInf, const sal_uInt16 nPorHeight, sal_Int32 nSpacesInLine, - sal_uInt16 nPropWordSpacing ) + sal_uInt16 nPropWordSpacing, sal_Int16 nSpaceWidth ) { m_nCutPos = rInf.GetIdx(); @@ -191,19 +191,13 @@ bool SwTextGuess::Guess( const SwTextPortion& rPor, SwTextFormatInfo &rInf, if ( nSpacesInLine ) { - static constexpr OUStringLiteral STR_BLANK = u" "; - sal_Int16 nSpaceWidth = rInf.GetTextSize(STR_BLANK).Width(); - float fWordSpacing = nPropWordSpacing == SAL_MAX_UINT16 - ? 0.75 // MSO interoperability value - // allow up to 25% shrinking of the spaces - : nPropWordSpacing / 100.0; - SwTwips nExtraSpace = nSpacesInLine * nSpaceWidth * (1.0 - fWordSpacing); + SwTwips nExtraSpace = nSpacesInLine * nSpaceWidth/10.0 * (1.0 - nPropWordSpacing / 100.0); nLineWidth += nExtraSpace; // convert maximum word spacing to hyphenation zone, if defined if ( nPropWordSpacing == aAdjustItem.GetPropWordSpacing() ) { SwTwips nMaxDif = aAdjustItem.GetPropWordSpacingMaximum() - nPropWordSpacing; - nWordSpacingMaximumZone = nSpacesInLine * nSpaceWidth * nMaxDif / 100.0; + nWordSpacingMaximumZone = nSpacesInLine * nSpaceWidth/10.0 * nMaxDif / 100.0; } rInf.SetExtraSpace(nExtraSpace); @@ -845,6 +839,7 @@ bool SwTextGuess::Guess( const SwTextPortion& rPor, SwTextFormatInfo &rInf, rInf.GetTextSize(&rSI, rInf.GetIdx(), nPorLen, std::nullopt, nMaxComp, m_nBreakWidth, nMaxSizeDiff, nExtraAscent, nExtraDescent, rInf.GetCachedVclData().get()); + rInf.SetBreakWidth(m_nBreakWidth); // save maximum width for later use if ( nMaxSizeDiff ) rInf.SetMaxWidthDiff( &rPor, nMaxSizeDiff ); diff --git a/sw/source/core/text/guess.hxx b/sw/source/core/text/guess.hxx index 54f38c7f1e90..5c1d0d387b16 100644 --- a/sw/source/core/text/guess.hxx +++ b/sw/source/core/text/guess.hxx @@ -45,7 +45,7 @@ public: // true, if current portion still fits to current line bool Guess( const SwTextPortion& rPor, SwTextFormatInfo &rInf, const sal_uInt16 nHeight, sal_Int32 nSpacesInLine = 0, - sal_uInt16 nPropWordSpacing = 100 ); + sal_uInt16 nPropWordSpacing = 100, sal_Int16 nSpaceWidth = 0 ); bool AlternativeSpelling( const SwTextFormatInfo &rInf, const TextFrameIndex nPos ); SwHangingPortion* GetHangingPortion() const { return m_pHanging.get(); } diff --git a/sw/source/core/text/inftxt.cxx b/sw/source/core/text/inftxt.cxx index 692be2c2da41..3cb5a8226d7a 100644 --- a/sw/source/core/text/inftxt.cxx +++ b/sw/source/core/text/inftxt.cxx @@ -225,6 +225,7 @@ SwTextSizeInfo::SwTextSizeInfo() , m_bSnapToGrid(false) , m_nDirection(0) , m_nExtraSpace(0) +, m_nBreakWidth(0) {} SwTextSizeInfo::SwTextSizeInfo( const SwTextSizeInfo &rNew ) @@ -256,7 +257,8 @@ SwTextSizeInfo::SwTextSizeInfo( const SwTextSizeInfo &rNew ) m_bForbiddenChars( rNew.HasForbiddenChars() ), m_bSnapToGrid( rNew.SnapToGrid() ), m_nDirection( rNew.GetDirection() ), - m_nExtraSpace( rNew.GetExtraSpace() ) + m_nExtraSpace( rNew.GetExtraSpace() ), + m_nBreakWidth( rNew.GetBreakWidth() ) { #if OSL_DEBUG_LEVEL > 0 ChkOutDev( *this ); @@ -269,6 +271,7 @@ void SwTextSizeInfo::CtorInitTextSizeInfo( OutputDevice* pRenderContext, SwTextF m_pKanaComp = nullptr; m_nKanaIdx = 0; m_nExtraSpace = 0; + m_nBreakWidth = 0; m_pFrame = pFrame; CtorInitTextInfo( m_pFrame ); SwDoc const& rDoc(m_pFrame->GetDoc()); @@ -370,7 +373,8 @@ SwTextSizeInfo::SwTextSizeInfo( const SwTextSizeInfo &rNew, const OUString* pTex m_bForbiddenChars( rNew.HasForbiddenChars() ), m_bSnapToGrid( rNew.SnapToGrid() ), m_nDirection( rNew.GetDirection() ), - m_nExtraSpace( rNew.GetExtraSpace() ) + m_nExtraSpace( rNew.GetExtraSpace() ), + m_nBreakWidth( rNew.GetBreakWidth() ) { #if OSL_DEBUG_LEVEL > 0 ChkOutDev( *this ); @@ -2283,4 +2287,30 @@ bool SwTextFormatInfo::CheckCurrentPosBookmark() } } +sal_Int32 SwTextFormatInfo::GetLineSpaceCount(TextFrameIndex nBreakPos) +{ + if ( sal_Int32(nBreakPos) >= GetText().getLength() ) + return 0; + + sal_Int32 nSpaces = 0; + sal_Int32 nInlineSpaces = -1; + for (sal_Int32 i = sal_Int32(GetLineStart()); i < sal_Int32(nBreakPos); ++i) + { + sal_Unicode cChar = GetText()[i]; + if ( cChar == CH_BLANK ) + ++nSpaces; + else + { + if ( nInlineSpaces == -1 ) + { + nInlineSpaces = 0; + nSpaces = 0; + } + else + nInlineSpaces = nSpaces; + } + } + return nInlineSpaces == -1 ? 0: nInlineSpaces; +} + /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/inftxt.hxx b/sw/source/core/text/inftxt.hxx index 6440c76340f7..4792ce8399d1 100644 --- a/sw/source/core/text/inftxt.hxx +++ b/sw/source/core/text/inftxt.hxx @@ -174,6 +174,7 @@ protected: bool m_bSnapToGrid : 1; // paragraph snaps to grid sal_uInt8 m_nDirection : 2; // writing direction: 0/90/180/270 degree SwTwips m_nExtraSpace; // extra space before shrinking = nSpacesInLine * (nSpaceWidth/0.8 - nSpaceWidth) + SwTwips m_nBreakWidth; // break width to calculate space width at justification protected: void CtorInitTextSizeInfo( OutputDevice* pRenderContext, SwTextFrame *pFrame, @@ -302,6 +303,9 @@ public: // extra space before shrinking = nSpacesInLine * (nSpaceWidth/0.8 - nSpaceWidth) void SetExtraSpace(SwTwips nVal) { m_nExtraSpace = nVal; } SwTwips GetExtraSpace() const { return m_nExtraSpace; } + // set break width to calculate space width later + void SetBreakWidth(SwTwips nVal) { m_nBreakWidth = nVal; } + SwTwips GetBreakWidth() const { return m_nBreakWidth; } // If Kana Compression is enabled, a minimum and maximum portion width // is calculated. We format lines with minimal size and share remaining @@ -711,6 +715,9 @@ public: void SetTabOverflow( bool bOverflow ) { m_bTabOverflow = bOverflow; } bool IsTabOverflow() const { return m_bTabOverflow; } + // get line space count between line start and break position + // by stripping also terminating spaces + sal_Int32 GetLineSpaceCount(TextFrameIndex nBreakPos); }; /** diff --git a/sw/source/core/text/portxt.cxx b/sw/source/core/text/portxt.cxx index df577062e712..36ad3e50f5a3 100644 --- a/sw/source/core/text/portxt.cxx +++ b/sw/source/core/text/portxt.cxx @@ -367,19 +367,19 @@ bool SwTextPortion::Format_( SwTextFormatInfo &rInf ) const SvxAdjust aAdjust = aAdjustItem.GetAdjust(); bool bFullJustified = bFull && aAdjust == SvxAdjust::Block && pGuess->BreakPos() != TextFrameIndex(COMPLETE_STRING); - bool bInteropSmartJustify = bFullJustified && + bool bInteropSmartJustify = rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess().get( - DocumentSettingId::JUSTIFY_LINES_WITH_SHRINKING) && - // support different Kashida etc. values - aAdjustItem.GetPropWordSpacing() == 100 && + DocumentSettingId::JUSTIFY_LINES_WITH_SHRINKING); + bool bNoWordSpacing = aAdjustItem.GetPropWordSpacing() == 100 && aAdjustItem.GetPropWordSpacingMinimum() == 100 && aAdjustItem.GetPropWordSpacingMaximum() == 100; - bool bWordSpacing = bFullJustified && !bInteropSmartJustify && - aAdjustItem.GetPropWordSpacing() != 100; - bool bWordSpacingMaximum = bFullJustified && !bInteropSmartJustify && + // support old ODT documents, where only JustifyLinesWithShrinking was set + bool bOldInterop = bInteropSmartJustify && bNoWordSpacing; + bool bWordSpacing = bFullJustified && (!bNoWordSpacing || bOldInterop); + bool bWordSpacingMaximum = bWordSpacing && !bOldInterop && aAdjustItem.GetPropWordSpacingMaximum() > aAdjustItem.GetPropWordSpacing(); - bool bWordSpacingMinimum = bFullJustified && !bInteropSmartJustify && - aAdjustItem.GetPropWordSpacingMinimum() < aAdjustItem.GetPropWordSpacing(); + bool bWordSpacingMinimum = bWordSpacing && ( bOldInterop || + aAdjustItem.GetPropWordSpacingMinimum() < aAdjustItem.GetPropWordSpacing() ); if ( ( bInteropSmartJustify || bWordSpacing || bWordSpacingMaximum || bWordSpacingMinimum ) && // tdf#164499 no shrinking in tabulated line @@ -388,13 +388,9 @@ bool SwTextPortion::Format_( SwTextFormatInfo &rInf ) // very short word resulted endless loop !rInf.IsUnderflow() ) { - sal_Int32 nSpacesInLine(0); - for (sal_Int32 i = sal_Int32(rInf.GetLineStart()); i < sal_Int32(pGuess->BreakPos()); ++i) - { - sal_Unicode cChar = rInf.GetText()[i]; - if ( cChar == CH_BLANK ) - ++nSpacesInLine; - } + sal_Int32 nSpacesInLine = rInf.GetLineSpaceCount( pGuess->BreakPos() ); + sal_Int32 nSpacesInLineOrig = nSpacesInLine; + SwTextSizeInfo aOrigInf( rInf ); // call with an extra space: shrinking can result a new word in the line // and a new space before that, which is also a shrank space @@ -410,70 +406,111 @@ bool SwTextPortion::Format_( SwTextFormatInfo &rInf ) ++nSpacesInLine; } - if ( nSpacesInLine > 0 ) + // there are spaces in the line, so it's possible to shrink them + if ( nSpacesInLineOrig > 0 ) { SwTwips nOldWidth = pGuess->BreakWidth(); - if ( bInteropSmartJustify ) + bool bIsPortion = rInf.GetLineWidth() < rInf.GetBreakWidth(); + + // measure ten spaces for higher precision + static constexpr OUStringLiteral STR_BLANK = u" "; + sal_Int16 nSpaceWidth = rInf.GetTextSize(STR_BLANK).Width(); + sal_Int32 nRealSpaces = rInf.GetLineSpaceCount( pGuess->BreakPos() ); + float fSpaceNormal = (rInf.GetLineWidth() - (rInf.GetBreakWidth() - nRealSpaces * nSpaceWidth/10.0))/nRealSpaces; + + bool bOrigHyphenated = pGuess->HyphWord().is() && + pGuess->BreakPos() > rInf.GetLineStart(); + // calculate line breaking with desired word spacing, also + // if the desired word spacing is 100%, but there is a greater + // maximum word spacing, and the word is hyphenated at the desired + // word spacing: to skip hyphenation, if the maximum word spacing allows it + if ( bWordSpacing || ( bWordSpacingMaximum && bOrigHyphenated ) ) { pGuess.emplace(); - bFull = !pGuess->Guess( *this, rInf, Height(), nSpacesInLine, SAL_MAX_UINT16 ); + bFull = !pGuess->Guess( *this, rInf, Height(), nSpacesInLine, aAdjustItem.GetPropWordSpacing(), nSpaceWidth ); + sal_Int32 nSpacesInLine2 = rInf.GetLineSpaceCount( pGuess->BreakPos() ); + + if ( rInf.GetBreakWidth() <= rInf.GetLineWidth() ) + fSpaceNormal = (rInf.GetLineWidth() - (rInf.GetBreakWidth() - nSpacesInLine2 * nSpaceWidth/10.0))/nSpacesInLine2; } - else + + sal_Int32 nSpacesInLineShrink = 0; + // TODO if both maximum word spacing or minimum word spacing can disable hyphenation, prefer the last one + if ( bWordSpacingMinimum ) { - bool bOrigHyphenated = pGuess->HyphWord().is() && - pGuess->BreakPos() > rInf.GetLineStart(); - if ( bWordSpacing || bWordSpacingMaximum ) - { - pGuess.emplace(); - bFull = !pGuess->Guess( *this, rInf, Height(), nSpacesInLine, aAdjustItem.GetPropWordSpacing() ); - } - // if both maximum word spacing or minimum word spacing can disable - // hyphenation, prefer the last one - if ( bWordSpacingMinimum && ( bWordSpacingMaximum || - ( pGuess->HyphWord().is() && pGuess->BreakPos() > rInf.GetLineStart() ) ) && - // if the desired word spacing is 100% (!bWordSpacing), and it was possible - // to break the line without hyphenation in the first run (where maximum - // word spacing was not used), no need to check minimum word spacing - // FIXME: avoid too much shrinking, if desired word spacing is not 100% - ( bWordSpacing || bOrigHyphenated ) - ) + std::optional<SwTextGuess> pGuess2(std::in_place); + SwTwips nOldExtraSpace = rInf.GetExtraSpace(); + // break the line after the hyphenated word, if it's possible + // (hyphenation is disabled in Guess(), when called with GetPropWordSpacingMinimum()) + sal_uInt16 nMinimum = bOldInterop ? 75 : aAdjustItem.GetPropWordSpacingMinimum(); + bool bFull2 = !pGuess2->Guess( *this, rInf, Height(), nSpacesInLine, nMinimum, nSpaceWidth ); + nSpacesInLineShrink = rInf.GetLineSpaceCount( pGuess2->BreakPos() ); + if ( pGuess2->BreakWidth() > nOldWidth ) { - std::optional<SwTextGuess> pGuess2(std::in_place); - SwTwips nOldExtraSpace = rInf.GetExtraSpace(); - // break the line after the hyphenated word, if it's possible - // (hyphenation is disabled in Guess(), when called with GetPropWordSpacingMinimum()) - bool bFull2 = !pGuess2->Guess( *this, rInf, Height(), nSpacesInLine, aAdjustItem.GetPropWordSpacingMinimum() ); - if ( pGuess2->BreakWidth() > nOldWidth ) + // instead of the maximum shrinking, break after the word which was hyphenated before + sal_Int32 i = sal_Int32(pGuess->BreakPos()); + sal_Int32 j = sal_Int32(pGuess2->BreakPos()); + // skip terminal spaces + for (; i < j && rInf.GetText()[i] == CH_BLANK; ++i); + for (; j > i && rInf.GetText()[i] == CH_BLANK; --j); + sal_Int32 nOldBreakTrim = i; + sal_Int32 nOldBreak = j - i; + for (; i < j; ++i) { - // instead of the maximum shrinking, break after the word which was hyphenated before - for (sal_Int32 i = sal_Int32(pGuess->BreakPos()); i < sal_Int32(pGuess2->BreakPos()); ++i) + sal_Unicode cChar = rInf.GetText()[i]; + // first space after the hyphenated word, and it's not the chosen one + if ( cChar == CH_BLANK ) { - sal_Unicode cChar = rInf.GetText()[i]; - // first space after the hyphenated word, and it's not the chosen one - if ( cChar == CH_BLANK ) + // using a weighted word spacing, try to break the line after the hyphenated word + sal_Int32 nNewBreak = i - nOldBreakTrim; + SwTwips nWeightedSpacing = nMinimum * (1.0 * nNewBreak/nOldBreak) + + aAdjustItem.GetPropWordSpacing() * (1.0 * (nOldBreak - nNewBreak)/nOldBreak); + std::optional<SwTextGuess> pGuess3(std::in_place); + pGuess3->Guess( *this, rInf, Height(), nSpacesInLineShrink-1, nWeightedSpacing, nSpaceWidth ); + + sal_Int32 nSpacesInLineShrink2 = rInf.GetLineSpaceCount( pGuess3->BreakPos() ); + if ( nSpacesInLineShrink2 == nSpacesInLineShrink ) { - // using a weighted word spacing, try to break the line after the hyphenated word - sal_Int32 nOldBreak = sal_Int32(pGuess2->BreakPos()) - sal_Int32(pGuess->BreakPos()); - sal_Int32 nNewBreak = i - sal_Int32(pGuess->BreakPos()); - SwTwips nWeightedSpacing = aAdjustItem.GetPropWordSpacingMinimum() * (1.0 * nNewBreak/nOldBreak) + - aAdjustItem.GetPropWordSpacing() * (1.0 * (nOldBreak - nNewBreak)/nOldBreak); - std::optional<SwTextGuess> pGuess3(std::in_place); - pGuess3->Guess( *this, rInf, Height(), nSpacesInLine, nWeightedSpacing ); - if ( pGuess3->BreakWidth() > nOldWidth ) - { - pGuess2.emplace(); - pGuess2 = std::move(pGuess3); - } - break; + nNewBreak = i - nOldBreakTrim - 1; + nWeightedSpacing = nMinimum * (1.0 * nNewBreak/nOldBreak) + + aAdjustItem.GetPropWordSpacing() * (1.0 * (nOldBreak - nNewBreak)/nOldBreak); + pGuess3->Guess( *this, rInf, Height(), nSpacesInLineShrink-1, nWeightedSpacing, nSpaceWidth ); } + + if ( pGuess3->BreakWidth() > nOldWidth ) + { + pGuess2.emplace(); + pGuess2 = std::move(pGuess3); + } + break; + } + } + + nSpacesInLineShrink = rInf.GetLineSpaceCount( pGuess2->BreakPos() ); + if ( rInf.GetBreakWidth() > rInf.GetLineWidth() || bIsPortion ) + { + float fExpansionWeight = static_cast<float>(1/1.7); + float fSpaceShrunk = nSpacesInLineShrink > 0 + ? (rInf.GetLineWidth() - (rInf.GetBreakWidth() - nSpacesInLineShrink * nSpaceWidth/10.0))/nSpacesInLineShrink + : 0; + float z0 = (nSpaceWidth/10.0)/fSpaceShrunk; + float z1 = (nSpaceWidth/10.0+((fSpaceNormal-nSpaceWidth/10.0)*fExpansionWeight))/(nSpaceWidth/10.0); + // TODO shrink line portions only if needed + if ( z1 >= z0 || bIsPortion ) + { + pGuess = std::move(pGuess2); + bFull = bFull2; } + } + else if ( bOldInterop ) + { pGuess = std::move(pGuess2); bFull = bFull2; } - else - // minimum word spacing is not applicable - rInf.SetExtraSpace(nOldExtraSpace); } + else + // minimum word spacing is not applicable + rInf.SetExtraSpace(nOldExtraSpace); } if ( pGuess->BreakWidth() != nOldWidth ) diff --git a/sw/source/writerfilter/dmapper/DomainMapper.cxx b/sw/source/writerfilter/dmapper/DomainMapper.cxx index 8ee662a8b3d7..51239d40bed8 100644 --- a/sw/source/writerfilter/dmapper/DomainMapper.cxx +++ b/sw/source/writerfilter/dmapper/DomainMapper.cxx @@ -4887,6 +4887,12 @@ void DomainMapper::handleParaJustification(const sal_Int32 nIntValue, const ::to case NS_ooxml::LN_Value_ST_Jc_both: nAdjust = style::ParagraphAdjust_BLOCK; aStringValue = "both"; + // set default smart justify + if ( GetSettingsTable()->GetWordCompatibilityMode() >= 15 ) + { + rContext->Insert( PROP_PARA_WORD_SPACING_MINIMUM, uno::Any( sal_uInt16(75) ) ); + rContext->Insert( PROP_PARA_WORD_SPACING_MAXIMUM, uno::Any( sal_uInt16(133) ) ); + } break; case NS_ooxml::LN_Value_ST_Jc_lowKashida: nAdjust = style::ParagraphAdjust_BLOCK;