/* -*- 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 // These need being explicit because of SalLayoutGlyphsImpl being private in vcl. SalLayoutGlyphs::SalLayoutGlyphs() {} SalLayoutGlyphs::~SalLayoutGlyphs() {} SalLayoutGlyphs::SalLayoutGlyphs(SalLayoutGlyphs&& rOther) noexcept { std::swap(m_pImpl, rOther.m_pImpl); std::swap(m_pExtraImpls, rOther.m_pExtraImpls); } SalLayoutGlyphs& SalLayoutGlyphs::operator=(SalLayoutGlyphs&& rOther) noexcept { if (this != &rOther) { std::swap(m_pImpl, rOther.m_pImpl); std::swap(m_pExtraImpls, rOther.m_pExtraImpls); } return *this; } bool SalLayoutGlyphs::IsValid() const { if (m_pImpl == nullptr) return false; if (!m_pImpl->IsValid()) return false; if (m_pExtraImpls) for (std::unique_ptr const& impl : *m_pExtraImpls) if (!impl->IsValid()) return false; return true; } void SalLayoutGlyphs::Invalidate() { // Invalidating is in fact simply clearing. m_pImpl.reset(); m_pExtraImpls.reset(); } SalLayoutGlyphsImpl* SalLayoutGlyphs::Impl(unsigned int nLevel) const { if (nLevel == 0) return m_pImpl.get(); if (m_pExtraImpls != nullptr && nLevel - 1 < m_pExtraImpls->size()) return (*m_pExtraImpls)[nLevel - 1].get(); return nullptr; } void SalLayoutGlyphs::AppendImpl(SalLayoutGlyphsImpl* pImpl) { if (!m_pImpl) m_pImpl.reset(pImpl); else { if (!m_pExtraImpls) m_pExtraImpls.reset(new std::vector>); m_pExtraImpls->emplace_back(pImpl); } } SalLayoutGlyphsImpl* SalLayoutGlyphsImpl::clone() const { return new SalLayoutGlyphsImpl(*this); } // Clone, but only glyphs in the given range in the original text string. // It is possible the given range may not be cloned, in which case this returns nullptr. SalLayoutGlyphsImpl* SalLayoutGlyphsImpl::cloneCharRange(sal_Int32 index, sal_Int32 length) const { std::unique_ptr copy(new SalLayoutGlyphsImpl(*GetFont())); copy->SetFlags(GetFlags()); if (empty()) return copy.release(); bool rtl = front().IsRTLGlyph(); // Avoid mixing LTR/RTL or layouts that do not have it set explicitly (BiDiStrong). Otherwise // the subset may not quite match what would a real layout call give (e.g. some characters with neutral // direction such as space might have different LTR/RTL flag). It seems bailing out here mostly // avoid relatively rare corner cases and doesn't matter for performance. // This is also checked in SalLayoutGlyphsCache::GetLayoutGlyphs() below. if (!(GetFlags() & SalLayoutFlags::BiDiStrong) || rtl != bool(GetFlags() & SalLayoutFlags::BiDiRtl)) return nullptr; copy->reserve(std::min(size(), length)); sal_Int32 beginPos = index; sal_Int32 endPos = index + length; const_iterator pos; if (rtl) { // Glyphs are in reverse order for RTL. beginPos = index + length - 1; endPos = index - 1; // Skip glyphs that are in the string after the given index, i.e. are before the glyphs // we want. pos = std::partition_point( begin(), end(), [beginPos](const GlyphItem& it) { return it.charPos() > beginPos; }); } else { // Skip glyphs that are in the string before the given index (glyphs are sorted by charPos()). pos = std::partition_point( begin(), end(), [beginPos](const GlyphItem& it) { return it.charPos() < beginPos; }); } if (pos == end()) return nullptr; // Require a start at the exact position given, otherwise bail out. if (pos->charPos() != beginPos) return nullptr; // For RTL make sure we're not cutting in the middle of a multi-character glyph // (for non-RTL charPos is always the start of a multi-character glyph). if (rtl && pos->charPos() + pos->charCount() > beginPos + 1) return nullptr; if (!isSafeToBreak(pos, rtl)) return nullptr; // LinearPos needs adjusting to start at xOffset/yOffset for the first item, // that's how it's computed in GenericSalLayout::LayoutText(). DevicePoint zeroPoint = pos->linearPos() - DevicePoint(pos->xOffset(), pos->yOffset()); // Add and adjust all glyphs until the given length. // The check is written as 'charPos + charCount <= endPos' rather than 'charPos < endPos' // (or similarly for RTL) to make sure we include complete glyphs. If a glyph is composed // from several characters, we should not cut in the middle of those characters, so this // checks the glyph is entirely in the given character range. If it is not, this will end // the loop and the later 'pos->charPos() != endPos' check will fail and bail out. // CppunitTest_sw_layoutwriter's testCombiningCharacterCursorPosition would fail without this. while (pos != end() && (rtl ? pos->charPos() - pos->charCount() >= endPos : pos->charPos() + pos->charCount() <= endPos)) { if (pos->IsRTLGlyph() != rtl) return nullptr; // Don't mix RTL and non-RTL runs. // HACK: When running CppunitTest_sw_uiwriter3's testTdf104649 on Mac there's glyph // with id 1232 that has 0 charCount, 0 origWidth and inconsistent xOffset (sometimes 0, // but sometimes not). Possibly font or Harfbuzz bug? It's extremely rare, so simply bail out. if (pos->charCount() == 0 && pos->origWidth() == 0) return nullptr; copy->push_back(*pos); copy->back().setLinearPos(copy->back().linearPos() - zeroPoint); ++pos; } if (pos != end()) { // Fail if the next character is not at the expected past-end position. For RTL check // that we're not cutting in the middle of a multi-character glyph. if (rtl ? pos->charPos() + pos->charCount() != endPos + 1 : pos->charPos() != endPos) return nullptr; if (!isSafeToBreak(pos, rtl)) return nullptr; } return copy.release(); } bool SalLayoutGlyphsImpl::isSafeToBreak(const_iterator pos, bool rtl) const { if (rtl) { // RTL is more complicated, because HB_GLYPH_FLAG_UNSAFE_TO_BREAK talks about beginning // of a cluster, which refers to the text, not glyphs. This function is called // for the first glyph of the subset and the first glyph after the subset, but since // the glyphs are backwards, and we need the beginning of cluster at the start of the text // and beginning of the cluster after the text, we need to check glyphs before this position. if (pos == begin()) return true; --pos; } // Don't create a subset if it's not safe to break at the beginning or end of the sequence // (https://harfbuzz.github.io/harfbuzz-hb-buffer.html#hb-glyph-flags-t). if (pos->IsUnsafeToBreak() || (pos->IsInCluster() && !pos->IsClusterStart())) return false; return true; } #ifdef DBG_UTIL bool SalLayoutGlyphsImpl::isEqual(const SalLayoutGlyphsImpl* other) const { if (!GetFont()->mxFontMetric->CompareDeviceIndependentFontAttributes( *other->GetFont()->mxFontMetric)) return false; if (GetFlags() != other->GetFlags()) return false; if (empty() || other->empty()) return empty() == other->empty(); if (size() != other->size()) return false; for (size_t pos = 0; pos < size(); ++pos) { if ((*this)[pos] != (*other)[pos]) return false; } return true; } #endif bool SalLayoutGlyphsImpl::IsValid() const { if (!m_rFontInstance.is()) return false; return true; } void SalLayoutGlyphsCache::clear() { mCachedGlyphs.clear(); } SalLayoutGlyphsCache* SalLayoutGlyphsCache::self() { static vcl::DeleteOnDeinit cache( !utl::ConfigManager::IsFuzzing() ? officecfg::Office::Common::Cache::Font::GlyphsCacheSize::get() : 20000); return cache.get(); } static UBiDiDirection getBiDiDirection(const OUString& text, sal_Int32 index, sal_Int32 len) { // Return whether all character are LTR, RTL, neutral or whether it's mixed. // This is sort of ubidi_getBaseDirection() and ubidi_getDirection(), // but it's meant to be fast but also check all characters. sal_Int32 end = index + len; UBiDiDirection direction = UBIDI_NEUTRAL; while (index < end) { switch (u_charDirection(text.iterateCodePoints(&index))) { // Only characters with strong direction. case U_LEFT_TO_RIGHT: if (direction == UBIDI_RTL) return UBIDI_MIXED; direction = UBIDI_LTR; break; case U_RIGHT_TO_LEFT: case U_RIGHT_TO_LEFT_ARABIC: if (direction == UBIDI_LTR) return UBIDI_MIXED; direction = UBIDI_RTL; break; default: break; } } return direction; } static SalLayoutGlyphs makeGlyphsSubset(const SalLayoutGlyphs& source, const OutputDevice* outputDevice, const OUString& text, sal_Int32 index, sal_Int32 len) { // tdf#149264: We need to check if the text is LTR, RTL or mixed. Apparently // harfbuzz doesn't give reproducible results (or possibly HB_GLYPH_FLAG_UNSAFE_TO_BREAK // is not reliable?) when asked to lay out RTL text as LTR. So require that the whole // subset ir either LTR or RTL. UBiDiDirection direction = getBiDiDirection(text, index, len); if (direction == UBIDI_MIXED) return SalLayoutGlyphs(); SalLayoutGlyphs ret; for (int level = 0;; ++level) { const SalLayoutGlyphsImpl* sourceLevel = source.Impl(level); if (sourceLevel == nullptr) break; bool sourceRtl = bool(sourceLevel->GetFlags() & SalLayoutFlags::BiDiRtl); if ((direction == UBIDI_LTR && sourceRtl) || (direction == UBIDI_RTL && !sourceRtl)) return SalLayoutGlyphs(); SalLayoutGlyphsImpl* cloned = sourceLevel->cloneCharRange(index, len); // If the glyphs range cannot be cloned, bail out. if (cloned == nullptr) return SalLayoutGlyphs(); // If the entire string is mixed LTR/RTL but the subset is only LTR, // then make sure the flags match that, otherwise checkGlyphsEqual() // would assert on flags being different. cloned->SetFlags(cloned->GetFlags() | outputDevice->GetBiDiLayoutFlags(text, index, index + len)); // SalLayoutFlags::KashidaJustification is set only if any glyph // in the range has GlyphItemFlags::ALLOW_KASHIDA (otherwise unset it). if (cloned->GetFlags() & SalLayoutFlags::KashidaJustification) { bool hasKashida = false; for (const GlyphItem& item : *cloned) { if (item.AllowKashida()) { hasKashida = true; break; } } if (!hasKashida) cloned->SetFlags(cloned->GetFlags() & ~SalLayoutFlags::KashidaJustification); } #ifdef DBG_UTIL else for (const GlyphItem& item : *cloned) assert(!item.AllowKashida()); #endif ret.AppendImpl(cloned); } return ret; } #ifdef DBG_UTIL static void checkGlyphsEqual(const SalLayoutGlyphs& g1, const SalLayoutGlyphs& g2) { for (int level = 0;; ++level) { const SalLayoutGlyphsImpl* l1 = g1.Impl(level); const SalLayoutGlyphsImpl* l2 = g2.Impl(level); if (l1 == nullptr || l2 == nullptr) { assert(l1 == l2); break; } assert(l1->isEqual(l2)); } } #endif const SalLayoutGlyphs* SalLayoutGlyphsCache::GetLayoutGlyphs(VclPtr outputDevice, const OUString& text, sal_Int32 nIndex, sal_Int32 nLen, tools::Long nLogicWidth, const vcl::text::TextLayoutCache* layoutCache) { if (nLen == 0) return nullptr; const CachedGlyphsKey key(outputDevice, text, nIndex, nLen, nLogicWidth); GlyphsCache::const_iterator it = mCachedGlyphs.find(key); if (it != mCachedGlyphs.end()) { if (it->second.IsValid()) return &it->second; // Do not try to create the layout here. If a cache item exists, it's already // been attempted and the layout was invalid (this happens with MultiSalLayout). // So in that case this is a cached failure. return nullptr; } bool resetLastSubstringKey = true; const sal_Unicode nbSpace = 0xa0; // non-breaking space // SalLayoutGlyphsImpl::cloneCharRange() requires BiDiStrong, so if not set, do not even try. bool skipGlyphSubsets = !(outputDevice->GetLayoutMode() & vcl::text::ComplexTextLayoutFlags::BiDiStrong); if ((nIndex != 0 || nLen != text.getLength()) && !skipGlyphSubsets) { // The glyphs functions are often called first for an entire string // and then with an increasing starting index until the end of the string. // Which means it's possible to get the glyphs faster by just copying // a subset of the full glyphs and adjusting as necessary. if (mLastTemporaryKey.has_value() && mLastTemporaryKey == key) return &mLastTemporaryGlyphs; const CachedGlyphsKey keyWhole(outputDevice, text, 0, text.getLength(), nLogicWidth); GlyphsCache::const_iterator itWhole = mCachedGlyphs.find(keyWhole); if (itWhole == mCachedGlyphs.end()) { // This function may often be called repeatedly for segments of the same string, // in which case it is more efficient to cache glyphs for the entire string // and then return subsets of them. So if a second call either starts at the same // position or starts at the end of the previous call, cache the entire string. // This used to do this only for the first two segments of the string, // but that missed the case when the font slightly changed e.g. because of the first // part being underlined. Doing this for any two segments allows this optimization // even when the prefix of the string would use a different font. // TODO: Can those font differences be ignored? // Writer layouts tests enable SAL_ABORT_ON_NON_APPLICATION_FONT_USE in order // to make PrintFontManager::Substitute() abort if font fallback happens. When // laying out the entire string the chance this happens increases (e.g. testAbi11870 // normally calls this function only for a part of a string, but this optimization // lays out the entire string and causes a fallback). Since this optimization // does not change result of this function, simply disable it for those tests. static bool bAbortOnFontSubstitute = getenv("SAL_ABORT_ON_NON_APPLICATION_FONT_USE") != nullptr; if (mLastSubstringKey.has_value() && !bAbortOnFontSubstitute) { sal_Int32 pos = nIndex; if (mLastSubstringKey->len < pos && text[pos - 1] == nbSpace) --pos; // Writer skips a non-breaking space, so skip that character too. if ((mLastSubstringKey->len == pos || mLastSubstringKey->index == nIndex) && mLastSubstringKey == CachedGlyphsKey(outputDevice, text, mLastSubstringKey->index, mLastSubstringKey->len, nLogicWidth)) { GetLayoutGlyphs(outputDevice, text, 0, text.getLength(), nLogicWidth, layoutCache); itWhole = mCachedGlyphs.find(keyWhole); } else mLastSubstringKey.reset(); } if (!mLastSubstringKey.has_value()) { mLastSubstringKey = key; resetLastSubstringKey = false; } } if (itWhole != mCachedGlyphs.end() && itWhole->second.IsValid()) { mLastSubstringKey.reset(); mLastTemporaryGlyphs = makeGlyphsSubset(itWhole->second, outputDevice, text, nIndex, nLen); if (mLastTemporaryGlyphs.IsValid()) { mLastTemporaryKey = std::move(key); #ifdef DBG_UTIL std::shared_ptr tmpLayoutCache; if (layoutCache == nullptr) { tmpLayoutCache = vcl::text::TextLayoutCache::Create(text); layoutCache = tmpLayoutCache.get(); } // Check if the subset result really matches what we would get normally, // to make sure corner cases are handled well (see SalLayoutGlyphsImpl::cloneCharRange()). std::unique_ptr layout = outputDevice->ImplLayout(text, nIndex, nLen, Point(0, 0), nLogicWidth, {}, SalLayoutFlags::GlyphItemsOnly, layoutCache); assert(layout); checkGlyphsEqual(mLastTemporaryGlyphs, layout->GetGlyphs()); #endif return &mLastTemporaryGlyphs; } } } if (resetLastSubstringKey) { // Writer does non-breaking space differently (not as part of the string), so in that // case ignore that call and still allow finding two adjacent substrings that have // the non-breaking space between them. if (nLen != 1 || text[nIndex] != nbSpace) mLastSubstringKey.reset(); } std::shared_ptr tmpLayoutCache; if (layoutCache == nullptr) { tmpLayoutCache = vcl::text::TextLayoutCache::Create(text); layoutCache = tmpLayoutCache.get(); } std::unique_ptr layout = outputDevice->ImplLayout(text, nIndex, nLen, Point(0, 0), nLogicWidth, {}, SalLayoutFlags::GlyphItemsOnly, layoutCache); if (layout) { SalLayoutGlyphs glyphs = layout->GetGlyphs(); if (glyphs.IsValid()) { // TODO: Fallbacks do not work reliably (fallback font not included in the key), // so do not cache (but still return once, using the temporary without a key set). if (glyphs.Impl(1) != nullptr) { mLastTemporaryGlyphs = std::move(glyphs); mLastTemporaryKey.reset(); return &mLastTemporaryGlyphs; } mCachedGlyphs.insert(std::make_pair(key, layout->GetGlyphs())); assert(mCachedGlyphs.find(key) == mCachedGlyphs.begin()); // newly inserted item is first return &mCachedGlyphs.begin()->second; } } // Failure, cache it too as invalid glyphs. mCachedGlyphs.insert(std::make_pair(key, SalLayoutGlyphs())); return nullptr; } SalLayoutGlyphsCache::CachedGlyphsKey::CachedGlyphsKey( const VclPtr& outputDevice, const OUString& t, sal_Int32 i, sal_Int32 l, tools::Long w) : text(t) , index(i) , len(l) , logicWidth(w) // we also need to save things used in OutputDevice::ImplPrepareLayoutArgs(), in case they // change in the output device, plus mapMode affects the sizes. , fontMetric(outputDevice->GetFontMetric()) // TODO It would be possible to get a better hit ratio if mapMode wasn't part of the key // and results that differ only in mapmode would have coordinates adjusted based on that. // That would occasionally lead to rounding errors (at least differences that would // make checkGlyphsEqual() fail). , mapMode(outputDevice->GetMapMode()) , rtl(outputDevice->IsRTLEnabled()) , layoutMode(outputDevice->GetLayoutMode()) , digitLanguage(outputDevice->GetDigitLanguage()) { const LogicalFontInstance* fi = outputDevice->GetFontInstance(); fi->GetScale(&fontScaleX, &fontScaleY); const vcl::font::FontSelectPattern& rFSD = fi->GetFontSelectPattern(); artificialItalic = rFSD.GetItalic() != ITALIC_NONE && fontMetric.GetItalic() == ITALIC_NONE; artificialBold = rFSD.GetWeight() > WEIGHT_MEDIUM && fontMetric.GetWeight() <= WEIGHT_MEDIUM; hashValue = 0; o3tl::hash_combine(hashValue, vcl::text::FirstCharsStringHash()(text)); o3tl::hash_combine(hashValue, index); o3tl::hash_combine(hashValue, len); o3tl::hash_combine(hashValue, logicWidth); o3tl::hash_combine(hashValue, outputDevice.get()); // Need to use IgnoreColor, because sometimes the color changes, but it's irrelevant // for text layout (and also obsolete in vcl::Font). o3tl::hash_combine(hashValue, fontMetric.GetHashValueIgnoreColor()); // For some reason font scale may differ even if vcl::Font is the same, // so explicitly check it too. o3tl::hash_combine(hashValue, fontScaleX); o3tl::hash_combine(hashValue, fontScaleY); o3tl::hash_combine(hashValue, mapMode.GetHashValue()); o3tl::hash_combine(hashValue, rtl); o3tl::hash_combine(hashValue, artificialItalic); o3tl::hash_combine(hashValue, artificialBold); o3tl::hash_combine(hashValue, layoutMode); o3tl::hash_combine(hashValue, digitLanguage.get()); } inline bool SalLayoutGlyphsCache::CachedGlyphsKey::operator==(const CachedGlyphsKey& other) const { return hashValue == other.hashValue && index == other.index && len == other.len && logicWidth == other.logicWidth && mapMode == other.mapMode && rtl == other.rtl && artificialItalic == other.artificialItalic && artificialBold == other.artificialBold && layoutMode == other.layoutMode && digitLanguage == other.digitLanguage && fontScaleX == other.fontScaleX && fontScaleY == other.fontScaleY && fontMetric.EqualIgnoreColor(other.fontMetric) && vcl::text::FastStringCompareEqual()(text, other.text); // Slower things last in the comparison. } size_t SalLayoutGlyphsCache::GlyphsCost::operator()(const SalLayoutGlyphs& glyphs) const { size_t cost = 0; for (int level = 0;; ++level) { const SalLayoutGlyphsImpl* impl = glyphs.Impl(level); if (impl == nullptr) break; // Count size in bytes, both the SalLayoutGlyphsImpl instance and contained GlyphItem's. cost += sizeof(*impl); cost += impl->size() * sizeof(impl->front()); } return cost; } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */