sw/qa/extras/layout/data/tdf168448.fodt |   45 ++++++++++++++++++++++
 sw/qa/extras/layout/layout3.cxx         |   65 ++++++++++++++++++++++++++++++++
 sw/source/core/text/itrpaint.cxx        |   10 ++++
 sw/source/core/text/portxt.cxx          |   50 ++++++++++++++++++------
 sw/source/core/text/portxt.hxx          |    2 
 5 files changed, 158 insertions(+), 14 deletions(-)

New commits:
commit d86f68e6b845f65896c1ffc4f55d09ec48fd9840
Author:     László Németh <nem...@numbertext.org>
AuthorDate: Wed Sep 17 13:41:02 2025 +0200
Commit:     László Németh <nem...@numbertext.org>
CommitDate: Wed Sep 17 22:58:27 2025 +0200

    tdf#168448 sw letter spacing: enable it + glyph scaling at hyphenation
    
    Letter spacing and glyph scaling were disabled in hyphenated lines
    because of their broken layout. Now the hyphen mark is positioned
    correctly at the end of the line, not before that, applying plus letter
    spacing and glyph scaling on horizontal position of the hyphen portion,
    and without expanding the text over the hyphen portion.
    
    Follow-up to commit 33f20d0a0fb32bf4452c15c6ae0e7c85b2ee6d46
    "tdf#168351 sw letter spacing: fix spaces in the last line",
    commit 45ec7bd76196dcc60b4c2db2f6f00623ecbaf5a4
    "tdf#168251 cui offapi xmloff sw glyph scaling: extend UNO/UX/ODF",
    commit 3c53797210bf0a4e3ffb36ed2beac4d5ce229ff2
    "tdf#167648 sw letter spacing: implement minimum letter spacing"
    and commit f83a04c51056445bbf947a31c8c1866a5c30bef1
    "tdf#167648 cui offapi xmloff sw: add DTP-feature maximum letter
    spacing".
    
    Change-Id: I36ec02be9afc60e1d640c40b628a786aea9a12aa
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/191080
    Tested-by: Jenkins
    Reviewed-by: László Németh <nem...@numbertext.org>

diff --git a/sw/qa/extras/layout/data/tdf168448.fodt 
b/sw/qa/extras/layout/data/tdf168448.fodt
new file mode 100644
index 000000000000..ce9f43714210
--- /dev/null
+++ b/sw/qa/extras/layout/data/tdf168448.fodt
@@ -0,0 +1,45 @@
+<?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="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="35.46pt" 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:letter-kerning="true" 
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: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="0pt" fo:margin-right="0pt" 
fo:margin-top="0pt" fo:margin-bottom="0pt" 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="11.99pt" 
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" fo:hyphenation-ladder-count="no-limit" 
fo:hyphenation-keep="auto" loext:hyphenation-keep-type="column" 
loext:hyphenation-keep-line="true" fo:text-indent="0pt" 
style:auto-text-indent="false" style:page-number="auto" 
style:writing-mode="lr-tb" loext:letter-spacing-minimum="-25%" 
loext:letter-spacing-maximum="50%"/>
+   <style:text-properties officeooo:paragraph-rsid="00110e77" 
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="4" 
loext:hyphenation-zone="no-limit" 
loext:hyphenation-compound-remain-char-count="2"/>
+  </style:style>
+  <style:style style:name="T1" style:family="text">
+   <style:text-properties fo:letter-spacing="0.51pt"/>
+  </style:style>
+  <style:page-layout style:name="pm1">
+   <style:page-layout-properties fo:page-width="595.3pt" 
fo:page-height="841.89pt" style:num-format="1" 
style:print-orientation="portrait" fo:margin-top="72pt" fo:margin-bottom="72pt" 
fo:margin-left="28.35pt" fo:margin-right="398.89pt" 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.51pt" 
style:distance-before-sep="2.86pt" style:distance-after-sep="2.86pt" 
style:line-style="solid" style:adjustment="left" style:rel-width="25%" 
style:color="#000000"/>
+   </style:page-layout-properties>
+   <style:header-style/>
+   <style:footer-style/>
+  </style:page-layout>
+  <style:style style:name="dp1" style:family="drawing-page">
+   <style:drawing-page-properties draw:background-size="full"/>
+  </style:style>
+ </office:automatic-styles>
+ <office:master-styles>
+  <style:master-page style:name="Standard" style:page-layout-name="pm1" 
draw:style-name="dp1"/>
+ </office:master-styles>
+ <office:body>
+  <office:text>
+   <text:p text:style-name="P1"><text:span text:style-name="T1">Lorem ipsum 
dolor sit consectetur ad.</text:span></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 7c60f5e02c35..20185ad41211 100644
--- a/sw/qa/extras/layout/layout3.cxx
+++ b/sw/qa/extras/layout/layout3.cxx
@@ -735,6 +735,71 @@ CPPUNIT_TEST_FIXTURE(SwLayoutWriter3, testTdf168351)
     }
 }
 
