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;

Reply via email to