cui/source/inc/paragrph.hxx                       |    4 -
 cui/source/tabpages/paragrph.cxx                  |   45 +--------------
 cui/uiconfig/ui/paragalignpage.ui                 |   37 ++----------
 offapi/com/sun/star/style/ParagraphProperties.idl |   15 +++++
 sw/qa/extras/layout/data/tdf167648_minimum.fodt   |   56 ++++++++++++++++++
 sw/qa/extras/layout/layout3.cxx                   |   42 +++++++++++++-
 sw/source/core/text/guess.cxx                     |   12 ++++
 sw/source/core/text/inftxt.cxx                    |    2 
 sw/source/core/text/itradj.cxx                    |    4 -
 sw/source/core/text/portxt.cxx                    |   66 +++++++++++-----------
 sw/source/core/text/portxt.hxx                    |    2 
 11 files changed, 171 insertions(+), 114 deletions(-)

New commits:
commit 3c53797210bf0a4e3ffb36ed2beac4d5ce229ff2
Author:     László Németh <[email protected]>
AuthorDate: Thu Aug 28 11:27:07 2025 +0200
Commit:     László Németh <[email protected]>
CommitDate: Wed Sep 3 15:21:58 2025 +0200

    tdf#167648 sw letter spacing: implement minimum letter spacing
    
    Implement new paragraph justification option "Minimum letter spacing"
    allowing to shrink text lines more, choosing better paragraph layout
    instead of hyphenation or rivers of space.
    
    * Implement visual layout of minimum letter spacing.
    
    * Enable Minimum letter spacing spin box on Alignment pane of the
      paragraph formatting dialog window
    
    * Remove also desired letter spacing spin box from the alignment pane.
      It will be better to visualize CharKerning with percentage value
      instead of adding a new user interface to the same setting.
    
    * Set minimum and maximum spin box ranges to [-100, 0] and [0, 500],
      and remove unnecessary handlers.
    
    * offapi: extend API description with ranges of letter spacing
      [-100, 500] and word spacing ([0, 1000]).
    
    * Add ODF unit test.
    
    Note: hyphenated lines, lines with multiple portions haven't
    been using custom letter spacing, yet.
    
    Note: resolution of the custom letter spacing is only 1/20 point
    (1 twip) yet.
    
    Note: adjust testTdf163149 failing on a test machine, maybe
    because the DOCX test document contains the not supported Arial
    font, resulting small differences based on the font replacement.
    
    Follow-up to commit f83a04c51056445bbf947a31c8c1866a5c30bef1
    "tdf#167648 cui offapi xmloff sw: add DTP-feature maximum letter
    spacing".
    
    Change-Id: I0d686ccbdaa5324eaf6c4c7f6da0f37e76f60611
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/190533
    Tested-by: Jenkins
    Reviewed-by: László Németh <[email protected]>

diff --git a/cui/source/inc/paragrph.hxx b/cui/source/inc/paragrph.hxx
index 51d49765ff15..4218f3091e3b 100644
--- a/cui/source/inc/paragrph.hxx
+++ b/cui/source/inc/paragrph.hxx
@@ -161,7 +161,6 @@ class SvxParaAlignTabPage : public SfxTabPage
 
     /// letter spacing
     std::unique_ptr<weld::Label> m_xLabelLetterSpacing;
-    std::unique_ptr<weld::MetricSpinButton> m_xLetterSpacing;
     std::unique_ptr<weld::MetricSpinButton> m_xLetterSpacingMinimum;
     std::unique_ptr<weld::MetricSpinButton> m_xLetterSpacingMaximum;
 
@@ -171,9 +170,6 @@ class SvxParaAlignTabPage : public SfxTabPage
     DECL_LINK(WordSpacingHdl_Impl, weld::MetricSpinButton&, void);
     DECL_LINK(WordSpacingMinimumHdl_Impl, weld::MetricSpinButton&, void);
     DECL_LINK(WordSpacingMaximumHdl_Impl, weld::MetricSpinButton&, void);
-    DECL_LINK(LetterSpacingHdl_Impl, weld::MetricSpinButton&, void);
-    DECL_LINK(LetterSpacingMinimumHdl_Impl, weld::MetricSpinButton&, void);
-    DECL_LINK(LetterSpacingMaximumHdl_Impl, weld::MetricSpinButton&, void);
 
     void                    UpdateExample_Impl();
 