+CPPUNIT_TEST_FIXTURE(SwLayoutWriter3, testTdf168448)
+{
+    uno::Reference<linguistic2::XHyphenator> xHyphenator = 
LinguMgr::GetHyphenator();
+    if (!xHyphenator->hasLocale(lang::Locale(u"en"_ustr, u"US"_ustr, 
OUString())))
+        return;
+
+    createSwDoc("tdf168448.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 two text array actions (associated to the first text 
line)
+    bool bFirstArray = true;
+    for (size_t nAction = 0; nAction < xMetaFile->GetActionSize(); nAction++)
+    {
+        auto pAction = xMetaFile->GetAction(nAction);
+
+        // check letter spacing in the first line (in the first text array)
+        if (bFirstArray && pAction->GetType() == MetaActionType::TEXTARRAY)
+        {
+            auto pTextArrayAction = static_cast<MetaTextArrayAction*>(pAction);
+            auto pDXArray = pTextArrayAction->GetDXArray();
+
+            // There should be 25 characters on the first line
+            CPPUNIT_ASSERT_EQUAL(size_t(25), pDXArray.size());
+
+            // Assert we are using the expected position for the
+            // last character of the first word with enlarged letter-spacing
+            // This was 750, now 786, according to the enabled maximum letter 
spacing
+            CPPUNIT_ASSERT_GREATER(sal_Int32(770), sal_Int32(pDXArray[4]));
+
+            // Assert we are using the expected position for the
+            // first character of the second word with enlarged letter-spacing
+            // This was 881, now 877, according to the enabled maximum letter 
spacing
+            CPPUNIT_ASSERT_LESS(sal_Int32(880), sal_Int32(pDXArray[5]));
+
+            bFirstArray = false;
+            continue;
+        }
+
+        // check hyphen position of the first line (in the second text array)
+        if (!bFirstArray && pAction->GetType() == MetaActionType::TEXTARRAY)
+        {
+            auto pTextArrayAction = static_cast<MetaTextArrayAction*>(pAction);
+            auto pDXArray = pTextArrayAction->GetDXArray();
+
+            // There should be 1 character, the hyphen of the first line
+            CPPUNIT_ASSERT_EQUAL(size_t(1), pDXArray.size());
+
+            // This was 3662 (at enabled letter spacing for the hyphenated 
line),
+            // now 4149, according to the fixed hyphen position
+            auto nX = pTextArrayAction->GetPoint().X();
+            CPPUNIT_ASSERT_GREATER(sal_Int32(4100), sal_Int32(nX));
+
+            break;
+        }
+    }
+}
+
 CPPUNIT_TEST_FIXTURE(SwLayoutWriter3, testTdf164499)
 {
     createSwDoc("tdf164499.docx");
diff --git a/sw/source/core/text/itrpaint.cxx b/sw/source/core/text/itrpaint.cxx
index 6e9d48a3a8f3..d9d5ec7f3ac2 100644
--- a/sw/source/core/text/itrpaint.cxx
+++ b/sw/source/core/text/itrpaint.cxx
@@ -427,7 +427,17 @@ void SwTextPainter::DrawTextLine( const SwRect &rPaint, 
SwSaveClip &rClip,
             if( pPor->IsMultiPortion() )
                 PaintMultiPortion( rPaint, static_cast<SwMultiPortion&>(*pPor) 
);
             else
+            {
+                // adjust the hyphen at custom spacing
+                if ( pPor->InHyphGrp() && ( 
m_pCurr->GetFirstPortion()->GetLetterSpacing() > 0 ||
+                            m_pCurr->GetFirstPortion()->GetScaleWidthSpacing() 
> 0 ) )
+                {
+                    GetInfo().X( GetInfo().X() +
+                            m_pCurr->GetFirstPortion()->GetLetterSpacing() +
+                            m_pCurr->GetFirstPortion()->GetScaleWidthSpacing() 
);
+                }
                 pPor->Paint( GetInfo() );
+            }
         }
 
         // lazy open LBody and paragraph tag after num portions have been 
painted to Lbl
diff --git a/sw/source/core/text/portxt.cxx b/sw/source/core/text/portxt.cxx
index a440c0af336f..a88b7f670502 100644
--- a/sw/source/core/text/portxt.cxx
+++ b/sw/source/core/text/portxt.cxx
@@ -19,6 +19,8 @@
 
 #include <com/sun/star/i18n/ScriptType.hpp>
 #include <com/sun/star/i18n/XBreakIterator.hpp>
+#include <com/sun/star/uno/Reference.hxx>
+#include <editeng/unolingu.hxx>
 #include <i18nlangtag/mslangid.hxx>
 #include <breakit.hxx>
 #include <hintids.hxx>
@@ -341,23 +343,45 @@ sal_uInt16 SwTextPortion::GetMaxComp(const 
SwTextFormatInfo& rInf) const
                : 0;
 }
 
