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); }