editeng/source/editeng/impedit3.cxx                  |   14 +-
 i18nutil/qa/cppunit/test_kashida.cxx                 |   38 +++++
 i18nutil/source/utility/kashida.cxx                  |   62 +++++---
 include/i18nutil/kashida.hxx                         |    4 
 include/vcl/outdev.hxx                               |    3 
 sw/source/core/inc/scriptinfo.hxx                    |    8 +
 sw/source/core/text/itradj.cxx                       |   70 ++++++++--
 sw/source/core/text/porlay.cxx                       |   21 +++
 vcl/qa/cppunit/pdfexport/data/tdf163105-editeng.fodt |  131 +++++++++++++++++++
 vcl/qa/cppunit/pdfexport/data/tdf163105-writer.fodt  |  114 ++++++++++++++++
 vcl/qa/cppunit/pdfexport/pdfexport2.cxx              |   95 +++++++++++++
 vcl/source/outdev/font.cxx                           |   29 ++++
 12 files changed, 553 insertions(+), 36 deletions(-)

New commits:
commit d8f430e4bef414616fd80bbf4ea16d767991b5b9
Author:     Jonathan Clark <jonat...@libreoffice.org>
AuthorDate: Wed Sep 25 16:07:06 2024 -0600
Commit:     Jonathan Clark <jonat...@libreoffice.org>
CommitDate: Thu Sep 26 22:44:03 2024 +0200

    tdf#163105 Use HB data while selecting kashida insertion positions
    
    Previously, Writer and Edit Engine would skip inserting kashida in words
    if the highest-priority candidate position is marked as invalid by
    HarfBuzz. This would happen even if the word contained multiple
    lower-ranked valid candidate positions.
    
    This change updates Writer and Edit Engine to pass HarfBuzz kashida
    position data to the selection algorithm. The algorithm has been updated
    to return the highest-priority valid position, if any. The algorithm has
    also been updated to use raw positions marked as valid by HarfBuzz as a
    fallback, if no better positions could be found.
    
    Change-Id: I40c6432c4607aee197e8767e5667db504469956a
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/173963
    Tested-by: Jenkins
    Reviewed-by: Jonathan Clark <jonat...@libreoffice.org>

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: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.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
similarity index 100%
rename from vcl/qa/cppunit/pdfexport/data/tdf163105.fodt
rename to 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: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.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
 {

Reply via email to