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: خالد حسني <khaled@aliftype.com>
diff --git a/vcl/qa/cppunit/complextext.cxx b/vcl/qa/cppunit/complextext.cxx
index 0b76f5e..fb25459b 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 f75dc12..014d4f7 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);
            }