summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--editeng/source/editeng/impedit3.cxx14
-rw-r--r--i18nutil/qa/cppunit/test_kashida.cxx38
-rw-r--r--i18nutil/source/utility/kashida.cxx62
-rw-r--r--include/i18nutil/kashida.hxx4
-rw-r--r--include/vcl/outdev.hxx3
-rw-r--r--sw/source/core/inc/scriptinfo.hxx8
-rw-r--r--sw/source/core/text/itradj.cxx70
-rw-r--r--sw/source/core/text/porlay.cxx21
-rw-r--r--vcl/qa/cppunit/pdfexport/data/tdf163105-editeng.fodt131
-rw-r--r--vcl/qa/cppunit/pdfexport/data/tdf163105-kashida-spaces.fodt (renamed from vcl/qa/cppunit/pdfexport/data/tdf163105.fodt)0
-rw-r--r--vcl/qa/cppunit/pdfexport/data/tdf163105-writer.fodt114
-rw-r--r--vcl/qa/cppunit/pdfexport/pdfexport2.cxx95
-rw-r--r--vcl/source/outdev/font.cxx29
13 files changed, 553 insertions, 36 deletions
diff --git a/editeng/source/editeng/impedit3.cxx b/editeng/source/editeng/impedit3.cxx
index b961393bb24b..5993ab9a3ca0 100644
--- a/editeng/source/editeng/impedit3.cxx
+++ b/editeng/source/editeng/impedit3.cxx
@@ -60,6 +60,7 @@
#include <editeng/forbiddencharacterstable.hxx>
#include <comphelper/configuration.hxx>
+#include <comphelper/scopeguard.hxx>
#include <math.h>
#include <vcl/metric.hxx>
@@ -2344,10 +2345,17 @@ void ImpEditEngine::ImpAdjustBlocks(ParaPortion& rParaPortion, EditLine& rLine,
void ImpEditEngine::ImpFindKashidas(ContentNode* pNode, sal_Int32 nStart, sal_Int32 nEnd,
std::vector<sal_Int32>& rArray, sal_Int32 nRemainingSpace)
{
+ auto nOldLayout = GetRefDevice()->GetLayoutMode();
+ comphelper::ScopeGuard stGuard{ [this, nOldLayout]
+ { GetRefDevice()->SetLayoutMode(nOldLayout); } };
+
+ GetRefDevice()->SetLayoutMode(nOldLayout | vcl::text::ComplexTextLayoutFlags::BiDiRtl);
+
// Kashida glyph looks suspicious, skip Kashida justification
if (GetRefDevice()->GetMinKashida() <= 0)
return;
+ std::vector<bool> aValidPositions;
std::vector<sal_Int32> aKashidaArray;
std::vector<sal_Int32> aMinKashidaArray;
sal_Int32 nTotalMinKashida = 0U;
@@ -2367,11 +2375,12 @@ void ImpEditEngine::ImpFindKashidas(ContentNode* pNode, sal_Int32 nStart, sal_In
aWordSel.Max().SetIndex( nEnd );
OUString aWord = GetSelected( aWordSel );
+ GetRefDevice()->GetWordKashidaPositions(aWord, &aValidPositions);
// restore selection for proper iteration at the end of the function
aWordSel.Max().SetIndex( nSavPos );
- auto stKashidaPos = i18nutil::GetWordKashidaPosition(aWord);
+ auto stKashidaPos = i18nutil::GetWordKashidaPosition(aWord, aValidPositions);
if (stKashidaPos.has_value())
{
@@ -2406,12 +2415,9 @@ void ImpEditEngine::ImpFindKashidas(ContentNode* pNode, sal_Int32 nStart, sal_In
// Validate
std::vector<sal_Int32> aDropped;
- auto nOldLayout = GetRefDevice()->GetLayoutMode();
- GetRefDevice()->SetLayoutMode(nOldLayout | vcl::text::ComplexTextLayoutFlags::BiDiRtl);
GetRefDevice()->ValidateKashidas(pNode->GetString(), nStart, nEnd - nStart,
/*nPartIdx=*/nStart, /*nPartLen=*/nEnd - nStart, aKashidaArray,
&aDropped);
- GetRefDevice()->SetLayoutMode(nOldLayout);
for (auto const& pos : aKashidaArray)
if (std::find(aDropped.begin(), aDropped.end(), pos) == aDropped.end())
diff --git a/i18nutil/qa/cppunit/test_kashida.cxx b/i18nutil/qa/cppunit/test_kashida.cxx
index 1ab2729cb06a..46b40c2a5b7a 100644
--- a/i18nutil/qa/cppunit/test_kashida.cxx
+++ b/i18nutil/qa/cppunit/test_kashida.cxx
@@ -22,13 +22,17 @@ class KashidaTest : public CppUnit::TestFixture
{
public:
void testCharacteristic();
+ void testManualKashida();
void testFinalYeh();
void testNoZwnjExpansion();
+ void testExcludeInvalid();
CPPUNIT_TEST_SUITE(KashidaTest);
CPPUNIT_TEST(testCharacteristic);
+ CPPUNIT_TEST(testManualKashida);
CPPUNIT_TEST(testFinalYeh);
CPPUNIT_TEST(testNoZwnjExpansion);
+ CPPUNIT_TEST(testExcludeInvalid);
CPPUNIT_TEST_SUITE_END();
};
@@ -54,6 +58,14 @@ void KashidaTest::testCharacteristic()
CPPUNIT_ASSERT_EQUAL(sal_Int32(3), GetWordKashidaPosition(u"تمثیل"_ustr).value().nIndex);
}
+void KashidaTest::testManualKashida()
+{
+ CPPUNIT_ASSERT_EQUAL(sal_Int32(2), GetWordKashidaPosition(u"برـای"_ustr).value().nIndex);
+
+ // Normally, a kashida would not be inserted after a final Yeh.
+ CPPUNIT_ASSERT_EQUAL(sal_Int32(4), GetWordKashidaPosition(u"نیمِـي"_ustr).value().nIndex);
+}
+
// tdf#65344: Do not insert kashida before a final Yeh
void KashidaTest::testFinalYeh()
{
@@ -73,6 +85,32 @@ void KashidaTest::testNoZwnjExpansion()
CPPUNIT_ASSERT(!GetWordKashidaPosition(u"مت\u200Cن"_ustr).has_value());
}
+// tdf#163105: Do not insert kashida if the position is invalid
+void KashidaTest::testExcludeInvalid()
+{
+ std::vector<bool> aValid;
+ aValid.resize(5, true);
+
+ CPPUNIT_ASSERT_EQUAL(sal_Int32(3),
+ GetWordKashidaPosition(u"نویسه"_ustr, aValid).value().nIndex);
+
+ aValid[3] = false;
+ CPPUNIT_ASSERT_EQUAL(sal_Int32(0),
+ GetWordKashidaPosition(u"نویسه"_ustr, aValid).value().nIndex);
+
+ // Calls after this use the last resort (positions in aValid from end to start)
+ aValid[0] = false;
+ CPPUNIT_ASSERT_EQUAL(sal_Int32(2),
+ GetWordKashidaPosition(u"نویسه"_ustr, aValid).value().nIndex);
+
+ aValid[2] = false;
+ CPPUNIT_ASSERT_EQUAL(sal_Int32(1),
+ GetWordKashidaPosition(u"نویسه"_ustr, aValid).value().nIndex);
+
+ aValid[1] = false;
+ CPPUNIT_ASSERT(!GetWordKashidaPosition(u"نویسه"_ustr, aValid).has_value());
+}
+
CPPUNIT_TEST_SUITE_REGISTRATION(KashidaTest);
}
diff --git a/i18nutil/source/utility/kashida.cxx b/i18nutil/source/utility/kashida.cxx
index d016e96294fb..6a6c7adde690 100644
--- a/i18nutil/source/utility/kashida.cxx
+++ b/i18nutil/source/utility/kashida.cxx
@@ -46,6 +46,7 @@ namespace
- tdf#65344: Kashida must not be inserted before the final form of Yeh, unless
preceded by an initial or medial Seen.
+ - tdf#163105: As a last resort, use the last valid insertion position from VCL.
*/
#define IS_JOINING_GROUP(c, g) (u_getIntPropertyValue((c), UCHAR_JOINING_GROUP) == U_JG_##g)
@@ -134,7 +135,8 @@ bool CanConnectToPrev(sal_Unicode cCh, sal_Unicode cPrevCh)
}
}
-std::optional<i18nutil::KashidaPosition> i18nutil::GetWordKashidaPosition(const OUString& rWord)
+std::optional<i18nutil::KashidaPosition>
+i18nutil::GetWordKashidaPosition(const OUString& rWord, const std::vector<bool>& pValidPositions)
{
sal_Int32 nIdx = 0;
sal_Int32 nPrevIdx = 0;
@@ -142,35 +144,45 @@ std::optional<i18nutil::KashidaPosition> i18nutil::GetWordKashidaPosition(const
sal_Unicode cCh = 0;
sal_Unicode cPrevCh = 0;
- int nPriorityLevel = 7; // 0..6 = level found, 7 not found
+ int nPriorityLevel = 8; // 0..7 = level found, 8 not found
sal_Int32 nWordLen = rWord.getLength();
+ SAL_WARN_IF(!pValidPositions.empty() && pValidPositions.size() != static_cast<size_t>(nWordLen),
+ "i18n", "Kashida valid position array wrong size");
+
// ignore trailing vowel chars
while (nWordLen && isTransparentChar(rWord[nWordLen - 1]))
{
--nWordLen;
}
- auto fnTryInsertBefore = [&rWord, &nIdx, &nPrevIdx, &nKashidaPos, &nPriorityLevel,
- &nWordLen](sal_Int32 nNewPriority, bool bIgnoreFinalYeh = false) {
- // Exclusions:
-
- // #i98410#: prevent ZWNJ expansion
- if (rWord[nPrevIdx] == 0x200C || rWord[nPrevIdx + 1] == 0x200C)
- {
- return;
- }
-
- // tdf#65344: Do not insert kashida before a final Yeh
- if (!bIgnoreFinalYeh && nIdx == (nWordLen - 1) && isYehChar(rWord[nIdx]))
- {
- return;
- }
-
- nKashidaPos = nPrevIdx;
- nPriorityLevel = nNewPriority;
- };
+ auto fnTryInsertBefore
+ = [&rWord, &nIdx, &nPrevIdx, &nKashidaPos, &nPriorityLevel, &nWordLen,
+ &pValidPositions](sal_Int32 nNewPriority, bool bIgnoreFinalYeh = false) {
+ // Exclusions:
+
+ // tdf#163105: Do not insert kashida if the position is invalid
+ if (!pValidPositions.empty() && !pValidPositions[nPrevIdx])
+ {
+ return;
+ }
+
+ // #i98410#: prevent ZWNJ expansion
+ if (rWord[nPrevIdx] == 0x200C || rWord[nPrevIdx + 1] == 0x200C)
+ {
+ return;
+ }
+
+ // tdf#65344: Do not insert kashida before a final Yeh
+ if (!bIgnoreFinalYeh && nIdx == (nWordLen - 1) && isYehChar(rWord[nIdx]))
+ {
+ return;
+ }
+
+ nKashidaPos = nPrevIdx;
+ nPriorityLevel = nNewPriority;
+ };
while (nIdx < nWordLen)
{
@@ -270,7 +282,7 @@ std::optional<i18nutil::KashidaPosition> i18nutil::GetWordKashidaPosition(const
}
}
- // other connecting possibilities
+ // 7. Other connecting possibilities
if (nPriorityLevel >= 6 && nIdx > 0)
{
// Reh, Zain (right joining) final form may appear in the middle of word
@@ -286,6 +298,12 @@ std::optional<i18nutil::KashidaPosition> i18nutil::GetWordKashidaPosition(const
}
}
+ // 8. If valid position data exists, use the last legal position
+ if (nPriorityLevel >= 7 && nIdx > 0 && !pValidPositions.empty())
+ {
+ fnTryInsertBefore(7);
+ }
+
// Do not consider vowel marks when checking if a character
// can be connected to previous character.
if (!isTransparentChar(cCh))
diff --git a/include/i18nutil/kashida.hxx b/include/i18nutil/kashida.hxx
index 54797143143c..96969ff89197 100644
--- a/include/i18nutil/kashida.hxx
+++ b/include/i18nutil/kashida.hxx
@@ -10,6 +10,7 @@
#include <i18nutil/i18nutildllapi.h>
#include <rtl/ustring.hxx>
#include <optional>
+#include <vector>
namespace i18nutil
{
@@ -18,7 +19,8 @@ struct KashidaPosition
sal_Int32 nIndex;
};
-I18NUTIL_DLLPUBLIC std::optional<KashidaPosition> GetWordKashidaPosition(const OUString& rWord);
+I18NUTIL_DLLPUBLIC std::optional<KashidaPosition>
+GetWordKashidaPosition(const OUString& rWord, const std::vector<bool>& pValidPositions = {});
}
/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
diff --git a/include/vcl/outdev.hxx b/include/vcl/outdev.hxx
index 8d0d0c0bf5a9..3f534c50b29a 100644
--- a/include/vcl/outdev.hxx
+++ b/include/vcl/outdev.hxx
@@ -1171,6 +1171,9 @@ public:
std::vector<sal_Int32>* pKashidaPosDropped // invalid kashida positions (out)
) const;
+ // tdf#163105: Get map of valid kashida positions for a single word
+ void GetWordKashidaPositions(const OUString& rText, std::vector<bool>* pOutMap) const;
+
static void BeginFontSubstitution();
static void EndFontSubstitution();
static void AddFontSubstitute( const OUString& rFontName,
diff --git a/sw/source/core/inc/scriptinfo.hxx b/sw/source/core/inc/scriptinfo.hxx
index 4f6933c520d4..ae37779b6f71 100644
--- a/sw/source/core/inc/scriptinfo.hxx
+++ b/sw/source/core/inc/scriptinfo.hxx
@@ -322,6 +322,14 @@ public:
void GetKashidaPositions(TextFrameIndex nStt, TextFrameIndex nLen,
std::vector<TextFrameIndex>& rKashidaPosition);
+/** replaces kashida opportunities for a given text range.
+
+ rKashidaPositions: buffer containing char indices of the
+ kashida opportunities relative to the paragraph
+*/
+ void ReplaceKashidaPositions(TextFrameIndex nStt, TextFrameIndex nEnd,
+ const std::vector<TextFrameIndex>& rKashidaPositions);
+
/** Use regular blank justification instead of kashdida justification for the given line of text.
nStt Start char index of the line referring to the paragraph.
nLen Number of characters in the line
diff --git a/sw/source/core/text/itradj.cxx b/sw/source/core/text/itradj.cxx
index 0ead2534d8b4..1a22579c24f1 100644
--- a/sw/source/core/text/itradj.cxx
+++ b/sw/source/core/text/itradj.cxx
@@ -20,6 +20,9 @@
#include <sal/config.h>
#include <o3tl/safeint.hxx>
+#include <com/sun/star/i18n/WordType.hpp>
+#include <swscanner.hxx>
+#include <i18nutil/kashida.hxx>
#include <IDocumentSettingAccess.hxx>
#include <doc.hxx>
@@ -135,13 +138,64 @@ void SwTextAdjuster::FormatBlock( )
GetInfo().GetParaPortion()->GetRepaint().SetOffset(0);
}
-static bool lcl_CheckKashidaPositions( SwScriptInfo& rSI, SwTextSizeInfo& rInf, SwTextIter& rItr,
- sal_Int32& rKashidas, TextFrameIndex& nGluePortion)
+static bool lcl_CheckKashidaPositions(SwScriptInfo& rSI, SwTextSizeInfo& rInf, SwTextIter& rItr,
+ sal_Int32& rKashidas, TextFrameIndex& nGluePortion,
+ bool& rRemovedAllKashida)
{
+ rRemovedAllKashida = true;
+
// i60594 validate Kashida justification
TextFrameIndex nIdx = rItr.GetStart();
TextFrameIndex nEnd = rItr.GetEnd();
+ // Get the initial kashida position set, for invalidation
+ std::vector<TextFrameIndex> aOldKashidaPositions;
+ rSI.GetKashidaPositions(nIdx, rItr.GetLength(), aOldKashidaPositions);
+
+ std::vector<TextFrameIndex> aNewKashidaPositions;
+ std::vector<bool> aValidPositions;
+
+ // Reparse the text, and reapply the kashida insertion rules
+ std::function<LanguageType(sal_Int32, sal_Int32, bool)> const pGetLangOfChar(
+ [&rInf](sal_Int32 const nBegin, sal_uInt16 const nScript, bool const bNoChar)
+ { return rInf.GetTextFrame()->GetLangOfChar(TextFrameIndex{ nBegin }, nScript, bNoChar); });
+ SwScanner aScanner(pGetLangOfChar, rInf.GetText(), nullptr, ModelToViewHelper(),
+ i18n::WordType::DICTIONARY_WORD, sal_Int32(nIdx), sal_Int32(nEnd));
+
+ while (aScanner.NextWord())
+ {
+ const OUString& rWord = aScanner.GetWord();
+
+ // Fetch the set of valid positions from VCL, where possible
+ aValidPositions.clear();
+ if ( SwScriptInfo::IsArabicText( rInf.GetText(), TextFrameIndex{aScanner.GetBegin()}, TextFrameIndex{aScanner.GetLen()} ) )
+ {
+ rItr.SeekAndChgAttrIter(TextFrameIndex{ aScanner.GetBegin() }, rInf.GetRefDev());
+
+ vcl::text::ComplexTextLayoutFlags nOldLayout = rInf.GetRefDev()->GetLayoutMode();
+ rInf.GetRefDev()->SetLayoutMode(nOldLayout | vcl::text::ComplexTextLayoutFlags::BiDiRtl);
+
+ rInf.GetRefDev()->GetWordKashidaPositions(rWord, &aValidPositions);
+
+ rInf.GetRefDev()->SetLayoutMode(nOldLayout);
+ }
+
+ auto stKashidaPos = i18nutil::GetWordKashidaPosition(rWord, aValidPositions);
+ if (stKashidaPos.has_value())
+ {
+ TextFrameIndex nNewKashidaPos{ aScanner.GetBegin() + stKashidaPos->nIndex };
+ aNewKashidaPositions.push_back(nNewKashidaPos);
+ }
+ }
+
+ if (aOldKashidaPositions != aNewKashidaPositions)
+ {
+ // Kashida positions have changed; restart CalcNewBlock
+ rSI.ReplaceKashidaPositions(nIdx, nEnd, aNewKashidaPositions);
+ rRemovedAllKashida = aNewKashidaPositions.empty();
+ return false;
+ }
+
// Note on calling KashidaJustify():
// Kashida positions may be marked as invalid. Therefore KashidaJustify may return the clean
// total number of kashida positions, or the number of kashida positions after some positions
@@ -154,12 +208,10 @@ static bool lcl_CheckKashidaPositions( SwScriptInfo& rSI, SwTextSizeInfo& rInf,
// kashida positions found in SwScriptInfo are not necessarily valid in every font
// if two characters are replaced by a ligature glyph, there will be no place for a kashida
- std::vector<TextFrameIndex> aUncastKashidaPos;
- rSI.GetKashidaPositions(nIdx, rItr.GetLength(), aUncastKashidaPos);
- assert(aUncastKashidaPos.size() >= o3tl::make_unsigned(rKashidas));
+ assert(aNewKashidaPositions.size() >= o3tl::make_unsigned(rKashidas));
std::vector<sal_Int32> aKashidaPos;
- std::transform(std::cbegin(aUncastKashidaPos), std::cend(aUncastKashidaPos),
+ std::transform(std::cbegin(aNewKashidaPositions), std::cend(aNewKashidaPositions),
std::back_inserter(aKashidaPos),
[](TextFrameIndex nPos) { return static_cast<sal_Int32>(nPos); });
@@ -404,13 +456,15 @@ void SwTextAdjuster::CalcNewBlock( SwLineLayout *pCurrent,
{
// kashida positions found in SwScriptInfo are not necessarily valid in every font
// if two characters are replaced by a ligature glyph, there will be no place for a kashida
- if ( !lcl_CheckKashidaPositions ( rSI, aInf, aItr, nKashidas, nGluePortion ))
+ bool bRemovedAllKashida = false;
+ if (!lcl_CheckKashidaPositions(rSI, aInf, aItr, nKashidas, nGluePortion,
+ bRemovedAllKashida))
{
// all kashida positions are invalid
// do regular blank justification
pCurrent->FinishSpaceAdd();
GetInfo().SetIdx( m_nStart );
- CalcNewBlock( pCurrent, pStopAt, nReal, true );
+ CalcNewBlock(pCurrent, pStopAt, nReal, bRemovedAllKashida);
return;
}
}
diff --git a/sw/source/core/text/porlay.cxx b/sw/source/core/text/porlay.cxx
index 8574f6d31d12..c18969fc709c 100644
--- a/sw/source/core/text/porlay.cxx
+++ b/sw/source/core/text/porlay.cxx
@@ -2314,6 +2314,27 @@ void SwScriptInfo::GetKashidaPositions(
}
}
+void SwScriptInfo::ReplaceKashidaPositions(TextFrameIndex const nStt, TextFrameIndex const nEnd,
+ const std::vector<TextFrameIndex>& rKashidaPositions)
+{
+ auto it = m_Kashida.begin();
+ while (it != m_Kashida.end() && *it < nStt)
+ {
+ ++it;
+ }
+
+ it = m_Kashida.insert(it, rKashidaPositions.begin(), rKashidaPositions.end());
+
+ it += rKashidaPositions.size();
+ auto jt = it;
+ while (jt != m_Kashida.end() && *jt < nEnd)
+ {
+ ++jt;
+ }
+
+ m_Kashida.erase(it, jt);
+}
+
void SwScriptInfo::SetNoKashidaLine(TextFrameIndex const nStt, TextFrameIndex const nLen)
{
m_NoKashidaLine.push_back( nStt );
diff --git a/vcl/qa/cppunit/pdfexport/data/tdf163105-editeng.fodt b/vcl/qa/cppunit/pdfexport/data/tdf163105-editeng.fodt
new file mode 100644
index 000000000000..55c7fa2ba93e
--- /dev/null
+++ b/vcl/qa/cppunit/pdfexport/data/tdf163105-editeng.fodt
@@ -0,0 +1,131 @@
+<?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:calcext="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.3" office:mimetype="application/vnd.oasis.opendocument.text">
+ <office:meta><meta:creation-date>2024-09-25T22:51:39.180592512</meta:creation-date><dc:date>2024-09-25T22:52:41.682098885</dc:date><meta:editing-duration>PT1M3S</meta:editing-duration><meta:editing-cycles>1</meta:editing-cycles><meta:document-statistic meta:table-count="0" meta:image-count="0" meta:object-count="0" meta:page-count="1" meta:paragraph-count="0" meta:word-count="0" meta:character-count="0" meta:non-whitespace-character-count="0"/><meta:generator>LibreOfficeDev/25.2.0.0.alpha0$Linux_X86_64 LibreOffice_project/8493389ffaaa809c9feb77622a5bd695dbb76e43</meta:generator></office:meta>
+ <office:font-face-decls>
+ <style:font-face style:name="Amiri" svg:font-family="Amiri" style:font-adornments="Regular" style:font-pitch="variable"/>
+ <style:font-face style:name="Liberation Serif" svg:font-family="'Liberation Serif'" style:font-family-generic="roman" style:font-pitch="variable"/>
+ <style:font-face style:name="Noto Sans" svg:font-family="'Noto Sans'" style:font-family-generic="swiss"/>
+ <style:font-face style:name="Noto Serif CJK SC" svg:font-family="'Noto Serif CJK SC'" style:font-family-generic="system" style:font-pitch="variable"/>
+ </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.1181in" draw:shadow-offset-y="0.1181in" draw:start-line-spacing-horizontal="0.1114in" draw:start-line-spacing-vertical="0.1114in" draw:end-line-spacing-horizontal="0.1114in" draw:end-line-spacing-vertical="0.1114in" style:flow-with-text="false"/>
+ <style:paragraph-properties style:text-autospace="ideograph-alpha" style:line-break="strict" loext:tab-stop-distance="0in" 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="Noto Sans" style:font-size-complex="12pt" style:language-complex="ar" style:country-complex="SA"/>
+ </style:default-style>
+ <style:default-style style:family="paragraph">
+ <style:paragraph-properties fo:orphans="2" fo:widows="2" fo:hyphenation-ladder-count="no-limit" fo:hyphenation-keep="auto" loext:hyphenation-keep-type="column" style:text-autospace="ideograph-alpha" style:punctuation-wrap="hanging" style:line-break="strict" style:tab-stop-distance="0.4925in" 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" 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="Noto Sans" style:font-size-complex="12pt" style:language-complex="ar" style:country-complex="SA" 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="5" 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:text-properties style:font-name-complex="Amiri" style:font-family-complex="Amiri" style:font-style-name-complex="Regular" style:font-pitch-complex="variable"/>
+ </style:style>
+ <text:outline-style style:name="Outline">
+ <text:outline-level-style text:level="1" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ <text:outline-level-style text:level="2" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ <text:outline-level-style text:level="3" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ <text:outline-level-style text:level="4" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ <text:outline-level-style text:level="5" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ <text:outline-level-style text:level="6" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ <text:outline-level-style text:level="7" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ <text:outline-level-style text:level="8" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ <text:outline-level-style text:level="9" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ <text:outline-level-style text:level="10" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ </text:outline-style>
+ <text:notes-configuration text:note-class="footnote" style:num-format="1" text:start-value="0" text:footnotes-position="page" text:start-numbering-at="document"/>
+ <text:notes-configuration text:note-class="endnote" style:num-format="i" text:start-value="0"/>
+ <text:linenumbering-configuration text:number-lines="false" text:offset="0.1965in" style:num-format="1" text:number-position="left" text:increment="5"/>
+ </office:styles>
+ <office:automatic-styles>
+ <style:style style:name="P1" style:family="paragraph">
+ <style:paragraph-properties fo:text-align="justify" style:writing-mode="rl-tb"/>
+ <style:text-properties style:font-family-complex="Amiri" style:font-pitch-complex="variable"/>
+ </style:style>
+ <style:style style:name="P2" style:family="paragraph">
+ <loext:graphic-properties draw:fill="none" draw:fill-color="#ffffff"/>
+ <style:paragraph-properties fo:text-align="end"/>
+ <style:text-properties style:font-family-complex="Amiri" style:font-pitch-complex="variable"/>
+ </style:style>
+ <style:style style:name="T1" style:family="text">
+ <style:text-properties style:font-family-complex="Amiri" style:font-pitch-complex="variable"/>
+ </style:style>
+ <style:style style:name="gr1" style:family="graphic">
+ <style:graphic-properties draw:stroke="none" svg:stroke-color="#000000" draw:fill="none" draw:fill-color="#ffffff" fo:min-height="2.1965in" loext:decorative="false" style:run-through="foreground" style:wrap="run-through" style:number-wrapped-paragraphs="no-limit" style:vertical-pos="from-top" style:vertical-rel="paragraph" style:horizontal-pos="from-left" style:horizontal-rel="paragraph"/>
+ <style:paragraph-properties style:writing-mode="lr-tb"/>
+ </style:style>
+ <style:page-layout style:name="pm1">
+ <style:page-layout-properties fo:page-width="8.2681in" fo:page-height="11.6929in" style:num-format="1" style:print-orientation="portrait" fo:margin-top="0.7874in" fo:margin-bottom="0.7874in" fo:margin-left="0.7874in" fo:margin-right="0.7874in" style:writing-mode="lr-tb" style:footnote-max-height="0in" loext:margin-gutter="0in">
+ <style:footnote-sep style:width="0.0071in" style:distance-before-sep="0.0398in" style:distance-after-sep="0.0398in" 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>
+ </office:automatic-styles>
+ <office:master-styles>
+ <style:master-page style:name="Standard" style:page-layout-name="pm1"/>
+ </office:master-styles>
+ <office:body>
+ <office:text>
+ <text:sequence-decls>
+ <text:sequence-decl text:display-outline-level="0" text:name="Illustration"/>
+ <text:sequence-decl text:display-outline-level="0" text:name="Table"/>
+ <text:sequence-decl text:display-outline-level="0" text:name="Text"/>
+ <text:sequence-decl text:display-outline-level="0" text:name="Drawing"/>
+ <text:sequence-decl text:display-outline-level="0" text:name="Figure"/>
+ </text:sequence-decls>
+ <text:p text:style-name="Standard"><draw:frame text:anchor-type="paragraph" draw:z-index="0" draw:name="Text Frame 1" draw:style-name="gr1" draw:text-style-name="P2" svg:width="6.4843in" svg:height="2.1969in" svg:x="0.0917in" svg:y="0.1091in">
+ <draw:text-box>
+ <text:p text:style-name="P1"><text:span text:style-name="T1">متن فارسی</text:span><text:span text:style-name="T1"><text:line-break/></text:span><text:span text:style-name="T1"/></text:p>
+ </draw:text-box>
+ </draw:frame></text:p>
+ </office:text>
+ </office:body>
+</office:document> \ No newline at end of file
diff --git a/vcl/qa/cppunit/pdfexport/data/tdf163105.fodt b/vcl/qa/cppunit/pdfexport/data/tdf163105-kashida-spaces.fodt
index eae83caedc1d..eae83caedc1d 100644
--- a/vcl/qa/cppunit/pdfexport/data/tdf163105.fodt
+++ b/vcl/qa/cppunit/pdfexport/data/tdf163105-kashida-spaces.fodt
diff --git a/vcl/qa/cppunit/pdfexport/data/tdf163105-writer.fodt b/vcl/qa/cppunit/pdfexport/data/tdf163105-writer.fodt
new file mode 100644
index 000000000000..8766d411fcd6
--- /dev/null
+++ b/vcl/qa/cppunit/pdfexport/data/tdf163105-writer.fodt
@@ -0,0 +1,114 @@
+<?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:calcext="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.3" office:mimetype="application/vnd.oasis.opendocument.text">
+ <office:meta><meta:creation-date>2024-09-25T22:50:47.128978128</meta:creation-date><dc:date>2024-09-25T22:51:34.458596131</dc:date><meta:editing-duration>PT48S</meta:editing-duration><meta:editing-cycles>1</meta:editing-cycles><meta:document-statistic meta:table-count="0" meta:image-count="0" meta:object-count="0" meta:page-count="1" meta:paragraph-count="1" meta:word-count="2" meta:character-count="10" meta:non-whitespace-character-count="8"/><meta:generator>LibreOfficeDev/25.2.0.0.alpha0$Linux_X86_64 LibreOffice_project/8493389ffaaa809c9feb77622a5bd695dbb76e43</meta:generator></office:meta>
+ <office:font-face-decls>
+ <style:font-face style:name="Amiri" svg:font-family="Amiri" style:font-adornments="Regular" style:font-pitch="variable"/>
+ <style:font-face style:name="Liberation Serif" svg:font-family="'Liberation Serif'" style:font-family-generic="roman" style:font-pitch="variable"/>
+ <style:font-face style:name="Noto Sans" svg:font-family="'Noto Sans'" style:font-family-generic="swiss"/>
+ <style:font-face style:name="Noto Serif CJK SC" svg:font-family="'Noto Serif CJK SC'" style:font-family-generic="system" style:font-pitch="variable"/>
+ </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.1181in" draw:shadow-offset-y="0.1181in" draw:start-line-spacing-horizontal="0.1114in" draw:start-line-spacing-vertical="0.1114in" draw:end-line-spacing-horizontal="0.1114in" draw:end-line-spacing-vertical="0.1114in" style:flow-with-text="false"/>
+ <style:paragraph-properties style:text-autospace="ideograph-alpha" style:line-break="strict" loext:tab-stop-distance="0in" 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="Noto Sans" style:font-size-complex="12pt" style:language-complex="ar" style:country-complex="SA"/>
+ </style:default-style>
+ <style:default-style style:family="paragraph">
+ <style:paragraph-properties fo:orphans="2" fo:widows="2" fo:hyphenation-ladder-count="no-limit" fo:hyphenation-keep="auto" loext:hyphenation-keep-type="column" style:text-autospace="ideograph-alpha" style:punctuation-wrap="hanging" style:line-break="strict" style:tab-stop-distance="0.4925in" 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" 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="Noto Sans" style:font-size-complex="12pt" style:language-complex="ar" style:country-complex="SA" 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="5" 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:text-properties style:font-name-complex="Amiri" style:font-family-complex="Amiri" style:font-style-name-complex="Regular" style:font-pitch-complex="variable"/>
+ </style:style>
+ <text:outline-style style:name="Outline">
+ <text:outline-level-style text:level="1" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ <text:outline-level-style text:level="2" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ <text:outline-level-style text:level="3" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ <text:outline-level-style text:level="4" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ <text:outline-level-style text:level="5" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ <text:outline-level-style text:level="6" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ <text:outline-level-style text:level="7" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ <text:outline-level-style text:level="8" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ <text:outline-level-style text:level="9" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ <text:outline-level-style text:level="10" style:num-format="">
+ <style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
+ <style:list-level-label-alignment text:label-followed-by="listtab"/>
+ </style:list-level-properties>
+ </text:outline-level-style>
+ </text:outline-style>
+ <text:notes-configuration text:note-class="footnote" style:num-format="1" text:start-value="0" text:footnotes-position="page" text:start-numbering-at="document"/>
+ <text:notes-configuration text:note-class="endnote" style:num-format="i" text:start-value="0"/>
+ <text:linenumbering-configuration text:number-lines="false" text:offset="0.1965in" style:num-format="1" text:number-position="left" text:increment="5"/>
+ </office:styles>
+ <office:automatic-styles>
+ <style:style style:name="P1" style:family="paragraph" style:parent-style-name="Standard">
+ <style:paragraph-properties fo:text-align="justify" style:justify-single-word="false" style:writing-mode="rl-tb"/>
+ </style:style>
+ <style:page-layout style:name="pm1">
+ <style:page-layout-properties fo:page-width="8.2681in" fo:page-height="11.6929in" style:num-format="1" style:print-orientation="portrait" fo:margin-top="0.7874in" fo:margin-bottom="0.7874in" fo:margin-left="0.7874in" fo:margin-right="0.7874in" style:writing-mode="lr-tb" style:footnote-max-height="0in" loext:margin-gutter="0in">
+ <style:footnote-sep style:width="0.0071in" style:distance-before-sep="0.0398in" style:distance-after-sep="0.0398in" 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>
+ </office:automatic-styles>
+ <office:master-styles>
+ <style:master-page style:name="Standard" style:page-layout-name="pm1"/>
+ </office:master-styles>
+ <office:body>
+ <office:text>
+ <text:sequence-decls>
+ <text:sequence-decl text:display-outline-level="0" text:name="Illustration"/>
+ <text:sequence-decl text:display-outline-level="0" text:name="Table"/>
+ <text:sequence-decl text:display-outline-level="0" text:name="Text"/>
+ <text:sequence-decl text:display-outline-level="0" text:name="Drawing"/>
+ <text:sequence-decl text:display-outline-level="0" text:name="Figure"/>
+ </text:sequence-decls>
+ <text:p text:style-name="P1">متن فارسی<text:line-break/></text:p>
+ </office:text>
+ </office:body>
+</office:document> \ No newline at end of file
diff --git a/vcl/qa/cppunit/pdfexport/pdfexport2.cxx b/vcl/qa/cppunit/pdfexport/pdfexport2.cxx
index 390dbc203438..de387cd31850 100644
--- a/vcl/qa/cppunit/pdfexport/pdfexport2.cxx
+++ b/vcl/qa/cppunit/pdfexport/pdfexport2.cxx
@@ -5627,7 +5627,7 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf151748KashidaSpace)
// tdf#163105 - Writer kashida justification should expand spaces
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf163105SwKashidaSpaceExpansion)
{
- saveAsPDF(u"tdf163105.fodt");
+ saveAsPDF(u"tdf163105-kashida-spaces.fodt");
auto pPdfDocument = parsePDFExport();
CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());
@@ -5668,6 +5668,99 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf163105SwKashidaSpaceExpansion)
CPPUNIT_ASSERT_GREATER(150.0, aRect.at(2).getWidth());
}
+// tdf#163105 - Writer should use font information when choosing kashida positions
+CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf163105Writer)
+{
+ saveAsPDF(u"tdf163105-writer.fodt");
+
+ auto pPdfDocument = parsePDFExport();
+ CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());
+
+ auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
+ CPPUNIT_ASSERT(pPdfPage);
+ auto pTextPage = pPdfPage->getTextPage();
+ CPPUNIT_ASSERT(pTextPage);
+
+ int nPageObjectCount = pPdfPage->getObjectCount();
+
+ // The fix allows kashida justification in this document.
+ // Without the fix, this will be 1.
+ CPPUNIT_ASSERT_EQUAL(5, nPageObjectCount);
+
+ std::vector<OUString> aText;
+ std::vector<basegfx::B2DRectangle> aRect;
+
+ int nTextObjectCount = 0;
+ for (int i = 0; i < nPageObjectCount; ++i)
+ {
+ auto pPageObject = pPdfPage->getObject(i);
+ CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
+ if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
+ {
+ aText.push_back(pPageObject->getText(pTextPage));
+ aRect.push_back(pPageObject->getBounds());
+ ++nTextObjectCount;
+ }
+ }
+
+ CPPUNIT_ASSERT_EQUAL(5, nTextObjectCount);
+
+ CPPUNIT_ASSERT_EQUAL(u"ارسی"_ustr, aText.at(0).trim());
+ CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(1).trim());
+ CPPUNIT_ASSERT_EQUAL(u"تن ف"_ustr, aText.at(2).trim()); // This span is whitespace justified
+ CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(3).trim());
+ CPPUNIT_ASSERT_EQUAL(u"م"_ustr, aText.at(4).trim());
+
+ // Without the fix, this will be greater than X
+ CPPUNIT_ASSERT_LESS(170.0, aRect.at(2).getWidth());
+}
+
+// tdf#163105 - Edit Engine should use font information when choosing kashida positions
+CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf163105Editeng)
+{
+ saveAsPDF(u"tdf163105-editeng.fodt");
+
+ auto pPdfDocument = parsePDFExport();
+ CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());
+
+ auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
+ CPPUNIT_ASSERT(pPdfPage);
+ auto pTextPage = pPdfPage->getTextPage();
+ CPPUNIT_ASSERT(pTextPage);
+
+ int nPageObjectCount = pPdfPage->getObjectCount();
+
+ // The fix allows kashida justification in this document.
+ // Without the fix, this will be 1.
+ CPPUNIT_ASSERT_EQUAL(5, nPageObjectCount);
+
+ std::vector<OUString> aText;
+ std::vector<basegfx::B2DRectangle> aRect;
+
+ int nTextObjectCount = 0;
+ for (int i = 0; i < nPageObjectCount; ++i)
+ {
+ auto pPageObject = pPdfPage->getObject(i);
+ CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
+ if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
+ {
+ aText.push_back(pPageObject->getText(pTextPage));
+ aRect.push_back(pPageObject->getBounds());
+ ++nTextObjectCount;
+ }
+ }
+
+ CPPUNIT_ASSERT_EQUAL(5, nTextObjectCount);
+
+ CPPUNIT_ASSERT_EQUAL(u"ارسی"_ustr, aText.at(0).trim());
+ CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(1).trim());
+ CPPUNIT_ASSERT_EQUAL(u"تن ف"_ustr, aText.at(2).trim()); // This span is whitespace justified
+ CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(3).trim());
+ CPPUNIT_ASSERT_EQUAL(u"م"_ustr, aText.at(4).trim());
+
+ CPPUNIT_ASSERT_LESS(170.0, aRect.at(2).getWidth());
+}
+
} // end anonymous namespace
CPPUNIT_PLUGIN_IMPLEMENT();
diff --git a/vcl/source/outdev/font.cxx b/vcl/source/outdev/font.cxx
index 2412ac17f415..a2f32327f72c 100644
--- a/vcl/source/outdev/font.cxx
+++ b/vcl/source/outdev/font.cxx
@@ -1214,6 +1214,35 @@ sal_Int32 OutputDevice::ValidateKashidas(const OUString& rTxt, sal_Int32 nIdx, s
return nDropped;
}
+// tdf#163105: Get map of valid kashida positions for a single word
+void OutputDevice::GetWordKashidaPositions(const OUString& rText,
+ std::vector<bool>* pOutMap) const
+{
+ pOutMap->clear();
+
+ auto nEnd = rText.getLength();
+
+ // do layout
+ std::unique_ptr<SalLayout> pSalLayout = ImplLayout(rText, 0, nEnd);
+ if (!pSalLayout)
+ return;
+
+ pOutMap->resize(nEnd, false);
+ for (sal_Int32 i = 0; i < nEnd; ++i)
+ {
+ auto nNextPos = i + 1;
+
+ // Skip combining marks to find the next character after this position.
+ while (nNextPos < nEnd
+ && u_getIntPropertyValue(rText[nNextPos], UCHAR_JOINING_TYPE) == U_JT_TRANSPARENT)
+ {
+ ++nNextPos;
+ }
+
+ pOutMap->at(i) = pSalLayout->IsKashidaPosValid(i, nNextPos);
+ }
+}
+
bool OutputDevice::GetGlyphBoundRects( const Point& rOrigin, const OUString& rStr,
int nIndex, int nLen, std::vector< tools::Rectangle >& rVector ) const
{