-void SwTextPortion::SetSpacing( SwTextFormatInfo &rInf, const TextFrameIndex 
nBreakPos,
+void SwTextPortion::SetSpacing( SwTextFormatInfo &rInf, SwTextGuess const 
&rGuess,
                 const sal_Int32 nSpaces, const sal_Int16 nWidthOf10Spaces )
 {
     // TODO allow letter spacing and glyph scaling in single word lines, too
     if ( nSpaces == 0 )
         return;
 
+    // adjust hyphen: remove hyphen width from the line width, because
+    // hyphen mark and optional text of the alternative hyphenation will
+    // be in an extra portion
+    sal_Int16 nHyphenWidth = 0;
+    if( rGuess.HyphWord().is() && rGuess.BreakPos() > rInf.GetLineStart()
+            && ( rGuess.BreakPos() > rInf.GetIdx() ||
+               ( rInf.GetLast() && ! rInf.GetLast()->IsFlyPortion() ) ) )
+    {
+        // TODO support custom hyphen mark, also in 
SwHyphPortion/CreateHyphen()
+        static constexpr OUString STR_HYPHEN = u"-"_ustr;
+        const uno::Reference<  com::sun::star::linguistic2::XHyphenatedWord >& 
 xHyphWord = rGuess.HyphWord();
+        // first case: hyphenated word has alternative spelling
+        if ( xHyphWord->isAlternativeSpelling() )
+        {
+            SvxAlternativeSpelling aAltSpell = SvxGetAltSpelling( xHyphWord );
+            OUString aAltText = aAltSpell.aReplacement;
+            nHyphenWidth = rInf.GetTextSize(aAltText + STR_HYPHEN).Width();
+        }
+        else
+            nHyphenWidth = rInf.GetTextSize(STR_HYPHEN).Width();
+    }
+
     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;
+    float fSpaceNormal = ( rInf.GetLineWidth() - nHyphenWidth -
+                    (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());
+    int nLetterCount = sal_Int32(rGuess.BreakPos()) - sal_Int32(rInf.GetIdx());
     // letter spacing/character to be added or subtracted to get the desired 
word spacing
     float fLetterSpacingForDesiredWordSpacing =
         nLetterCount > 0 ? (((fSpaceNormal - nWidthOf10Spaces/10.0) * nSpaces) 
/ nLetterCount) : 0;
@@ -383,7 +407,8 @@ void SwTextPortion::SetSpacing( SwTextFormatInfo &rInf, 
const TextFrameIndex nBr
     // apply it only after applying maximum letter spacing
     // TODO: change this after variable font support
     if ( aAdjustItem.GetPropScaleWidthMaximum() != 100 )
-        SetScaleWidth( 100.0 * rInf.GetLineWidth() / (rInf.GetBreakWidth() + 
GetLetterSpacing()) );
+        SetScaleWidth( 100.0 * ( rInf.GetLineWidth() - nHyphenWidth ) /
+                                         ( rInf.GetBreakWidth() + 
GetLetterSpacing() ) );
     if ( GetScaleWidth() > aAdjustItem.GetPropScaleWidthMaximum() )
         SetScaleWidth( aAdjustItem.GetPropScaleWidthMaximum() );
     // space used by glyph scaling to adjust word spacing
@@ -488,10 +513,9 @@ bool SwTextPortion::Format_( SwTextFormatInfo &rInf )
                         pGuess->BreakPos() > rInf.GetLineStart();
 
             // 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, also for 
hyphenated lines
-            if ( !bOrigHyphenated && rInf.GetLineStart() == rInf.GetIdx() )
-                SetSpacing(rInf, pGuess->BreakPos(), nRealSpaces, nSpaceWidth);
+            // for single portion lines, TODO: enable letter spacing for 
multiportion
+            if ( rInf.GetLineStart() == rInf.GetIdx() )
+                SetSpacing(rInf, *pGuess, nRealSpaces, nSpaceWidth);
 
             // calculate line breaking with desired word spacing, also
             // if the desired word spacing is 100%, but there is a greater
@@ -505,9 +529,9 @@ bool SwTextPortion::Format_( SwTextFormatInfo &rInf )
                 if ( aAdjustItem.GetPropLetterSpacingMinimum() < 0 || 
rInf.GetBreakWidth() <= rInf.GetLineWidth() )
                 {
                     fSpaceNormal = (rInf.GetLineWidth() - 
(rInf.GetBreakWidth() - nSpacesInLine2 * nSpaceWidth/10.0))/nSpacesInLine2;
-                    // TODO: enable letter spacing for multiportion, also for 
hyphenated lines
-                    if ( !bOrigHyphenated && rInf.GetLineStart() == 
rInf.GetIdx() )
-                        SetSpacing(rInf, pGuess->BreakPos(), nSpacesInLine2, 
nSpaceWidth);
+                    // TODO: enable letter spacing for multiportion
+                    if ( rInf.GetLineStart() == rInf.GetIdx() )
+                        SetSpacing(rInf, *pGuess, nSpacesInLine2, nSpaceWidth);
                 }
             }
 
@@ -576,7 +600,7 @@ bool SwTextPortion::Format_( SwTextFormatInfo &rInf )
                         if ( z1 >= z0 || bIsPortion )
                         {
                             pGuess = std::move(pGuess2);
-                            SetSpacing(rInf, pGuess->BreakPos(), 
nSpacesInLineShrink, nSpaceWidth);
+                            SetSpacing(rInf, *pGuess, nSpacesInLineShrink, 
nSpaceWidth);
                             bFull = bFull2;
                         }
                     }
diff --git a/sw/source/core/text/portxt.hxx b/sw/source/core/text/portxt.hxx
index aff2680731f4..ef7a4edcef9a 100644
--- a/sw/source/core/text/portxt.hxx
+++ b/sw/source/core/text/portxt.hxx
@@ -27,7 +27,7 @@ class SwTextPortion : public SwLinePortion
 {
     void BreakCut( SwTextFormatInfo &rInf, const SwTextGuess &rGuess );
     void BreakUnderflow( SwTextFormatInfo &rInf );
-    void SetSpacing( SwTextFormatInfo &rInf, const TextFrameIndex nBreakPos,
+    void SetSpacing( SwTextFormatInfo &rInf, SwTextGuess const &rGuess,
             const sal_Int32 nSpaces, const sal_Int16 nWidthOf10Spaces );
     bool Format_( SwTextFormatInfo &rInf );
 

Reply via email to