diff --git a/cui/source/tabpages/paragrph.cxx b/cui/source/tabpages/paragrph.cxx
index b55863973c2a..5feaf5420286 100644
--- a/cui/source/tabpages/paragrph.cxx
+++ b/cui/source/tabpages/paragrph.cxx
@@ -1288,7 +1288,6 @@ SvxParaAlignTabPage::SvxParaAlignTabPage(weld::Container* 
pPage, weld::DialogCon
     , 
m_xWordSpacingMinimum(m_xBuilder->weld_metric_spin_button(u"spin_WORD_SPACING_MIN"_ustr,
 FieldUnit::PERCENT))
     , 
m_xWordSpacingMaximum(m_xBuilder->weld_metric_spin_button(u"spin_WORD_SPACING_MAX"_ustr,
 FieldUnit::PERCENT))
     , m_xLabelLetterSpacing(m_xBuilder->weld_label(u"labelLetterSpacing"_ustr))
-    , 
m_xLetterSpacing(m_xBuilder->weld_metric_spin_button(u"spin_LETTER_SPACING"_ustr,
 FieldUnit::PERCENT))
     , 
m_xLetterSpacingMinimum(m_xBuilder->weld_metric_spin_button(u"spin_LETTER_SPACING_MIN"_ustr,
 FieldUnit::PERCENT))
     , 
m_xLetterSpacingMaximum(m_xBuilder->weld_metric_spin_button(u"spin_LETTER_SPACING_MAX"_ustr,
 FieldUnit::PERCENT))
 {
@@ -1334,12 +1333,6 @@ 
SvxParaAlignTabPage::SvxParaAlignTabPage(weld::Container* pPage, weld::DialogCon
     m_xWordSpacing->connect_value_changed(LINK(this, SvxParaAlignTabPage, 
WordSpacingHdl_Impl));
     m_xWordSpacingMinimum->connect_value_changed(LINK(this, 
SvxParaAlignTabPage, WordSpacingMinimumHdl_Impl));
     m_xWordSpacingMaximum->connect_value_changed(LINK(this, 
SvxParaAlignTabPage, WordSpacingMaximumHdl_Impl));
-
-    // Minimum <= Desired <= Maximum letter spacing
-    // apply these modifying the other values, if needed
-    m_xLetterSpacing->connect_value_changed(LINK(this, SvxParaAlignTabPage, 
LetterSpacingHdl_Impl));
-    m_xLetterSpacingMinimum->connect_value_changed(LINK(this, 
SvxParaAlignTabPage, LetterSpacingMinimumHdl_Impl));
-    m_xLetterSpacingMaximum->connect_value_changed(LINK(this, 
SvxParaAlignTabPage, LetterSpacingMaximumHdl_Impl));
 }
 
 SvxParaAlignTabPage::~SvxParaAlignTabPage()
@@ -1389,7 +1382,6 @@ bool SvxParaAlignTabPage::FillItemSet( SfxItemSet* 
rOutSet )
             m_xWordSpacing->get_value_changed_from_saved() ||
             m_xWordSpacingMinimum->get_value_changed_from_saved() ||
             m_xWordSpacingMaximum->get_value_changed_from_saved() ||
-            m_xLetterSpacing->get_value_changed_from_saved() ||
             m_xLetterSpacingMinimum->get_value_changed_from_saved() ||
             m_xLetterSpacingMaximum->get_value_changed_from_saved();
     }
@@ -1505,6 +1497,7 @@ void SvxParaAlignTabPage::Reset( const SfxItemSet* rSet )
             m_xLabelLetterSpacing->set_sensitive(true);
             // TODO add LetterSpacing (CharKern) and LetterSpacingMinimum
             m_xLetterSpacingMaximum->set_sensitive(true);
+            m_xLetterSpacingMinimum->set_sensitive(true);
             
m_xLetterSpacingMinimum->set_value(rAdj.GetPropLetterSpacingMinimum(), 
FieldUnit::PERCENT);
             
m_xLetterSpacingMaximum->set_value(rAdj.GetPropLetterSpacingMaximum(), 
FieldUnit::PERCENT);
         }
@@ -1518,7 +1511,6 @@ void SvxParaAlignTabPage::Reset( const SfxItemSet* rSet )
             m_xWordSpacingMinimum->set_sensitive(false);
             m_xWordSpacingMaximum->set_sensitive(false);
             m_xLabelLetterSpacing->set_sensitive(false);
-            m_xLetterSpacing->set_sensitive(false);
             m_xLetterSpacingMinimum->set_sensitive(false);
             m_xLetterSpacingMaximum->set_sensitive(false);
         }
@@ -1537,7 +1529,6 @@ void SvxParaAlignTabPage::Reset( const SfxItemSet* rSet )
         m_xWordSpacingMinimum->set_sensitive(false);
         m_xWordSpacingMaximum->set_sensitive(false);
         m_xLabelLetterSpacing->set_sensitive(false);
-        m_xLetterSpacing->set_sensitive(false);
         m_xLetterSpacingMinimum->set_sensitive(false);
         m_xLetterSpacingMaximum->set_sensitive(false);
     }
@@ -1600,7 +1591,6 @@ void SvxParaAlignTabPage::Reset( const SfxItemSet* rSet )
     m_xWordSpacing->save_value();
     m_xWordSpacingMinimum->save_value();
     m_xWordSpacingMaximum->save_value();
-    m_xLetterSpacing->save_value();
     m_xLetterSpacingMinimum->save_value();
     m_xLetterSpacingMaximum->save_value();
 
@@ -1621,7 +1611,6 @@ void SvxParaAlignTabPage::ChangesApplied()
     m_xWordSpacing->save_value();
     m_xWordSpacingMinimum->save_value();
     m_xWordSpacingMaximum->save_value();
-    m_xLetterSpacing->save_value();
     m_xLetterSpacingMinimum->save_value();
     m_xLetterSpacingMaximum->save_value();
 }
@@ -1639,9 +1628,8 @@ IMPL_LINK_NOARG(SvxParaAlignTabPage, AlignHdl_Impl, 
weld::Toggleable&, void)
     m_xWordSpacingMinimum->set_sensitive(bJustify);
     m_xWordSpacingMaximum->set_sensitive(bJustify);
     m_xLabelLetterSpacing->set_sensitive(bJustify);
-    // TODO implement LetterSpacing and LetterSpaceMinimum
-    m_xLetterSpacing->set_sensitive(false);
-    m_xLetterSpacingMinimum->set_sensitive(false);
+    // TODO visualize CharKerning with percentage
+    m_xLetterSpacingMinimum->set_sensitive(bJustify);
     m_xLetterSpacingMaximum->set_sensitive(bJustify);
 
     bool bLastLineIsBlock = m_xLastLineLB->get_active() == 2;
@@ -1698,33 +1686,6 @@ IMPL_LINK_NOARG(SvxParaAlignTabPage, 
WordSpacingMaximumHdl_Impl, weld::MetricSpi
         m_xWordSpacing->set_value(nMaximum, FieldUnit::PERCENT);
 }
 
