/* -*- 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 .
*/
// This is a helper class to enable correct styling and glyph placement when a grapheme cluster is // split across multiple adjoining layouts. // // In order to justify text, we need glyphs grouped into grapheme clusters so diacritics will stay // attached to characters under adjustment. However, in order to correctly position and style // grapheme clusters that span multiple layouts, we need best-effort character-level position data. // // At time of writing, HarfBuzz cannot provide both types of information simultaneously. As a work- // around, this helper class runs HarfBuzz a second time to get the missing information. Should a // future version of HarfBuzz support this use case directly, this helper code should be deleted. // // See tdf#61444, tdf#71956, tdf#124116 class UnclusteredGlyphMapper
{ private:
hb_buffer_t* m_pHbBuffer = nullptr;
std::multimap<sal_Int32, UnclusteredGlyphData> m_aGlyphs; bool m_bEnable = false;
public:
UnclusteredGlyphMapper(bool bEnable, int nGlyphCapacity)
: m_bEnable(bEnable)
{ if (!m_bEnable)
{ return;
}
// The shapers that we want HarfBuzz to use, in the order of // preference. constchar* const pHbShapers[] = { "graphite2", "ot", "fallback", nullptr }; bool ok
= hb_shape_full(pHbFont, m_pHbBuffer, maFeatures.data(), maFeatures.size(), pHbShapers);
assert(ok);
(void)ok;
int nRunGlyphCount = hb_buffer_get_length(m_pHbBuffer);
hb_glyph_info_t* pHbGlyphInfos = hb_buffer_get_glyph_infos(m_pHbBuffer, nullptr);
for (int i = 0; i < nRunGlyphCount; ++i)
{
int32_t nGlyphIndex = pHbGlyphInfos[i].codepoint;
int32_t nCharPos = pHbGlyphInfos[i].cluster;
//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; int nGraphemeEndPos = mxBreak->nextCharacters(rArgs.mrStr, nCharEnd - 1, rLocale,
css::i18n::CharacterIteratorMode::SKIPCELL, 1, nDone); // Safely advance nCharPos in case it is a non-BMP character.
rArgs.mrStr.iterateCodePoints(&nCharPos); int nGraphemeStartPos =
mxBreak->previousCharacters(rArgs.mrStr, nCharPos, rLocale,
css::i18n::CharacterIteratorMode::SKIPCELL, 1, nDone);
// tdf#107612 // If the start of the fallback run is Mongolian character and the previous // character is NNBSP, we want to include the NNBSP in the fallback since // it has special uses in Mongolian and have to be in the same text run to // work.
sal_Int32 nTempPos = nGraphemeStartPos; if (nGraphemeStartPos > 0)
{ auto nCurrChar = rArgs.mrStr.iterateCodePoints(&nTempPos, 0); auto nPrevChar = rArgs.mrStr.iterateCodePoints(&nTempPos, -1); if (nPrevChar == 0x202F
&& u_getIntPropertyValue(nCurrChar, UCHAR_SCRIPT) == USCRIPT_MONGOLIAN)
nGraphemeStartPos = nTempPos;
}
//stay inside the Layout range (e.g. with tdf124116-1.odt)
nGraphemeStartPos = std::max(rArgs.mnMinCharPos, nGraphemeStartPos);
nGraphemeEndPos = std::min(rArgs.mnEndCharPos, nGraphemeEndPos);
// 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)
{
sal_GlyphId nGlyphIndex = GetFont().GetGlyphIndex(aChar, aVariationSelector); if (!nGlyphIndex) returnfalse;
if (!mpVertGlyphs)
{
hb_face_t* pHbFace = hb_font_get_face(GetFont().GetHbFont());
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 input glyphs in each lookup (i.e. the glyphs that // this lookup applies to).
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);
}
}
hb_set_destroy(pLookups);
}
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) returntrue;
ImplLayoutRuns aFallbackRuns;
if (pGlyphs)
{ // Work with pre-computed glyph items.
m_GlyphItems = *pGlyphs;
for(const GlyphItem& item : m_GlyphItems)
{ if(!item.glyphId())
{
sal_Int32 nCurrCharPos = item.charPos(); auto aCurrChar = rArgs.mrStr.iterateCodePoints(&nCurrCharPos, 0); // tdf#126111: fallback is meaningless for PUA codepoints if (u_charType(aCurrChar) != U_PRIVATE_USE_CHAR)
aFallbackRuns.AddPos(item.charPos(), item.IsRTLGlyph());
}
}
std::shared_ptr<const vcl::text::TextLayoutCache> pNewScriptRun;
vcl::text::TextLayoutCache const* pTextLayout; if (rArgs.m_pTextLayoutCache)
{
pTextLayout = rArgs.m_pTextLayoutCache; // use cache!
} else
{ // tdf#92064, tdf#162663: // Also use the global LRU cache for full string script runs. // This obviates O(n^2) calls to vcl::ScriptRun::next() when laying out large paragraphs.
pNewScriptRun = vcl::text::TextLayoutCache::Create(rArgs.mrStr);
pTextLayout = pNewScriptRun.get();
}
// 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. double 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.0;
}
if (rArgs.mnFlags & SalLayoutFlags::DisableLigatures)
{
SAL_INFO("vcl.harfbuzz", "Disabling ligatures for font: " << rFontSelData.maTargetName);
// Both of these are optional ligatures, enabled by default but not for // orthographically-required ligatures.
maFeatures.push_back({ HB_TAG('l','i','g','a'), 0, 0, static_cast<unsignedint>(-1) });
maFeatures.push_back({ HB_TAG('c','l','i','g'), 0, 0, static_cast<unsignedint>(-1) });
}
// Characters 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 == U_VO_UPRIGHT || aVo == U_VO_TRANSFORMED_UPRIGHT ||
(aVo == U_VO_TRANSFORMED_ROTATED &&
HasVerticalAlternate(aChar, aVariationSelector)))
{
aDirection = HB_DIRECTION_TTB;
} else
{
aDirection = bRightToLeft ? HB_DIRECTION_RTL : HB_DIRECTION_LTR;
}
// The shapers that we want HarfBuzz to use, in the order of // preference. constchar*const pHbShapers[] = { "graphite2", "ot", "fallback", nullptr }; bool ok = hb_shape_full(pHbFont, pHbBuffer, maFeatures.data(), maFeatures.size(), pHbShapers);
assert(ok);
(void) ok;
// tdf#164106: Grapheme clusters can be split across multiple layouts. To do this, // the complete string is laid out, and only the necessary glyphs are extracted. // These sub-layouts are positioned side-by-side to form the complete text. // This approach is good enough for most diacritic cases, but it cannot handle cases // where a glyph with an advance is reordered into a different sub-layout. bool bStartClusterOutOfOrder = false; bool bEndClusterOutOfOrder = false;
{ double nNormalAdvance = 0.0; double nStartAdvance = 0.0; double nEndAdvance = 0.0;
auto fnHandleGlyph = [&](int i)
{
int32_t nGlyphIndex = pHbGlyphInfos[i].codepoint;
int32_t nCluster = pHbGlyphInfos[i].cluster; auto nOrigCharPos = stClusterMapper.RemapGlyph(nCluster, nGlyphIndex);
if (nOrigCharPos < rArgs.mnDrawMinCharPos)
{
nStartAdvance += nAdvance; if (nStartAdvance != nNormalAdvance)
{
bStartClusterOutOfOrder = true;
}
}
if (nOrigCharPos < rArgs.mnDrawEndCharPos)
{
nEndAdvance += nAdvance; if (nEndAdvance != nNormalAdvance)
{
bEndClusterOutOfOrder = true;
}
}
};
if (bRightToLeft)
{ for (int i = nRunGlyphCount - 1; i >= 0; --i)
{
fnHandleGlyph(i);
}
} else
{ for (int i = 0; i < nRunGlyphCount; ++i)
{
fnHandleGlyph(i);
}
}
stClusterMapper.Reset();
}
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 needed request glyph fallback by updating LayoutArgs auto nOrigCharPos = stClusterMapper.RemapGlyph(nCharPos, nGlyphIndex); if (!nGlyphIndex)
{ // Only request fallback for grapheme clusters that are drawn if (nOrigCharPos >= rArgs.mnDrawMinCharPos
&& nOrigCharPos < rArgs.mnDrawEndCharPos)
{
sal_Int32 nCurrCharPos = nOrigCharPos; auto aCurrChar = rArgs.mrStr.iterateCodePoints(&nCurrCharPos, 0); // tdf#126111: fallback is meaningless for PUA codepoints if (u_charType(aCurrChar) != U_PRIVATE_USE_CHAR)
{
aFallbackRuns.AddPos(nOrigCharPos, 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;
if (GetFont().NeedOffsetCorrection(pHbPositions[i].y_offset))
{ // We need glyph's advance, top bearing, and height to // correct y offset.
basegfx::B2DRectangle aRect; // Get cached bound rect value for the font,
GetFont().GetGlyphBoundRect(nGlyphIndex, aRect, true);
// Some flags are set as a side effect of text layout, save them here. if (rArgs.mnFlags & SalLayoutFlags::GlyphItemsOnly)
m_GlyphItems.SetFlags(rArgs.mnFlags);
for (autoconst& aGlyphItem : m_GlyphItems)
{ if (aGlyphItem.charPos() >= mnEndCharPos) continue;
unsignedint nGraphemeCount = 0; if (aGlyphItem.charCount() > 1 && aGlyphItem.newWidth() != 0 && !rStr.isEmpty())
{ // We are calculating DX array for cursor positions and this is a // ligature, find out how many grapheme clusters are in it. if (!xBreak.is())
xBreak = mxBreak.is() ? mxBreak : vcl::unohelper::CreateBreakIterator();
if (nGraphemeCount > 1)
{ // More than one grapheme cluster, we want to distribute the glyph // width over them.
std::vector<double> aWidths(nGraphemeCount);
// Check if the glyph has ligature caret positions. unsignedint 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.
sal_Int32 nDone;
sal_Int32 nPos = aGlyphItem.charPos(); for (auto nWidth : aWidths)
{
rCharWidths[nPos - mnMinCharPos] += nWidth;
nPos = xBreak->nextCharacters(rStr, nPos, rLocale,
css::i18n::CharacterIteratorMode::SKIPCELL, 1, nDone);
}
} else
rCharWidths[aGlyphItem.charPos() - mnMinCharPos] += aGlyphItem.newWidth();
}
}
// - stJustification: // - contains adjustments to glyph advances (usually due to justification). // - contains kashida insertion positions, for Arabic script justification. // - The number of kashidas is calculated from the adjusted advances. void GenericSalLayout::ApplyJustificationData(const JustificationData& rstJustification)
{ int nCharCount = mnEndCharPos - mnMinCharPos;
std::vector<double> aOldCharWidths;
std::unique_ptr<double[]> const pNewCharWidths(newdouble[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] = rstJustification.GetTotalAdvance(mnMinCharPos + i);
} else
{
pNewCharWidths[i] = rstJustification.GetTotalAdvance(mnMinCharPos + i)
- rstJustification.GetTotalAdvance(mnMinCharPos + i - 1);
}
}
// Map of Kashida insertion points (in the glyph items vector) and the // requested width.
std::map<size_t, std::pair<double, double>> pKashidas;
// The accumulated difference in X position. double 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; double 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].addNewWidth(nDiff);
m_GlyphItems[i].adjustLinearPosX(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].adjustLinearPosX(nDelta);
}
} elseif (m_GlyphItems[i].IsInCluster())
{ // RTL glyph in the middle of the cluster, will be handled in the // loop below.
i++;
} else// RTL
{ // Adjust the width and position of the first (rightmost) glyph in // the cluster. This is RTL, so we put all the adjustment to the // left of the glyph.
m_GlyphItems[i].addNewWidth(nDiff);
m_GlyphItems[i].adjustLinearPosX(nDelta + nDiff);
// Adjust the X position of the rest of the glyphs in the cluster. // We iterate backwards since this is an RTL glyph. for (size_t j = i; j >= 1 && m_GlyphItems[j - 1].IsInCluster(); --j)
m_GlyphItems[j - 1].adjustLinearPosX(nDelta + nDiff);
// This is a Kashida insertion position, mark it. Kashida glyphs // will be inserted below. if (rstJustification.GetPositionHasKashida(mnMinCharPos + nCharPos).value_or(false))
{
pKashidas[i] = { nDiff, pNewCharWidths[nCharPos] };
}
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 glyph belonging to the same // character which is wrong as DX adjustments are character based).
nDelta += nDiff;
}
// Insert Kashida glyphs. if (pKashidas.empty()) return;
// Find Kashida glyph width and index.
sal_GlyphId nKashidaIndex = GetFont().GetGlyphIndex(0x0640); double nKashidaWidth = GetFont().GetKashidaWidth(); if (!GetSubpixelPositioning())
nKashidaWidth = std::ceil(nKashidaWidth);
if (nKashidaWidth <= 0)
{
SAL_WARN("vcl.gdi", "Asked to insert Kashidas in a font with bogus Kashida width"); return;
}
size_t nInserted = 0; for (autoconst& pKashida : pKashidas)
{ auto pGlyphIter = m_GlyphItems.begin() + nInserted + pKashida.first;
// The total Kashida width. autoconst& [nTotalWidth, nClusterWidth] = 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. double nOverlap = 0; double nShortfall = nTotalWidth - nKashidaWidth * nCopies; if (nShortfall > 0)
{
++nCopies; double nExcess = nCopies * nKashidaWidth - nTotalWidth; if (nExcess > 0)
nOverlap = nExcess / (nCopies - 1);
}
basegfx::B2DPoint aPos = pGlyphIter->linearPos(); int nCharPos = pGlyphIter->charPos();
GlyphItemFlags const nFlags = GlyphItemFlags::IS_IN_CLUSTER | GlyphItemFlags::IS_RTL_GLYPH; // Move to the left side of the adjusted width and start inserting // glyphs there.
aPos.adjustX(-nClusterWidth + pGlyphIter->origWidth()); while (nCopies--)
{
GlyphItem aKashida(nCharPos, 0, nKashidaIndex, aPos, nFlags, 0, 0, 0, nCharPos);
pGlyphIter = m_GlyphItems.insert(pGlyphIter, aKashida);
aPos.adjustX(nKashidaWidth - nOverlap);
++pGlyphIter;
++nInserted;
}
}
}
// Kashida will be inserted between nCharPos and nNextCharPos. bool GenericSalLayout::IsKashidaPosValid(int nCharPos, int nNextCharPos) const
{ // Search for glyph items corresponding to nCharPos and nNextCharPos. autoconst aGlyph = std::find_if(m_GlyphItems.begin(), m_GlyphItems.end(),
[&](const GlyphItem& g) { return g.charPos() == nCharPos; }); autoconst aNextGlyph = std::find_if(m_GlyphItems.begin(), m_GlyphItems.end(),
[&](const GlyphItem& g) { return g.charPos() == nNextCharPos; });
// If either is not found then a ligature is created at this position, we // can’t insert Kashida here. if (aGlyph == m_GlyphItems.end() || aNextGlyph == m_GlyphItems.end()) returnfalse;
// If the either character is not supported by this layout, return false so // that fallback layouts would be checked for it. if (aGlyph->glyphId() == 0 || aNextGlyph->glyphId() == 0) returnfalse;
// Lastly check if this position is kashida-safe. return aNextGlyph->IsSafeToInsertKashida();
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.