vcl/qa/cppunit/complextext.cxx     |   91 +++++++++++++++++++++++++++++++++++++
 vcl/source/gdi/CommonSalLayout.cxx |   54 ++++++++++++++++++---
 2 files changed, 137 insertions(+), 8 deletions(-)

New commits:
commit bdbbdc9b931d35f6d7d816512ac5be599295dc80
Author:     Khaled Hosny <kha...@aliftype.com>
AuthorDate: Sun Aug 28 06:59:06 2022 +0200
Commit:     خالد حسني <kha...@aliftype.com>
CommitDate: Wed Aug 31 08:46:25 2022 +0200

    tdf#30731: Use ligature caret positions from the font
    
    When ligature caret positions from the font are available, use them for
    more accurate caret positions instead of evenly distributing the glyph
    width over grapheme clusters.
    
    Change-Id: I0ecfa35e1fff2b264b105182a4b29b2ebd033093
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/138955
    Tested-by: Jenkins
    Reviewed-by: خالد حسني <kha...@aliftype.com>

diff --git a/vcl/qa/cppunit/complextext.cxx b/vcl/qa/cppunit/complextext.cxx
index 0b76f5eb91d9..fb25459bb737 100644
--- a/vcl/qa/cppunit/complextext.cxx
+++ b/vcl/qa/cppunit/complextext.cxx
@@ -53,6 +53,7 @@ public:
     void testCaching();
     void testCachingSubstring();
     void testCaret();
+    void testGdefCaret();
 
     CPPUNIT_TEST_SUITE(VclComplexTextTest);
     CPPUNIT_TEST(testArabic);
@@ -60,6 +61,7 @@ public:
     CPPUNIT_TEST(testCaching);
     CPPUNIT_TEST(testCachingSubstring);
     CPPUNIT_TEST(testCaret);
+    CPPUNIT_TEST(testGdefCaret);
     CPPUNIT_TEST_SUITE_END();
 };
 
@@ -236,6 +238,8 @@ void VclComplexTextTest::testCachingSubstring()
 void VclComplexTextTest::testCaret()
 {
 #if HAVE_MORE_FONTS
+    // Test caret placement in fonts *without* ligature carets in GDEF table.
+
     ScopedVclPtrInstance<WorkWindow> pWin(static_cast<vcl::Window *>(nullptr));
     CPPUNIT_ASSERT( pWin );
 
@@ -313,6 +317,93 @@ void VclComplexTextTest::testCaret()
 #endif
 }
 