-IMPL_LINK_NOARG(SvxParaAlignTabPage, LetterSpacingHdl_Impl, 
weld::MetricSpinButton&, void)
-{
-    sal_Int16 nDesired = m_xLetterSpacing->get_value(FieldUnit::PERCENT);
-    if (nDesired < m_xLetterSpacingMinimum->get_value(FieldUnit::PERCENT))
-        m_xLetterSpacingMinimum->set_value(nDesired, FieldUnit::PERCENT);
-    if (nDesired > m_xLetterSpacingMaximum->get_value(FieldUnit::PERCENT))
-        m_xLetterSpacingMaximum->set_value(nDesired, FieldUnit::PERCENT);
-}
-
-IMPL_LINK_NOARG(SvxParaAlignTabPage, LetterSpacingMinimumHdl_Impl, 
weld::MetricSpinButton&, void)
-{
-    sal_Int16 nMinimum = 
m_xLetterSpacingMinimum->get_value(FieldUnit::PERCENT);
-    if (nMinimum > m_xLetterSpacing->get_value(FieldUnit::PERCENT))
-        m_xLetterSpacing->set_value(nMinimum, FieldUnit::PERCENT);
-    if (nMinimum > m_xLetterSpacingMaximum->get_value(FieldUnit::PERCENT))
-        m_xLetterSpacingMaximum->set_value(nMinimum, FieldUnit::PERCENT);
-}
-
-IMPL_LINK_NOARG(SvxParaAlignTabPage, LetterSpacingMaximumHdl_Impl, 
weld::MetricSpinButton&, void)
-{
-    sal_Int16 nMaximum = 
m_xLetterSpacingMaximum->get_value(FieldUnit::PERCENT);
-    if (nMaximum < m_xLetterSpacingMinimum->get_value(FieldUnit::PERCENT))
-        m_xLetterSpacingMinimum->set_value(nMaximum, FieldUnit::PERCENT);
-    if (nMaximum < m_xLetterSpacing->get_value(FieldUnit::PERCENT))
-        m_xLetterSpacing->set_value(nMaximum, FieldUnit::PERCENT);
-}
-
 void SvxParaAlignTabPage::UpdateExample_Impl()
 {
     if (m_xLeft->get_active())
diff --git a/cui/uiconfig/ui/paragalignpage.ui 
b/cui/uiconfig/ui/paragalignpage.ui
index 36bf63e7c379..451f6b6765cc 100644
--- a/cui/uiconfig/ui/paragalignpage.ui
+++ b/cui/uiconfig/ui/paragalignpage.ui
@@ -21,7 +21,7 @@
     <property name="page-increment">10</property>
   </object>
   <object class="GtkAdjustment" id="adjustmentPercent4">
-    <property name="upper">500</property>
+    <property name="upper">0</property>
     <property name="lower">-100</property>
     <property name="value">0</property>
     <property name="step-increment">1</property>
@@ -29,14 +29,7 @@
   </object>
   <object class="GtkAdjustment" id="adjustmentPercent5">
     <property name="upper">500</property>
-    <property name="lower">-100</property>
-    <property name="value">0</property>
-    <property name="step-increment">1</property>
-    <property name="page-increment">10</property>
-  </object>
-  <object class="GtkAdjustment" id="adjustmentPercent6">
-    <property name="upper">500</property>
-    <property name="lower">-100</property>
+    <property name="lower">0</property>
     <property name="value">0</property>
     <property name="step-increment">1</property>
     <property name="page-increment">10</property>
@@ -556,7 +549,7 @@
                   <object class="GtkLabel" id="labelLetterSpacing">
                     <property name="visible">True</property>
                     <property name="can-focus">False</property>
-                    <property name="label" translatable="yes" 
context="paragalignpage|labelWordSpacing">_Letter spacing:</property>
+                    <property name="label" translatable="yes" 
context="paragalignpage|labelLetterSpacing">_Letter spacing:</property>
                     <property name="use-underline">True</property>
                     <property name="xalign">0</property>
                   </object>
@@ -574,7 +567,7 @@
                     <property name="adjustment">adjustmentPercent4</property>
                     <child internal-child="accessible">
                       <object class="AtkObject" 
id="spin_LETTER_SPACING_MIN-atkobject">
-                        <property name="AtkObject::accessible-description" 
translatable="yes" 
context="paralignpage|extended_tip|LETTER-JUSTIFICATION-MIN">Adjusts the 
minimum letter spacing. Enter a number between -100% (no letter spacing) and 
250% (two and a half times the width of the normal letter spacing).</property>
+                        <property name="AtkObject::accessible-description" 
translatable="yes" 
context="paralignpage|extended_tip|LETTER-JUSTIFICATION-MIN">Adjusts the 
minimum letter spacing. Enter a number between -100% and 0% (original letter 
spacing).</property>
                       </object>
                     </child>
                   </object>
@@ -583,34 +576,16 @@
                     <property name="top-attach">2</property>
                   </packing>
                 </child>
-                <child>
-                  <object class="GtkSpinButton" id="spin_LETTER_SPACING">
-                    <property name="visible">True</property>
-                    <property name="can-focus">True</property>
-                    <property name="activates-default">True</property>
-                    <property name="truncate-multiline">True</property>
-                    <property name="adjustment">adjustmentPercent5</property>
-                    <child internal-child="accessible">
-                      <object class="AtkObject" 
id="spin_LETTER_SPACING-atkobject">
-                        <property name="AtkObject::accessible-description" 
translatable="yes" context="paralignpage|extended_tip|JUSTIFICATION">Adjusts 
the desired letter spacing. Enter a number between -100% (no letter spacing) 
and 250% (two and a half times the width of the normal letter 
spacing).</property>
-                      </object>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="left-attach">2</property>
-                    <property name="top-attach">2</property>
-                  </packing>
-                </child>
                 <child>
                   <object class="GtkSpinButton" id="spin_LETTER_SPACING_MAX">
                     <property name="visible">True</property>
                     <property name="can-focus">True</property>
                     <property name="activates-default">True</property>
                     <property name="truncate-multiline">True</property>
-                    <property name="adjustment">adjustmentPercent6</property>
+                    <property name="adjustment">adjustmentPercent5</property>
                     <child internal-child="accessible">
                       <object class="AtkObject" 
id="spin_LETTER_SPACING_MAX-atkobject">
-                        <property name="AtkObject::accessible-description" 
translatable="yes" 
context="paralignpage|extended_tip|JUSTIFICATION-MAX">Adjusts the maximum 
letter spacing. Enter a number between -100% (no letter spacing) and 250% (two 
and a half times the width of the normal letter spacing).</property>
+                        <property name="AtkObject::accessible-description" 
translatable="yes" 
context="paralignpage|extended_tip|LETTER-JUSTIFICATION-MAX">Adjusts the 
maximum letter spacing. Enter a number between 0% (original letter spacing) and 
500% (letter spacing is five times the width of the space character).</property>
                       </object>
                     </child>
                   </object>
diff --git a/offapi/com/sun/star/style/ParagraphProperties.idl 
b/offapi/com/sun/star/style/ParagraphProperties.idl
index 387aa569ae89..45bc14c6508e 100644
--- a/offapi/com/sun/star/style/ParagraphProperties.idl
+++ b/offapi/com/sun/star/style/ParagraphProperties.idl
@@ -514,6 +514,9 @@ published service ParagraphProperties
         /** specifies the desired word spacing as percentage value relative
             to the width of the space character.
 
+            <p>It takes a percent value between [0, 1000], where the original
+            word spacing is denoted by 100.</p>
+
             @see ParaWordSpacingMininum
 
             @see ParaWordSpacingMaximum
@@ -525,6 +528,9 @@ published service ParagraphProperties
         /** specifies the minimum word spacing as percentage value relative
             to the width of the space character.
 
+            <p>It takes a percent value between [0, 1000], where the original
+            word spacing is denoted by 100.</p>
+
             @see ParaWordSpacing
 
             @see ParaWordSpacingMaximum
@@ -536,6 +542,9 @@ published service ParagraphProperties
         /** specifies the maximum word spacing as percentage value relative
             to the width of the space character.
 
+            <p>It takes a percent value between [0, 1000], where the original
+            word spacing is denoted by 100.</p>
+
             @see ParaWordSpacing
 
             @see ParaWordSpacingMininum
@@ -547,6 +556,9 @@ published service ParagraphProperties
         /** specifies the minimum letter spacing as percentage value relative
             to the width of the space character.
 
+            <p>It takes a percent value between [-100, 0], where the original
+            letter spacing is denoted by 0, and negative values mean its 
shrinking.</p>
+
             @see CharKerning
 
             @see ParaLetterSpacingMaximum
@@ -558,6 +570,9 @@ published service ParagraphProperties
         /** specifies the maximum letter spacing as percentage value relative
             to the width of the space character.
 
+            <p>It takes a percent value between [0, 500], where the original
+            letter spacing is denoted by 0, and negative values mean its 
shrinking.</p>
+
             @see CharKerning
 
             @see ParaLetterSpacingMininum
diff --git a/sw/qa/extras/layout/data/tdf167648_minimum.fodt 
b/sw/qa/extras/layout/data/tdf167648_minimum.fodt
new file mode 100644
index 000000000000..7e81d76bd543
--- /dev/null
+++ b/sw/qa/extras/layout/data/tdf167648_minimum.fodt
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<office:document xmlns:css3t="http://www.w3.org/TR/css3-text/"; 
xmlns:grddl="http://www.w3.org/2003/g/data-view#"; 
xmlns:xhtml="http://www.w3.org/1999/xhtml"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xmlns:xsd="http://www.w3.org/2001/XMLSchema"; 
xmlns:xforms="http://www.w3.org/2002/xforms"; 
xmlns:dom="http://www.w3.org/2001/xml-events"; 
xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0" 
xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0" 
xmlns:math="http://www.w3.org/1998/Math/MathML"; 
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" 
xmlns:ooo="http://openoffice.org/2004/office"; 
xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" 
xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0" 
xmlns:ooow="http://openoffice.org/2004/writer"; 
xmlns:xlink="http://www.w3.org/1999/xlink"; 
xmlns:drawooo="http://openoffice.org/2010/draw"; 
xmlns:oooc="http://openoffice.org/2004/calc"; 
xmlns:dc="http://purl.org/dc/elements/1.1/"; xmlns:c
 alcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0" 
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" 
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" 
xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2" 
xmlns:tableooo="http://openoffice.org/2009/table"; 
xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" 
xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0" 
xmlns:rpt="http://openoffice.org/2005/report"; 
xmlns:formx="urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0"
 xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" 
xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0" 
xmlns:officeooo="http://openoffice.org/2009/office"; 
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" 
xmlns:field="urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0" 
xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" 
xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:
 meta:1.0" 
xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0"
 office:version="1.4" office:mimetype="application/vnd.oasis.opendocument.text">
+ <office:font-face-decls>
+  <style:font-face style:name="Liberation Serif" 
svg:font-family="&apos;Liberation Serif&apos;" 
style:font-family-generic="modern" style:font-pitch="fixed"/>
+ </office:font-face-decls>
+ <office:styles>
+  <style:default-style style:family="graphic">
+   <style:graphic-properties svg:stroke-color="#3465a4" 
draw:fill-color="#729fcf" fo:wrap-option="no-wrap" draw:shadow-offset-x="0.3cm" 
draw:shadow-offset-y="0.3cm" draw:start-line-spacing-horizontal="0.283cm" 
draw:start-line-spacing-vertical="0.283cm" 
draw:end-line-spacing-horizontal="0.283cm" 
draw:end-line-spacing-vertical="0.283cm" style:writing-mode="lr-tb" 
style:flow-with-text="false"/>
+   <style:paragraph-properties style:text-autospace="ideograph-alpha" 
style:line-break="strict" loext:tab-stop-distance="0cm" 
style:writing-mode="lr-tb" style:font-independent-line-spacing="false">
+    <style:tab-stops/>
+   </style:paragraph-properties>
+   <style:text-properties style:use-window-font-color="true" 
loext:opacity="0%" style:font-name="Liberation Serif" fo:font-size="12pt" 
fo:language="en" fo:country="US" style:letter-kerning="true" 
style:font-name-asian="Noto Serif CJK SC" style:font-size-asian="10.5pt" 
style:language-asian="zh" style:country-asian="CN" 
style:font-name-complex="Lohit Devanagari1" style:font-size-complex="12pt" 
style:language-complex="hi" style:country-complex="IN"/>
+  </style:default-style>
+  <style:default-style style:family="paragraph">
+   <style:paragraph-properties fo:hyphenation-ladder-count="no-limit" 
fo:hyphenation-keep="auto" loext:hyphenation-keep-type="column" 
loext:hyphenation-keep-line="false" style:text-autospace="ideograph-alpha" 
style:punctuation-wrap="hanging" style:line-break="strict" 
style:tab-stop-distance="1.251cm" style:writing-mode="page"/>
+   <style:text-properties style:use-window-font-color="true" 
loext:opacity="0%" style:font-name="Liberation Serif" fo:font-size="12pt" 
fo:language="en" fo:country="US" style:font-name-asian="Noto Serif CJK SC" 
style:font-size-asian="10.5pt" style:language-asian="zh" 
style:country-asian="CN" style:font-name-complex="Lohit Devanagari1" 
style:font-size-complex="12pt" style:language-complex="hi" 
style:country-complex="IN" fo:hyphenate="false" 
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="no-limit" loext:hyphenation-zone="no-limit"/>
+  </style:default-style>
+  <style:default-style style:family="table">
+   <style:table-properties table:border-model="collapsing"/>
+  </style:default-style>
+  <style:default-style style:family="table-row">
+   <style:table-row-properties fo:keep-together="auto"/>
+  </style:default-style>
+  <style:style style:name="Standard" style:family="paragraph" 
style:class="text"/>
+  <style:style style:name="Text_20_body" style:display-name="Text body" 
style:family="paragraph" style:parent-style-name="Standard" style:class="text" 
style:master-page-name="">
+   <style:paragraph-properties fo:margin-left="0cm" fo:margin-right="0cm" 
fo:margin-top="0cm" fo:margin-bottom="0cm" style:contextual-spacing="false" 
fo:line-height="100%" fo:text-align="start" style:justify-single-word="false" 
fo:orphans="2" fo:widows="2" fo:hyphenation-ladder-count="no-limit" 
fo:hyphenation-keep="auto" loext:hyphenation-keep-type="column" 
loext:hyphenation-keep-line="false" fo:text-indent="0.423cm" 
style:auto-text-indent="false" style:page-number="auto"/>
+   <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="no-limit" loext:hyphenation-zone="no-limit"/>
+  </style:style>
+ </office:styles>
+ <office:automatic-styles>
+  <style:style style:name="P1" style:family="paragraph" 
style:parent-style-name="Text_20_body" style:master-page-name="">
+   <style:paragraph-properties fo:text-align="justify" 
style:justify-single-word="false" loext:word-spacing-maximum="101%" 
loext:letter-spacing-minimum="-25%" loext:letter-spacing-maximum="25%" 
fo:hyphenation-ladder-count="no-limit" fo:hyphenation-keep="auto" 
loext:hyphenation-keep-type="column" loext:hyphenation-keep-line="true" 
style:page-number="auto" style:writing-mode="lr-tb"/>
+   <style:text-properties style:font-name="Linux Libertine G:litt=0" 
fo:letter-spacing="0.018cm" officeooo:paragraph-rsid="001f2a0b" 
fo:hyphenate="false" 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="no-limit" loext:hyphenation-zone="no-limit"/>
+  </style:style>
+  <style:page-layout style:name="pm1">
+   <style:page-layout-properties fo:page-width="594.99pt" 
fo:page-height="842pt" style:num-format="1" style:print-orientation="portrait" 
fo:margin-top="72pt" fo:margin-bottom="72pt" fo:margin-left="195pt" 
fo:margin-right="195pt" style:writing-mode="lr-tb" 
style:layout-grid-color="#c0c0c0" style:layout-grid-lines="20" 
style:layout-grid-base-height="20.01pt" style:layout-grid-ruby-height="10.01pt" 
style:layout-grid-mode="none" style:layout-grid-ruby-below="false" 
style:layout-grid-print="false" style:layout-grid-display="false" 
style:footnote-max-height="0pt" loext:margin-gutter="0pt">
+    <style:footnote-sep style:width="0.018cm" 
style:distance-before-sep="0.101cm" style:distance-after-sep="0.101cm" 
style:line-style="solid" style:adjustment="left" style:rel-width="25%" 
style:color="#000000"/>
+   </style:page-layout-properties>
+  </style:page-layout>
+ </office:automatic-styles>
+ <office:master-styles>
+  <style:master-page style:name="Standard" style:page-layout-name="pm1" 
draw:style-name="dp1">
+   <style:header>
+    <text:p text:style-name="Header"/>
+   </style:header>
+  </style:master-page>
+  <style:master-page style:name="Footnote" style:page-layout-name="pm2" 
draw:style-name="dp1"/>
+  <style:master-page style:name="Endnote" style:page-layout-name="pm2" 
draw:style-name="dp1"/>
+ </office:master-styles>
+ <office:body>
+  <office:text>
+   <text:p text:style-name="P1">Lorem ipsum dolor sit amet, consectetur 
adipiscing elit. Vestibulum consequat mi quis pretium semper. Proin luctus orci 
ac neque venenatis, quis commodo dolor posuere. Curabitur dignissim sapien quis 
cursus egestas. Donec blandit auctor arcu, nec pellentesque eros molestie eget. 
In consectetur aliquam hendrerit.</text:p>
+  </office:text>
+ </office:body>
+</office:document>
diff --git a/sw/qa/extras/layout/layout3.cxx b/sw/qa/extras/layout/layout3.cxx
index cfc53c6eca62..6b259aa69bc4 100644
--- a/sw/qa/extras/layout/layout3.cxx
+++ b/sw/qa/extras/layout/layout3.cxx
@@ -545,7 +545,7 @@ CPPUNIT_TEST_FIXTURE(SwLayoutWriter3, testTdf163149)
 
             // Assert we are using the expected position for the last char
             // This was 4673, now 4163, according to the fixed space shrinking
-            CPPUNIT_ASSERT_LESS(sal_Int32(4200), sal_Int32(pDXArray[45]));
+            CPPUNIT_ASSERT_LESS(sal_Int32(4250), sal_Int32(pDXArray[45]));
             break;
         }
     }
@@ -573,8 +573,7 @@ CPPUNIT_TEST_FIXTURE(SwLayoutWriter3, testTdf167648)
             auto pTextArrayAction = static_cast<MetaTextArrayAction*>(pAction);
             auto pDXArray = pTextArrayAction->GetDXArray();
 
-            // There should be 27 chars on the first line
-            // (tdf#164499 no space shrinking in lines with tabulation)
+            // There should be 27 characters on the first line
             CPPUNIT_ASSERT_EQUAL(size_t(27), pDXArray.size());
 
             // Assert we are using the expected position for the
@@ -593,6 +592,43 @@ CPPUNIT_TEST_FIXTURE(SwLayoutWriter3, testTdf167648)
     }
 }
 
+CPPUNIT_TEST_FIXTURE(SwLayoutWriter3, testTdf167648_minimum)
+{
+    createSwDoc("tdf167648_minimum.fodt");
+    // Ensure that all text portions are calculated before testing.
+    SwDocShell* pShell = getSwDocShell();
+
+    // Dump the rendering of the first page as an XML file.
+    std::shared_ptr<GDIMetaFile> xMetaFile = pShell->GetPreviewMetaFile();
+    MetafileXmlDump dumper;
+
+    xmlDocUniquePtr pXmlDoc = dumpAndParse(dumper, *xMetaFile);
+    CPPUNIT_ASSERT(pXmlDoc);
+
+    // Find the first text array action
+    for (size_t nAction = 0; nAction < xMetaFile->GetActionSize(); nAction++)
+    {
+        auto pAction = xMetaFile->GetAction(nAction);
+        if (pAction->GetType() == MetaActionType::TEXTARRAY)
+        {
+            auto pTextArrayAction = static_cast<MetaTextArrayAction*>(pAction);
+            auto pDXArray = pTextArrayAction->GetDXArray();
+
+            // There should be 39 characters on the first line
+            // This was 27 characters, but setting minimum letter spacing
+            // to -25% allows more words in the line
+            CPPUNIT_ASSERT_EQUAL(size_t(39), pDXArray.size());
+
+            // Assert we are using the expected position for the
+            // second character of the first word with enlarged letter-spacing
+            // This was 286, now 266, according to the -25% minimum letter 
spacing
+            CPPUNIT_ASSERT_LESS(sal_Int32(270), sal_Int32(pDXArray[1]));
+
+            break;
+        }
+    }
+}
+
 CPPUNIT_TEST_FIXTURE(SwLayoutWriter3, testTdf164499)
 {
     createSwDoc("tdf164499.docx");
diff --git a/sw/source/core/text/guess.cxx b/sw/source/core/text/guess.cxx
index 694a74361553..384c98baba8d 100644
--- a/sw/source/core/text/guess.cxx
+++ b/sw/source/core/text/guess.cxx
@@ -499,6 +499,18 @@ bool SwTextGuess::Guess( const SwTextPortion& rPor, 
SwTextFormatInfo &rInf,
     {
         m_nCutPos = rInf.GetTextBreak( nLineWidth, nMaxLen, nMaxComp, 
rInf.GetCachedVclData().get() );
 
+        // tdf#167648 minimum letter spacing allows more text in the line
+        // TODO don't be greedy, allow only an extra word or word part
+        if ( const sal_Int16 nLetterSpacingMinimum = 
aAdjustItem.GetPropLetterSpacingMinimum() )
+        {
+            SwTwips nExtraSpace = sal_Int32(m_nCutPos - rInf.GetIdx()) *
+                                     nSpaceWidth / 10.0 * 
nLetterSpacingMinimum / 100.0;
+            nLineWidth -= nExtraSpace;
+            // sum minimum word spacing and letter spacing
+            rInf.SetExtraSpace( rInf.GetExtraSpace() + nExtraSpace );
+            m_nCutPos = rInf.GetTextBreak( nLineWidth, nMaxLen, nMaxComp, 
rInf.GetCachedVclData().get() );
+        }
+
 #if OSL_DEBUG_LEVEL > 1
         if ( TextFrameIndex(COMPLETE_STRING) != m_nCutPos )
         {
diff --git a/sw/source/core/text/inftxt.cxx b/sw/source/core/text/inftxt.cxx
index 9c0f15d62451..8c297d4e300d 100644
--- a/sw/source/core/text/inftxt.cxx
+++ b/sw/source/core/text/inftxt.cxx
@@ -780,7 +780,7 @@ void SwTextPaintInfo::DrawText_( const OUString &rText, 
const SwLinePortion &rPo
             aDrawInf.SetSmartTags( bTmpSmart ? m_pSmartTags : nullptr );
 
             // set custom letter spacing (hyphenation hasn't been supported 
yet)
-            if ( rPor.GetLetterSpacing() > 0 )
+            if ( rPor.GetLetterSpacing() != 0 )
                 aDrawInf.SetLetterSpacing( rPor.GetLetterSpacing() / 
sal_Int32(nLength) );
 
             m_pFnt->DrawText_( aDrawInf );
diff --git a/sw/source/core/text/itradj.cxx b/sw/source/core/text/itradj.cxx
index 3a1a956f4059..c6230b18f5db 100644
--- a/sw/source/core/text/itradj.cxx
+++ b/sw/source/core/text/itradj.cxx
@@ -370,11 +370,11 @@ void SwTextAdjuster::CalcNewBlock( SwLineLayout *pCurrent,
                             : 0;
 
                     SwLinePortion *pPortion = pCurrent->GetFirstPortion();
-                    tools::Long nSpaceKern = pPortion->GetLetterSpacing() > 0 
&& pPortion->GetSpaceCount()
+                    tools::Long nSpaceKern = pPortion->GetSpaceCount()
                         ? tools::Long(pPortion->GetLetterSpacing()) / 
sal_Int32(pPortion->GetSpaceCount()) * 100
                         : 0;
                     // set expansion in 1/100 twips/space
-                    pCurrent->SetLLSpaceAdd( nSpaceSub ? nSpaceSub : 
(nSpaceAdd > nSpaceKern ? nSpaceAdd - nSpaceKern : 0), nSpaceIdx );
+                    pCurrent->SetLLSpaceAdd( nSpaceSub ? nSpaceSub + 
nSpaceKern : (nSpaceAdd > nSpaceKern ? nSpaceAdd - nSpaceKern : 0), nSpaceIdx );
                     pPos->Width( 
static_cast<SwGluePortion*>(pPos)->GetFixWidth() );
                 }
                 else if (IsOneBlock() && nCharCnt > TextFrameIndex(1))
diff --git a/sw/source/core/text/portxt.cxx b/sw/source/core/text/portxt.cxx
index a95ec986ef7b..a8c4fac62a03 100644
--- a/sw/source/core/text/portxt.cxx
+++ b/sw/source/core/text/portxt.cxx
@@ -337,6 +337,33 @@ sal_uInt16 SwTextPortion::GetMaxComp(const 
SwTextFormatInfo& rInf) const
                : 0;
 }
 
+void SwTextPortion::SetSpacing( SwTextFormatInfo &rInf, const TextFrameIndex 
nBreakPos,
+                const sal_Int32 nSpaces, const sal_Int16 nWidthOf10Spaces )
+{
+    SvxAdjustItem aAdjustItem =
+        
rInf.GetTextFrame()->GetTextNodeForParaProps()->GetSwAttrSet().GetAdjust();
+    // width of a single expanded space without letter spacing and glyph 
scaling
+    float fSpaceNormal =
+        (rInf.GetLineWidth() - (rInf.GetBreakWidth() - nSpaces * 
nWidthOf10Spaces/10.0)) / nSpaces;
+    // the part to be removed: the previous width minus the maximum allowed 
space width
+    float fExpansionOverMax =
+        fSpaceNormal - nWidthOf10Spaces / 10.0 * 
aAdjustItem.GetPropWordSpacingMaximum() / 100.0;
+    ExtraSpaceSize( fExpansionOverMax > 0 ? fExpansionOverMax : 0 );
+    int nLetterCount = sal_Int32(nBreakPos) - sal_Int32(rInf.GetIdx());
+    // letter spacing/character to be added or substracted to get the desired 
word spacing
+    float fLetterSpacingForDesiredWordSpacing =
+        nLetterCount > 0 ? (((fSpaceNormal - nWidthOf10Spaces/10.0) * nSpaces) 
/ nLetterCount) : 0;
+    // letter spacing/character allowed by maximum letter spacing
+    float fMaximumLetterSpacing =
+        nWidthOf10Spaces / 10.0 * aAdjustItem.GetPropLetterSpacingMaximum() / 
100.0;
+    // final letter spacing/character based on the desired word spacing and 
maximum letter spacing
+    // TODO fix resolution applying 1/100 twips instead of 1 twip
+    SwTwips nLetterSpacing = std::min( fLetterSpacingForDesiredWordSpacing, 
fMaximumLetterSpacing );
+    // full width of the extra (rounded) letter spacing within the line
+    SetLetterSpacing( SwTwips(nLetterSpacing * nLetterCount) );
+    SetSpaceCount( TextFrameIndex(nSpaces) );
+}
+
 bool SwTextPortion::Format_( SwTextFormatInfo &rInf )
 {
     // 5744: If only the hyphen does not fit anymore, we still need to wrap
@@ -379,6 +406,7 @@ bool SwTextPortion::Format_( SwTextFormatInfo &rInf )
     bool bNoWordSpacing = aAdjustItem.GetPropWordSpacing() == 100 &&
                     aAdjustItem.GetPropWordSpacingMinimum() == 100 &&
                     aAdjustItem.GetPropWordSpacingMaximum() == 100 &&
+                    aAdjustItem.GetPropLetterSpacingMinimum() == 0 &&
                     aAdjustItem.GetPropLetterSpacingMaximum() == 0;
     // support old ODT documents, where only JustifyLinesWithShrinking was set
     bool bOldInterop = bInteropSmartJustify && bNoWordSpacing;
@@ -430,20 +458,9 @@ bool SwTextPortion::Format_( SwTextFormatInfo &rInf )
 
             // calculate available word spacing for letter spacing, and for 
the word spacing indicator
             // for non-hyphenated single portion lines
-            // TODO: enable letter spacing for multiportion lines
+            // TODO: enable letter spacing for multiportion, also for 
hyphenated lines
             if ( !bOrigHyphenated && rInf.GetLineStart() == rInf.GetIdx() )
-            {
-                // TODO calculate correct value for letter spacing of 
hyphenated lines
-                float fExpansionOverMax = fSpaceNormal - nSpaceWidth/10.0 * 
aAdjustItem.GetPropWordSpacingMaximum()/100.0;
-                ExtraSpaceSize( rInf.GetBreakWidth() > rInf.GetLineWidth()/2 
&& fExpansionOverMax > 0 ? fExpansionOverMax : 0);
-                int nLetterCount = sal_Int32(pGuess->BreakPos()) - 
sal_Int32(rInf.GetIdx());
-                float fAvailableLetterSpacing = ((fSpaceNormal - 
nSpaceWidth/10.0) * nRealSpaces) / nLetterCount;
-                float fCustomLetterSpacing = nSpaceWidth/10.0 * 
aAdjustItem.GetPropLetterSpacingMaximum() / 100.0;
-                // TODO fix resolution applying 1/100 twips instead of 1 twip
-                SwTwips nLetterSpacing = std::min(fAvailableLetterSpacing, 
fCustomLetterSpacing);
-                SetLetterSpacing(SwTwips(nLetterSpacing * nLetterCount));
-                SetSpaceCount(TextFrameIndex(nRealSpaces));
-            }
+                SetSpacing(rInf, pGuess->BreakPos(), nRealSpaces, nSpaceWidth);
 
             // calculate line breaking with desired word spacing, also
             // if the desired word spacing is 100%, but there is a greater
@@ -454,23 +471,12 @@ bool SwTextPortion::Format_( SwTextFormatInfo &rInf )
                 pGuess.emplace();
                 bFull = !pGuess->Guess( *this, rInf, Height(), nSpacesInLine, 
aAdjustItem.GetPropWordSpacing(), nSpaceWidth );
                 sal_Int32 nSpacesInLine2 = rInf.GetLineSpaceCount( 
pGuess->BreakPos() );
-
-                if ( rInf.GetBreakWidth() <= rInf.GetLineWidth() )
+                if ( aAdjustItem.GetPropLetterSpacingMinimum() < 0 || 
rInf.GetBreakWidth() <= rInf.GetLineWidth() )
                 {
                     fSpaceNormal = (rInf.GetLineWidth() - 
(rInf.GetBreakWidth() - nSpacesInLine2 * nSpaceWidth/10.0))/nSpacesInLine2;
-                    // TODO: enable letter spacing for multiportion lines
+                    // TODO: enable letter spacing for multiportion, also for 
hyphenated lines
                     if ( !bOrigHyphenated && rInf.GetLineStart() == 
rInf.GetIdx() )
-                    {
-                        float fExpansionOverMax = fSpaceNormal - 
nSpaceWidth/10.0 * aAdjustItem.GetPropWordSpacingMaximum()/100.0;
-                        ExtraSpaceSize( rInf.GetBreakWidth() > 
rInf.GetLineWidth()/2 && fExpansionOverMax > 0 ? fExpansionOverMax : 0);
-                        int nLetterCount = sal_Int32(pGuess->BreakPos()) - 
sal_Int32(rInf.GetIdx());
-                        float fAvailableLetterSpacing = ((fSpaceNormal - 
nSpaceWidth/10.0) * nRealSpaces) / nLetterCount;
-                        float fCustomLetterSpacing = nSpaceWidth/10.0 * 
aAdjustItem.GetPropLetterSpacingMaximum() / 100.0;
-                        // TODO fix resolution applying 1/100 twips instead of 
1 twip
-                        SwTwips nLetterSpacing = 
std::min(fAvailableLetterSpacing, fCustomLetterSpacing);
-                        SetLetterSpacing(SwTwips(nLetterSpacing * 
nLetterCount));
-                        SetSpaceCount(TextFrameIndex(nRealSpaces));
-                    }
+                        SetSpacing(rInf, pGuess->BreakPos(), nSpacesInLine2, 
nSpaceWidth);
                 }
             }
 
@@ -539,16 +545,15 @@ bool SwTextPortion::Format_( SwTextFormatInfo &rInf )
                         if ( z1 >= z0 || bIsPortion )
                         {
                             pGuess = std::move(pGuess2);
-                            ExtraSpaceSize(0);
-                            SetLetterSpacing(0);
+                            SetSpacing(rInf, pGuess->BreakPos(), 
nSpacesInLineShrink, nSpaceWidth);
                             bFull = bFull2;
                         }
                     }
                     else if ( bOldInterop )
                     {
                         pGuess = std::move(pGuess2);
+                        SetSpaceCount( TextFrameIndex(nSpacesInLineShrink));
                         ExtraSpaceSize(0);
-                        SetLetterSpacing(0);
                         bFull = bFull2;
                     }
                 }
@@ -561,7 +566,6 @@ bool SwTextPortion::Format_( SwTextFormatInfo &rInf )
             {
                 ExtraShrunkWidth( pGuess->BreakWidth() );
                 ExtraSpaceSize( 0 );
-                SetLetterSpacing(0);
             }
         }
     }
diff --git a/sw/source/core/text/portxt.hxx b/sw/source/core/text/portxt.hxx
index 132efe86dd20..aff2680731f4 100644
--- a/sw/source/core/text/portxt.hxx
+++ b/sw/source/core/text/portxt.hxx
@@ -27,6 +27,8 @@ class SwTextPortion : public SwLinePortion
 {
     void BreakCut( SwTextFormatInfo &rInf, const SwTextGuess &rGuess );
     void BreakUnderflow( SwTextFormatInfo &rInf );
+    void SetSpacing( SwTextFormatInfo &rInf, const TextFrameIndex nBreakPos,
+            const sal_Int32 nSpaces, const sal_Int16 nWidthOf10Spaces );
     bool Format_( SwTextFormatInfo &rInf );
 
 public:

Reply via email to