/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This file is part of the LibreOffice project. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This file incorporates work covered by the following license notice: * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed * with this work for additional information regarding copyright * ownership. The ASF licenses this file to you under the Apache * License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.apache.org/licenses/LICENSE-2.0 . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if !HB_VERSION_ATLEAST(1, 1, 0) // Disabled Unicode compatibility decomposition, see fdo#66715 static unsigned int unicodeDecomposeCompatibility(hb_unicode_funcs_t* /*ufuncs*/, hb_codepoint_t /*u*/, hb_codepoint_t* /*decomposed*/, void* /*user_data*/) { return 0; } static hb_unicode_funcs_t* getUnicodeFuncs() { static hb_unicode_funcs_t* ufuncs = hb_unicode_funcs_create(hb_icu_get_unicode_funcs()); hb_unicode_funcs_set_decompose_compatibility_func(ufuncs, unicodeDecomposeCompatibility, nullptr, nullptr); return ufuncs; } #endif GenericSalLayout::GenericSalLayout(LogicalFontInstance &rFont) : m_GlyphItems(rFont) , mpVertGlyphs(nullptr) , mbFuzzing(utl::ConfigManager::IsFuzzing()) { } GenericSalLayout::~GenericSalLayout() { } void GenericSalLayout::ParseFeatures(const OUString& aName) { vcl::font::FeatureParser aParser(aName); const OUString& sLanguage = aParser.getLanguage(); if (!sLanguage.isEmpty()) msLanguage = OUStringToOString(sLanguage, RTL_TEXTENCODING_ASCII_US); for (auto const &rFeat : aParser.getFeatures()) { hb_feature_t aFeature { rFeat.m_nTag, rFeat.m_nValue, rFeat.m_nStart, rFeat.m_nEnd }; maFeatures.push_back(aFeature); } } namespace { struct SubRun { int32_t mnMin; int32_t mnEnd; hb_script_t maScript; hb_direction_t maDirection; }; } namespace { #if U_ICU_VERSION_MAJOR_NUM >= 63 enum class VerticalOrientation { Upright = U_VO_UPRIGHT, Rotated = U_VO_ROTATED, TransformedUpright = U_VO_TRANSFORMED_UPRIGHT, TransformedRotated = U_VO_TRANSFORMED_ROTATED }; #else #include "VerticalOrientationData.cxx" // These must match the values in the file included above. enum class VerticalOrientation { Upright = 0, Rotated = 1, TransformedUpright = 2, TransformedRotated = 3 }; #endif VerticalOrientation GetVerticalOrientation(sal_UCS4 cCh, const LanguageTag& rTag) { // Override orientation of fullwidth colon , semi-colon, // and Bopomofo tonal marks. if ((cCh == 0xff1a || cCh == 0xff1b || cCh == 0x2ca || cCh == 0x2cb || cCh == 0x2c7 || cCh == 0x2d9) && rTag.getLanguage() == "zh") return VerticalOrientation::TransformedUpright; #if U_ICU_VERSION_MAJOR_NUM >= 63 int32_t nRet = u_getIntPropertyValue(cCh, UCHAR_VERTICAL_ORIENTATION); #else uint8_t nRet = 1; if (cCh < 0x10000) { nRet = sVerticalOrientationValues[sVerticalOrientationPages[0][cCh >> kVerticalOrientationCharBits]] [cCh & ((1 << kVerticalOrientationCharBits) - 1)]; } else if (cCh < (kVerticalOrientationMaxPlane + 1) * 0x10000) { nRet = sVerticalOrientationValues[sVerticalOrientationPages[sVerticalOrientationPlanes[(cCh >> 16) - 1]] [(cCh & 0xffff) >> kVerticalOrientationCharBits]] [cCh & ((1 << kVerticalOrientationCharBits) - 1)]; } else { // Default value for unassigned SAL_WARN("vcl.gdi", "Getting VerticalOrientation for codepoint outside Unicode range"); } #endif return VerticalOrientation(nRet); } } // namespace std::shared_ptr GenericSalLayout::CreateTextLayoutCache(OUString const& rString) { return std::make_shared(rString.getStr(), rString.getLength()); } SalLayoutGlyphs GenericSalLayout::GetGlyphs() const { SalLayoutGlyphs glyphs; glyphs.AppendImpl(m_GlyphItems.clone()); return glyphs; } void GenericSalLayout::SetNeedFallback(vcl::text::ImplLayoutArgs& rArgs, sal_Int32 nCharPos, bool bRightToLeft) { if (nCharPos < 0 || mbFuzzing) return; using namespace ::com::sun::star; if (!mxBreak.is()) mxBreak = vcl::unohelper::CreateBreakIterator(); lang::Locale aLocale(rArgs.maLanguageTag.getLocale()); //if position nCharPos is missing in the font, grab the entire grapheme and //mark all glyphs as missing so the whole thing is rendered with the same //font sal_Int32 nDone; sal_Int32 nGraphemeEndPos = mxBreak->nextCharacters(rArgs.mrStr, nCharPos, aLocale, i18n::CharacterIteratorMode::SKIPCELL, 1, nDone); // Safely advance nCharPos in case it is a non-BMP character. rArgs.mrStr.iterateCodePoints(&nCharPos); sal_Int32 nGraphemeStartPos = mxBreak->previousCharacters(rArgs.mrStr, nCharPos, aLocale, i18n::CharacterIteratorMode::SKIPCELL, 1, nDone); rArgs.AddFallbackRun(nGraphemeStartPos, nGraphemeEndPos, bRightToLeft); } void GenericSalLayout::AdjustLayout(vcl::text::ImplLayoutArgs& rArgs) { SalLayout::AdjustLayout(rArgs); if (rArgs.mpAltNaturalDXArray) // Used when "TextRenderModeForResolutionIndependentLayout" is set ApplyDXArray(rArgs.mpAltNaturalDXArray, rArgs.mnFlags); else if (rArgs.mpDXArray) // Normal case ApplyDXArray(rArgs.mpDXArray, rArgs.mnFlags); else if (rArgs.mnLayoutWidth) Justify(rArgs.mnLayoutWidth); // apply asian kerning if the glyphs are not already formatted else if ((rArgs.mnFlags & SalLayoutFlags::KerningAsian) && !(rArgs.mnFlags & SalLayoutFlags::Vertical)) ApplyAsianKerning(rArgs.mrStr); } void GenericSalLayout::DrawText(SalGraphics& rSalGraphics) const { //call platform dependent DrawText functions rSalGraphics.DrawTextLayout( *this ); } // Find if the nominal glyph of the character is an input to “vert” feature. // We don’t check for a specific script or language as it shouldn’t matter // here; if the glyph would be the result from applying “vert” for any // script/language then we want to always treat it as upright glyph. bool GenericSalLayout::HasVerticalAlternate(sal_UCS4 aChar, sal_UCS4 aVariationSelector) { hb_codepoint_t nGlyphIndex = 0; hb_font_t *pHbFont = GetFont().GetHbFont(); if (!hb_font_get_glyph(pHbFont, aChar, aVariationSelector, &nGlyphIndex)) return false; if (!mpVertGlyphs) { hb_face_t* pHbFace = hb_font_get_face(pHbFont); mpVertGlyphs = hb_set_create(); // Find all GSUB lookups for “vert” feature. hb_set_t* pLookups = hb_set_create(); hb_tag_t const pFeatures[] = { HB_TAG('v','e','r','t'), HB_TAG_NONE }; hb_ot_layout_collect_lookups(pHbFace, HB_OT_TAG_GSUB, nullptr, nullptr, pFeatures, pLookups); if (!hb_set_is_empty(pLookups)) { // Find the output glyphs in each lookup (i.e. the glyphs that // would result from applying this lookup). hb_codepoint_t nIdx = HB_SET_VALUE_INVALID; while (hb_set_next(pLookups, &nIdx)) { hb_set_t* pGlyphs = hb_set_create(); hb_ot_layout_lookup_collect_glyphs(pHbFace, HB_OT_TAG_GSUB, nIdx, nullptr, // glyphs before pGlyphs, // glyphs input nullptr, // glyphs after nullptr); // glyphs out hb_set_union(mpVertGlyphs, pGlyphs); } } } return hb_set_has(mpVertGlyphs, nGlyphIndex) != 0; } bool GenericSalLayout::LayoutText(vcl::text::ImplLayoutArgs& rArgs, const SalLayoutGlyphsImpl* pGlyphs) { // No need to touch m_GlyphItems at all for an empty string. if (rArgs.mnEndCharPos - rArgs.mnMinCharPos <= 0) return true; if (pGlyphs) { // Work with pre-computed glyph items. m_GlyphItems = *pGlyphs; for(const GlyphItem& item : m_GlyphItems) if(!item.glyphId()) SetNeedFallback(rArgs, item.charPos(), item.IsRTLGlyph()); // Some flags are set as a side effect of text layout, restore them here. rArgs.mnFlags |= pGlyphs->GetFlags(); return true; } hb_font_t *pHbFont = GetFont().GetHbFont(); bool isGraphite = GetFont().IsGraphiteFont(); int nGlyphCapacity = 2 * (rArgs.mnEndCharPos - rArgs.mnMinCharPos); m_GlyphItems.reserve(nGlyphCapacity); const int nLength = rArgs.mrStr.getLength(); const sal_Unicode *pStr = rArgs.mrStr.getStr(); std::optional oNewScriptRun; vcl::text::TextLayoutCache const* pTextLayout; if (rArgs.m_pTextLayoutCache) { pTextLayout = rArgs.m_pTextLayoutCache; // use cache! } else { oNewScriptRun.emplace(pStr, rArgs.mnEndCharPos); pTextLayout = &*oNewScriptRun; } // nBaseOffset is used to align vertical text to the center of rotated // horizontal text. That is the offset from original baseline to // the center of EM box. Maybe we can use OpenType base table to improve this // in the future. DeviceCoordinate nBaseOffset = 0; if (rArgs.mnFlags & SalLayoutFlags::Vertical) { hb_font_extents_t extents; if (hb_font_get_h_extents(pHbFont, &extents)) nBaseOffset = ( extents.ascender + extents.descender ) / 2; } hb_buffer_t* pHbBuffer = hb_buffer_create(); hb_buffer_pre_allocate(pHbBuffer, nGlyphCapacity); #if !HB_VERSION_ATLEAST(1, 1, 0) static hb_unicode_funcs_t* pHbUnicodeFuncs = getUnicodeFuncs(); hb_buffer_set_unicode_funcs(pHbBuffer, pHbUnicodeFuncs); #endif const vcl::font::FontSelectPattern& rFontSelData = GetFont().GetFontSelectPattern(); if (rArgs.mnFlags & SalLayoutFlags::DisableKerning) { SAL_INFO("vcl.harfbuzz", "Disabling kerning for font: " << rFontSelData.maTargetName); maFeatures.push_back({ HB_TAG('k','e','r','n'), 0, 0, static_cast(-1) }); } if (rFontSelData.GetPitch() == PITCH_FIXED) { SAL_INFO("vcl.harfbuzz", "Disabling ligatures for font: " << rFontSelData.maTargetName); maFeatures.push_back({ HB_TAG('l','i','g','a'), 0, 0, static_cast(-1) }); } ParseFeatures(rFontSelData.maTargetName); double nXScale = 0; double nYScale = 0; GetFont().GetScale(&nXScale, &nYScale); DevicePoint aCurrPos(0, 0); while (true) { int nBidiMinRunPos, nBidiEndRunPos; bool bRightToLeft; if (!rArgs.GetNextRun(&nBidiMinRunPos, &nBidiEndRunPos, &bRightToLeft)) break; // Find script subruns. std::vector aSubRuns; int nCurrentPos = nBidiMinRunPos; size_t k = 0; for (; k < pTextLayout->runs.size(); ++k) { vcl::text::Run const& rRun(pTextLayout->runs[k]); if (rRun.nStart <= nCurrentPos && nCurrentPos < rRun.nEnd) { break; } } if (isGraphite) { hb_script_t aScript = hb_icu_script_to_script(pTextLayout->runs[k].nCode); aSubRuns.push_back({ nBidiMinRunPos, nBidiEndRunPos, aScript, bRightToLeft ? HB_DIRECTION_RTL : HB_DIRECTION_LTR }); } else { while (nCurrentPos < nBidiEndRunPos && k < pTextLayout->runs.size()) { int32_t nMinRunPos = nCurrentPos; int32_t nEndRunPos = std::min(pTextLayout->runs[k].nEnd, nBidiEndRunPos); hb_direction_t aDirection = bRightToLeft ? HB_DIRECTION_RTL : HB_DIRECTION_LTR; hb_script_t aScript = hb_icu_script_to_script(pTextLayout->runs[k].nCode); // For vertical text, further divide the runs based on character // orientation. if (rArgs.mnFlags & SalLayoutFlags::Vertical) { sal_Int32 nIdx = nMinRunPos; while (nIdx < nEndRunPos) { sal_Int32 nPrevIdx = nIdx; sal_UCS4 aChar = rArgs.mrStr.iterateCodePoints(&nIdx); VerticalOrientation aVo = GetVerticalOrientation(aChar, rArgs.maLanguageTag); sal_UCS4 aVariationSelector = 0; if (nIdx < nEndRunPos) { sal_Int32 nNextIdx = nIdx; sal_UCS4 aNextChar = rArgs.mrStr.iterateCodePoints(&nNextIdx); if (u_hasBinaryProperty(aNextChar, UCHAR_VARIATION_SELECTOR)) { nIdx = nNextIdx; aVariationSelector = aNextChar; } } // Charters with U and Tu vertical orientation should // be shaped in vertical direction. But characters // with Tr should be shaped in vertical direction // only if they have vertical alternates, otherwise // they should be shaped in horizontal direction // and then rotated. // See http://unicode.org/reports/tr50/#vo if (aVo == VerticalOrientation::Upright || aVo == VerticalOrientation::TransformedUpright || (aVo == VerticalOrientation::TransformedRotated && HasVerticalAlternate(aChar, aVariationSelector))) { aDirection = HB_DIRECTION_TTB; } else { aDirection = bRightToLeft ? HB_DIRECTION_RTL : HB_DIRECTION_LTR; } if (aSubRuns.empty() || aSubRuns.back().maDirection != aDirection) aSubRuns.push_back({ nPrevIdx, nIdx, aScript, aDirection }); else aSubRuns.back().mnEnd = nIdx; } } else { aSubRuns.push_back({ nMinRunPos, nEndRunPos, aScript, aDirection }); } nCurrentPos = nEndRunPos; ++k; } } // RTL subruns should be reversed to ensure that final glyph order is // correct. if (bRightToLeft) std::reverse(aSubRuns.begin(), aSubRuns.end()); for (const auto& aSubRun : aSubRuns) { hb_buffer_clear_contents(pHbBuffer); const int nMinRunPos = aSubRun.mnMin; const int nEndRunPos = aSubRun.mnEnd; const int nRunLen = nEndRunPos - nMinRunPos; OString sLanguage = msLanguage; if (sLanguage.isEmpty()) sLanguage = OUStringToOString(rArgs.maLanguageTag.getBcp47(), RTL_TEXTENCODING_ASCII_US); int nHbFlags = HB_BUFFER_FLAGS_DEFAULT; if (nMinRunPos == 0) nHbFlags |= HB_BUFFER_FLAG_BOT; /* Beginning-of-text */ if (nEndRunPos == nLength) nHbFlags |= HB_BUFFER_FLAG_EOT; /* End-of-text */ hb_buffer_set_direction(pHbBuffer, aSubRun.maDirection); hb_buffer_set_script(pHbBuffer, aSubRun.maScript); hb_buffer_set_language(pHbBuffer, hb_language_from_string(sLanguage.getStr(), -1)); hb_buffer_set_flags(pHbBuffer, static_cast(nHbFlags)); hb_buffer_add_utf16( pHbBuffer, reinterpret_cast(pStr), nLength, nMinRunPos, nRunLen); hb_buffer_set_cluster_level(pHbBuffer, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS); // The shapers that we want HarfBuzz to use, in the order of // preference. The coretext_aat shaper is available only on macOS, // but there is no harm in always including it, HarfBuzz will // ignore unavailable shapers. const char*const pHbShapers[] = { "graphite2", "coretext_aat", "ot", "fallback", nullptr }; bool ok = hb_shape_full(pHbFont, pHbBuffer, maFeatures.data(), maFeatures.size(), pHbShapers); assert(ok); (void) ok; int nRunGlyphCount = hb_buffer_get_length(pHbBuffer); hb_glyph_info_t *pHbGlyphInfos = hb_buffer_get_glyph_infos(pHbBuffer, nullptr); hb_glyph_position_t *pHbPositions = hb_buffer_get_glyph_positions(pHbBuffer, nullptr); for (int i = 0; i < nRunGlyphCount; ++i) { int32_t nGlyphIndex = pHbGlyphInfos[i].codepoint; int32_t nCharPos = pHbGlyphInfos[i].cluster; int32_t nCharCount = 0; bool bInCluster = false; bool bClusterStart = false; // Find the number of characters that make up this glyph. if (!bRightToLeft) { // If the cluster is the same as previous glyph, then this // already consumed, skip. if (i > 0 && pHbGlyphInfos[i].cluster == pHbGlyphInfos[i - 1].cluster) { nCharCount = 0; bInCluster = true; } else { // Find the next glyph with a different cluster, or the // end of text. int j = i; int32_t nNextCharPos = nCharPos; while (nNextCharPos == nCharPos && j < nRunGlyphCount) nNextCharPos = pHbGlyphInfos[j++].cluster; if (nNextCharPos == nCharPos) nNextCharPos = nEndRunPos; nCharCount = nNextCharPos - nCharPos; if ((i == 0 || pHbGlyphInfos[i].cluster != pHbGlyphInfos[i - 1].cluster) && (i < nRunGlyphCount - 1 && pHbGlyphInfos[i].cluster == pHbGlyphInfos[i + 1].cluster)) bClusterStart = true; } } else { // If the cluster is the same as previous glyph, then this // will be consumed later, skip. if (i < nRunGlyphCount - 1 && pHbGlyphInfos[i].cluster == pHbGlyphInfos[i + 1].cluster) { nCharCount = 0; bInCluster = true; } else { // Find the previous glyph with a different cluster, or // the end of text. int j = i; int32_t nNextCharPos = nCharPos; while (nNextCharPos == nCharPos && j >= 0) nNextCharPos = pHbGlyphInfos[j--].cluster; if (nNextCharPos == nCharPos) nNextCharPos = nEndRunPos; nCharCount = nNextCharPos - nCharPos; if ((i == nRunGlyphCount - 1 || pHbGlyphInfos[i].cluster != pHbGlyphInfos[i + 1].cluster) && (i > 0 && pHbGlyphInfos[i].cluster == pHbGlyphInfos[i - 1].cluster)) bClusterStart = true; } } // if needed request glyph fallback by updating LayoutArgs if (!nGlyphIndex) { SetNeedFallback(rArgs, nCharPos, bRightToLeft); if (SalLayoutFlags::ForFallback & rArgs.mnFlags) continue; } GlyphItemFlags nGlyphFlags = GlyphItemFlags::NONE; if (bRightToLeft) nGlyphFlags |= GlyphItemFlags::IS_RTL_GLYPH; if (bClusterStart) nGlyphFlags |= GlyphItemFlags::IS_CLUSTER_START; if (bInCluster) nGlyphFlags |= GlyphItemFlags::IS_IN_CLUSTER; sal_UCS4 aChar = rArgs.mrStr.iterateCodePoints(&o3tl::temporary(sal_Int32(nCharPos)), 0); if (u_getIntPropertyValue(aChar, UCHAR_GENERAL_CATEGORY) == U_NON_SPACING_MARK) nGlyphFlags |= GlyphItemFlags::IS_DIACRITIC; if (u_isUWhiteSpace(aChar)) nGlyphFlags |= GlyphItemFlags::IS_SPACING; if (aSubRun.maScript == HB_SCRIPT_ARABIC && HB_DIRECTION_IS_BACKWARD(aSubRun.maDirection) && !(nGlyphFlags & GlyphItemFlags::IS_SPACING)) { nGlyphFlags |= GlyphItemFlags::ALLOW_KASHIDA; rArgs.mnFlags |= SalLayoutFlags::KashidaJustification; } DeviceCoordinate nAdvance, nXOffset, nYOffset; if (aSubRun.maDirection == HB_DIRECTION_TTB) { nGlyphFlags |= GlyphItemFlags::IS_VERTICAL; nAdvance = -pHbPositions[i].y_advance; nXOffset = -pHbPositions[i].y_offset; nYOffset = -pHbPositions[i].x_offset - nBaseOffset; } else { nAdvance = pHbPositions[i].x_advance; nXOffset = pHbPositions[i].x_offset; nYOffset = -pHbPositions[i].y_offset; } nAdvance = std::lround(nAdvance * nXScale); nXOffset = std::lround(nXOffset * nXScale); nYOffset = std::lround(nYOffset * nYScale); DevicePoint aNewPos(aCurrPos.getX() + nXOffset, aCurrPos.getY() + nYOffset); const GlyphItem aGI(nCharPos, nCharCount, nGlyphIndex, aNewPos, nGlyphFlags, nAdvance, nXOffset); m_GlyphItems.push_back(aGI); aCurrPos.adjustX(nAdvance); } } } hb_buffer_destroy(pHbBuffer); // Some flags are set as a side effect of text layout, save them here. if (rArgs.mnFlags & SalLayoutFlags::GlyphItemsOnly) m_GlyphItems.SetFlags(rArgs.mnFlags); return true; } void GenericSalLayout::GetCharWidths(std::vector& rCharWidths) const { const int nCharCount = mnEndCharPos - mnMinCharPos; rCharWidths.clear(); rCharWidths.resize(nCharCount, 0); for (auto const& aGlyphItem : m_GlyphItems) { const int nIndex = aGlyphItem.charPos() - mnMinCharPos; if (nIndex >= nCharCount) continue; rCharWidths[nIndex] += aGlyphItem.m_nNewWidth; } } // A note on how Kashida justification is implemented (because it took me 5 // years to figure it out): // The decision to insert Kashidas, where and how much is taken by Writer. // This decision is communicated to us in a very indirect way; by increasing // the width of the character after which Kashidas should be inserted by the // desired amount. // // Writer eventually calls IsKashidaPosValid() to check whether it can insert a // Kashida between two characters or not. // // Here we do: // - In LayoutText() set KashidaJustification flag based on text script. // - In ApplyDXArray(): // * Check the above flag to decide whether to insert Kashidas or not. // * For any RTL glyph that has DX adjustment, insert enough Kashidas to // fill in the added space. template void GenericSalLayout::ApplyDXArray(const DC* pDXArray, SalLayoutFlags nLayoutFlags) { int nCharCount = mnEndCharPos - mnMinCharPos; std::vector aOldCharWidths; std::unique_ptr const pNewCharWidths(new DC[nCharCount]); // Get the natural character widths (i.e. before applying DX adjustments). GetCharWidths(aOldCharWidths); // Calculate the character widths after DX adjustments. for (int i = 0; i < nCharCount; ++i) { if (i == 0) pNewCharWidths[i] = pDXArray[i]; else pNewCharWidths[i] = pDXArray[i] - pDXArray[i - 1]; } bool bKashidaJustify = false; DeviceCoordinate nKashidaWidth = 0; hb_codepoint_t nKashidaIndex = 0; if (nLayoutFlags & SalLayoutFlags::KashidaJustification) { hb_font_t *pHbFont = GetFont().GetHbFont(); // Find Kashida glyph width and index. if (hb_font_get_glyph(pHbFont, 0x0640, 0, &nKashidaIndex)) nKashidaWidth = GetFont().GetKashidaWidth(); bKashidaJustify = nKashidaWidth != 0; } // Map of Kashida insertion points (in the glyph items vector) and the // requested width. std::map pKashidas; // The accumulated difference in X position. DC nDelta = 0; // Apply the DX adjustments to glyph positions and widths. size_t i = 0; while (i < m_GlyphItems.size()) { // Accumulate the width difference for all characters corresponding to // this glyph. int nCharPos = m_GlyphItems[i].charPos() - mnMinCharPos; DC nDiff = 0; for (int j = 0; j < m_GlyphItems[i].charCount(); j++) nDiff += pNewCharWidths[nCharPos + j] - aOldCharWidths[nCharPos + j]; if (!m_GlyphItems[i].IsRTLGlyph()) { // Adjust the width and position of the first (leftmost) glyph in // the cluster. m_GlyphItems[i].m_nNewWidth += nDiff; m_GlyphItems[i].m_aLinearPos.adjustX(nDelta); // Adjust the position of the rest of the glyphs in the cluster. while (++i < m_GlyphItems.size()) { if (!m_GlyphItems[i].IsInCluster()) break; m_GlyphItems[i].m_aLinearPos.adjustX(nDelta); } } else if (m_GlyphItems[i].IsInCluster()) { // RTL glyph in the middle of the cluster, will be handled in the // loop below. i++; } else { // Adjust the width and position of the first (rightmost) glyph in // the cluster. // For RTL, we put all the adjustment to the left of the glyph. m_GlyphItems[i].m_nNewWidth += nDiff; m_GlyphItems[i].m_aLinearPos.adjustX(nDelta + nDiff); // Adjust the X position of all glyphs in the cluster. size_t j = i; while (j > 0) { --j; if (!m_GlyphItems[j].IsInCluster()) break; m_GlyphItems[j].m_aLinearPos.adjustX(nDelta + nDiff); } // If this glyph is Kashida-justifiable, then mark this as a // Kashida position. Since this must be a RTL glyph, we mark the // last glyph in the cluster not the first as this would be the // base glyph. if (bKashidaJustify && m_GlyphItems[i].AllowKashida() && nDiff > m_GlyphItems[i].charCount()) // Rounding errors, 1 pixel per character! { pKashidas[i] = nDiff; // Move any non-spacing marks attached to this cluster as well. // Looping backward because this is RTL glyph. while (j > 0) { if (!m_GlyphItems[j].IsDiacritic()) break; m_GlyphItems[j--].m_aLinearPos.adjustX(nDiff); } } i++; } // Increment the delta, the loop above makes sure we do so only once // for every character (cluster) not for every glyph (otherwise we // would apply it multiple times for each glyphs belonging to the same // character which is wrong since DX adjustments are character based). nDelta += nDiff; } // Insert Kashida glyphs. if (!bKashidaJustify || pKashidas.empty()) return; size_t nInserted = 0; for (auto const& pKashida : pKashidas) { auto pGlyphIter = m_GlyphItems.begin() + nInserted + pKashida.first; // The total Kashida width. DeviceCoordinate nTotalWidth = pKashida.second; // Number of times to repeat each Kashida. int nCopies = 1; if (nTotalWidth > nKashidaWidth) nCopies = nTotalWidth / nKashidaWidth; // See if we can improve the fit by adding an extra Kashidas and // squeezing them together a bit. DeviceCoordinate nOverlap = 0; DeviceCoordinate nShortfall = nTotalWidth - nKashidaWidth * nCopies; if (nShortfall > 0) { ++nCopies; DeviceCoordinate nExcess = nCopies * nKashidaWidth - nTotalWidth; if (nExcess > 0) nOverlap = nExcess / (nCopies - 1); } DevicePoint aPos(pGlyphIter->m_aLinearPos.getX() - nTotalWidth, 0); int nCharPos = pGlyphIter->charPos(); GlyphItemFlags const nFlags = GlyphItemFlags::IS_IN_CLUSTER | GlyphItemFlags::IS_RTL_GLYPH; while (nCopies--) { GlyphItem aKashida(nCharPos, 0, nKashidaIndex, aPos, nFlags, nKashidaWidth, 0); pGlyphIter = m_GlyphItems.insert(pGlyphIter, aKashida); aPos.adjustX(nKashidaWidth - nOverlap); ++pGlyphIter; ++nInserted; } } } bool GenericSalLayout::IsKashidaPosValid(int nCharPos) const { for (auto pIter = m_GlyphItems.begin(); pIter != m_GlyphItems.end(); ++pIter) { if (pIter->charPos() == nCharPos) { // The position is the first glyph, this would happen if we // changed the text styling in the middle of a word. Since we don’t // do ligatures across layout engine instances, this can’t be a // ligature so it should be fine. if (pIter == m_GlyphItems.begin()) return true; // If the character is not supported by this layout, return false // so that fallback layouts would be checked for it. if (pIter->glyphId() == 0) break; // Search backwards for previous glyph belonging to a different // character. We are looking backwards because we are dealing with // RTL glyphs, which will be in visual order. for (auto pPrev = pIter - 1; pPrev != m_GlyphItems.begin(); --pPrev) { if (pPrev->charPos() != nCharPos) { // Check if the found glyph belongs to the next character, // otherwise the current glyph will be a ligature which is // invalid kashida position. if (pPrev->charPos() == (nCharPos + 1)) return true; break; } } } } return false; } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */