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 <nemeth@numbertext.org>
This commit is contained in:
László Németh
2025-06-09 23:59:40 +02:00
parent 3f85923ccf
commit 529755f091
8 changed files with 172 additions and 92 deletions
+1 -1
View File
@@ -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">
+20 -15
View File
@@ -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)
+4 -9
View File
@@ -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 );
+1 -1
View File
@@ -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(); }
+32 -2
View File
@@ -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: */
+7
View File
@@ -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);
};
/**
+101 -64
View File
@@ -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 )
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 )
{
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())
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 )
@@ -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;