+void VclComplexTextTest::testGdefCaret()
+{
+#if HAVE_MORE_FONTS
+    // Test caret placement in fonts *with* ligature carets in GDEF table.
+
+    ScopedVclPtrInstance<WorkWindow> pWin(static_cast<vcl::Window *>(nullptr));
+    CPPUNIT_ASSERT( pWin );
+
+    OutputDevice *pOutDev = pWin->GetOutDev();
+
+    vcl::Font aFont;
+    OUString aText;
+    std::vector<sal_Int32> aCharWidths, aRefCharWidths;
+    tools::Long nTextWidth, nTextWidth2;
+
+    // A. RTL text
+    aFont = vcl::Font("Noto Naskh Arabic", "Regular", Size(0, 200));
+    pOutDev->SetFont(aFont);
+
+    aText = u"لا بلا";
+
+    // 1) Regular DX array, the ligature width is given to the first components
+    // and the next ones are all zero width.
+    aRefCharWidths = { 104, 104, 148, 203, 325, 325 };
+    aCharWidths.resize(aText.getLength());
+    std::fill(aCharWidths.begin(), aCharWidths.end(), 0);
+    nTextWidth = pOutDev->GetTextArray(aText, &aCharWidths, 0, -1, 
/*bCaret*/false);
+    CPPUNIT_ASSERT_EQUAL(aRefCharWidths, aCharWidths);
+    CPPUNIT_ASSERT_EQUAL(tools::Long(325), nTextWidth);
+    CPPUNIT_ASSERT_EQUAL(sal_Int32(nTextWidth), aCharWidths.back());
+
+    // 2) Caret placement DX array, ligature width is distributed over its
+    // components.
+    aRefCharWidths = { 53, 104, 148, 203, 265, 325 };
+    aCharWidths.resize(aText.getLength());
+    std::fill(aCharWidths.begin(), aCharWidths.end(), 0);
+    nTextWidth = pOutDev->GetTextArray(aText, &aCharWidths, 0, -1, 
/*bCaret*/true);
+    CPPUNIT_ASSERT_EQUAL(aRefCharWidths, aCharWidths);
+    CPPUNIT_ASSERT_EQUAL(tools::Long(325), nTextWidth);
+    CPPUNIT_ASSERT_EQUAL(sal_Int32(nTextWidth), aCharWidths.back());
+
+    // 3) caret placement with combining marks, they should not add to ligature
+    // component count.
+    aText = u"لَاَ بلَاَ";
+    aRefCharWidths = { 53, 53, 104, 104, 148, 203, 265, 265, 325, 325 };
+    aCharWidths.resize(aText.getLength());
+    std::fill(aCharWidths.begin(), aCharWidths.end(), 0);
+    nTextWidth2 = pOutDev->GetTextArray(aText, &aCharWidths, 0, -1, 
/*bCaret*/true);
+    CPPUNIT_ASSERT_EQUAL(aCharWidths[0], aCharWidths[1]);
+    CPPUNIT_ASSERT_EQUAL(aCharWidths[2], aCharWidths[3]);
+    CPPUNIT_ASSERT_EQUAL(aCharWidths[6], aCharWidths[7]);
+    CPPUNIT_ASSERT_EQUAL(aCharWidths[8], aCharWidths[9]);
+    CPPUNIT_ASSERT_EQUAL(aRefCharWidths, aCharWidths);
+    CPPUNIT_ASSERT_EQUAL(tools::Long(325), nTextWidth2);
+    CPPUNIT_ASSERT_EQUAL(nTextWidth, nTextWidth2);
+    CPPUNIT_ASSERT_EQUAL(sal_Int32(nTextWidth), aCharWidths.back());
+
+    // B. LTR text
+    aFont = vcl::Font("Amiri", "Regular", Size(0, 200));
+    pOutDev->SetFont(aFont);
+
+    aText = u"fi ffi fl ffl fb ffb";
+
+    // 1) Regular DX array, the ligature width is given to the first components
+    // and the next ones are all zero width.
+    aRefCharWidths = { 104, 104, 162, 321, 321, 321, 379, 487, 487, 545, 708,
+                       708, 708, 766, 926, 926, 984, 1198, 1198, 1198 };
+    aCharWidths.resize(aText.getLength());
+    std::fill(aCharWidths.begin(), aCharWidths.end(), 0);
+    nTextWidth = pOutDev->GetTextArray(aText, &aCharWidths, 0, -1, 
/*bCaret*/false);
+    CPPUNIT_ASSERT_EQUAL(aRefCharWidths, aCharWidths);
+    CPPUNIT_ASSERT_EQUAL(tools::Long(1198), nTextWidth);
+    CPPUNIT_ASSERT_EQUAL(sal_Int32(nTextWidth), aCharWidths.back());
+
+    // 2) Caret placement DX array, ligature width is distributed over its
+    // components.
+    aRefCharWidths = { 53, 104, 162, 215, 269, 321, 379, 433, 487, 545, 599,
+                       654, 708, 766, 826, 926, 984, 1038, 1097, 1198 };
+    aCharWidths.resize(aText.getLength());
+    std::fill(aCharWidths.begin(), aCharWidths.end(), 0);
+    nTextWidth = pOutDev->GetTextArray(aText, &aCharWidths, 0, -1, 
/*bCaret*/true);
+    CPPUNIT_ASSERT_EQUAL(aRefCharWidths, aCharWidths);
+    CPPUNIT_ASSERT_EQUAL(tools::Long(1198), nTextWidth);
+    CPPUNIT_ASSERT_EQUAL(sal_Int32(nTextWidth), aCharWidths.back());
+#endif
+}
+
 CPPUNIT_TEST_SUITE_REGISTRATION(VclComplexTextTest);
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/source/gdi/CommonSalLayout.cxx 
b/vcl/source/gdi/CommonSalLayout.cxx
index f75dc12dedd0..014d4f738d9f 100644
--- a/vcl/source/gdi/CommonSalLayout.cxx
+++ b/vcl/source/gdi/CommonSalLayout.cxx
@@ -673,18 +673,56 @@ void 
GenericSalLayout::GetCharWidths(std::vector<DeviceCoordinate>& rCharWidths,
                 nGraphemeCount++;
             }
 
+            std::vector<DeviceCoordinate> aWidths(nGraphemeCount);
+
+            // Check if the glyph has ligature caret positions.
+            unsigned int nCarets = nGraphemeCount;
+            std::vector<hb_position_t> aCarets(nGraphemeCount);
+            hb_ot_layout_get_ligature_carets(GetFont().GetHbFont(),
+                aGlyphItem.IsRTLGlyph() ? HB_DIRECTION_RTL : HB_DIRECTION_LTR,
+                aGlyphItem.glyphId(), 0, &nCarets, aCarets.data());
+
+            // Carets are 1-less than the grapheme count (since the last
+            // position is defined by glyph width), if the count does not
+            // match, ignore it.
+            if (nCarets == nGraphemeCount - 1)
+            {
+                // Scale the carets and apply glyph offset to them since they
+                // are based on the default glyph metrics.
+                double fScale = 0;
+                GetFont().GetScale(&fScale, nullptr);
+                for (size_t i = 0; i < nCarets; i++)
+                    aCarets[i] = (aCarets[i] * fScale) + aGlyphItem.xOffset();
+
+                // Use the glyph width for the last caret.
+                aCarets[nCarets] = aGlyphItem.newWidth();
+
+                // Carets are absolute from the X origin of the glyph, turn
+                // them to relative widths that we need below.
+                for (size_t i = 0; i < nGraphemeCount; i++)
+                    aWidths[i] = aCarets[i] - (i == 0 ? 0 : aCarets[i - 1]);
+
+                // Carets are in visual order, but we want widths in logical
+                // order.
+                if (aGlyphItem.IsRTLGlyph())
+                    std::reverse(aWidths.begin(), aWidths.end());
+            }
+            else
+            {
+                // The glyph has no carets, distribute the width evenly.
+                auto nWidth = aGlyphItem.newWidth() / nGraphemeCount;
+                std::fill(aWidths.begin(), aWidths.end(), nWidth);
+
+                // Add rounding difference to the last component to maintain
+                // ligature width.
+                aWidths[nGraphemeCount - 1] += aGlyphItem.newWidth() - (nWidth 
* nGraphemeCount);
+            }
+
             // Set the width of each grapheme cluster.
             nPos = aGlyphItem.charPos();
-            auto nWidth = aGlyphItem.newWidth() / nGraphemeCount;
-            // rounding difference
-            auto nDiff = aGlyphItem.newWidth() - (nWidth * nGraphemeCount);
-            for (unsigned int i = 0; i < nGraphemeCount; i++)
+            for (auto nWidth : aWidths)
             {
                 rCharWidths[nPos - mnMinCharPos] += nWidth;
-                // add rounding difference to last component to maintain
-                // ligature width.
-                if (i == nGraphemeCount - 1)
-                    rCharWidths[nPos - mnMinCharPos] += nDiff;
                 nPos = xBreak->nextCharacters(rStr, nPos, aLocale,
                     css::i18n::CharacterIteratorMode::SKIPCELL, 1, nDone);
             }

Reply via email to