diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /gfx/thebes/gfxFont.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'gfx/thebes/gfxFont.cpp')
-rw-r--r-- | gfx/thebes/gfxFont.cpp | 4614 |
1 files changed, 4614 insertions, 0 deletions
diff --git a/gfx/thebes/gfxFont.cpp b/gfx/thebes/gfxFont.cpp new file mode 100644 index 0000000000..68bf0d902c --- /dev/null +++ b/gfx/thebes/gfxFont.cpp @@ -0,0 +1,4614 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "gfxFont.h" + +#include "mozilla/BinarySearch.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/FontPropertyTypes.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/IntegerRange.h" +#include "mozilla/intl/Segmenter.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/StaticPrefs_gfx.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/SVGContextPaint.h" + +#include "mozilla/Logging.h" + +#include "nsITimer.h" + +#include "gfxGlyphExtents.h" +#include "gfxPlatform.h" +#include "gfxTextRun.h" +#include "nsGkAtoms.h" + +#include "gfxTypes.h" +#include "gfxContext.h" +#include "gfxFontMissingGlyphs.h" +#include "gfxGraphiteShaper.h" +#include "gfxHarfBuzzShaper.h" +#include "gfxUserFontSet.h" +#include "nsCRT.h" +#include "nsSpecialCasingData.h" +#include "nsTextRunTransformations.h" +#include "nsUGenCategory.h" +#include "nsUnicodeProperties.h" +#include "nsStyleConsts.h" +#include "mozilla/AppUnits.h" +#include "mozilla/Likely.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "mozilla/Telemetry.h" +#include "gfxMathTable.h" +#include "gfxSVGGlyphs.h" +#include "gfx2DGlue.h" +#include "TextDrawTarget.h" + +#include "ThebesRLBox.h" + +#include "GreekCasing.h" + +#include "cairo.h" +#ifdef XP_WIN +# include "cairo-win32.h" +# include "gfxWindowsPlatform.h" +#endif + +#include "harfbuzz/hb.h" +#include "harfbuzz/hb-ot.h" + +#include <algorithm> +#include <limits> +#include <cmath> + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::unicode; +using mozilla::services::GetObserverService; + +gfxFontCache* gfxFontCache::gGlobalCache = nullptr; + +#ifdef DEBUG_roc +# define DEBUG_TEXT_RUN_STORAGE_METRICS +#endif + +#ifdef DEBUG_TEXT_RUN_STORAGE_METRICS +uint32_t gTextRunStorageHighWaterMark = 0; +uint32_t gTextRunStorage = 0; +uint32_t gFontCount = 0; +uint32_t gGlyphExtentsCount = 0; +uint32_t gGlyphExtentsWidthsTotalSize = 0; +uint32_t gGlyphExtentsSetupEagerSimple = 0; +uint32_t gGlyphExtentsSetupEagerTight = 0; +uint32_t gGlyphExtentsSetupLazyTight = 0; +uint32_t gGlyphExtentsSetupFallBackToTight = 0; +#endif + +#define LOG_FONTINIT(args) \ + MOZ_LOG(gfxPlatform::GetLog(eGfxLog_fontinit), LogLevel::Debug, args) +#define LOG_FONTINIT_ENABLED() \ + MOZ_LOG_TEST(gfxPlatform::GetLog(eGfxLog_fontinit), LogLevel::Debug) + +/* + * gfxFontCache - global cache of gfxFont instances. + * Expires unused fonts after a short interval; + * notifies fonts to age their cached shaped-word records; + * observes memory-pressure notification and tells fonts to clear their + * shaped-word caches to free up memory. + */ + +MOZ_DEFINE_MALLOC_SIZE_OF(FontCacheMallocSizeOf) + +NS_IMPL_ISUPPORTS(gfxFontCache::MemoryReporter, nsIMemoryReporter) + +/*virtual*/ +gfxTextRunFactory::~gfxTextRunFactory() { + // Should not be dropped by stylo + MOZ_ASSERT(!Servo_IsWorkerThread()); +} + +NS_IMETHODIMP +gfxFontCache::MemoryReporter::CollectReports( + nsIHandleReportCallback* aHandleReport, nsISupports* aData, + bool aAnonymize) { + FontCacheSizes sizes; + + gfxFontCache::GetCache()->AddSizeOfIncludingThis(&FontCacheMallocSizeOf, + &sizes); + + MOZ_COLLECT_REPORT("explicit/gfx/font-cache", KIND_HEAP, UNITS_BYTES, + sizes.mFontInstances, + "Memory used for active font instances."); + + MOZ_COLLECT_REPORT("explicit/gfx/font-shaped-words", KIND_HEAP, UNITS_BYTES, + sizes.mShapedWords, + "Memory used to cache shaped glyph data."); + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(gfxFontCache::Observer, nsIObserver) + +NS_IMETHODIMP +gfxFontCache::Observer::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* someData) { + if (!nsCRT::strcmp(aTopic, "memory-pressure")) { + gfxFontCache* fontCache = gfxFontCache::GetCache(); + if (fontCache) { + fontCache->FlushShapedWordCaches(); + } + } else { + MOZ_ASSERT_UNREACHABLE("unexpected notification topic"); + } + return NS_OK; +} + +nsresult gfxFontCache::Init() { + NS_ASSERTION(!gGlobalCache, "Where did this come from?"); + gGlobalCache = new gfxFontCache(GetMainThreadSerialEventTarget()); + if (!gGlobalCache) { + return NS_ERROR_OUT_OF_MEMORY; + } + RegisterStrongMemoryReporter(new MemoryReporter()); + return NS_OK; +} + +void gfxFontCache::Shutdown() { + delete gGlobalCache; + gGlobalCache = nullptr; + +#ifdef DEBUG_TEXT_RUN_STORAGE_METRICS + printf("Textrun storage high water mark=%d\n", gTextRunStorageHighWaterMark); + printf("Total number of fonts=%d\n", gFontCount); + printf("Total glyph extents allocated=%d (size %d)\n", gGlyphExtentsCount, + int(gGlyphExtentsCount * sizeof(gfxGlyphExtents))); + printf("Total glyph extents width-storage size allocated=%d\n", + gGlyphExtentsWidthsTotalSize); + printf("Number of simple glyph extents eagerly requested=%d\n", + gGlyphExtentsSetupEagerSimple); + printf("Number of tight glyph extents eagerly requested=%d\n", + gGlyphExtentsSetupEagerTight); + printf("Number of tight glyph extents lazily requested=%d\n", + gGlyphExtentsSetupLazyTight); + printf("Number of simple glyph extent setups that fell back to tight=%d\n", + gGlyphExtentsSetupFallBackToTight); +#endif +} + +gfxFontCache::gfxFontCache(nsIEventTarget* aEventTarget) + : ExpirationTrackerImpl<gfxFont, 3, Lock, AutoLock>( + FONT_TIMEOUT_SECONDS * 1000, "gfxFontCache", aEventTarget) { + nsCOMPtr<nsIObserverService> obs = GetObserverService(); + if (obs) { + obs->AddObserver(new Observer, "memory-pressure", false); + } + + nsIEventTarget* target = nullptr; + if (XRE_IsContentProcess() && NS_IsMainThread()) { + target = aEventTarget; + } + + // Create the timer used to expire shaped-word records from each font's + // cache after a short period of non-use. We have a single timer in + // gfxFontCache that loops over all fonts known to the cache, to avoid + // the overhead of individual timers in each font instance. + // The timer will be started any time shaped word records are cached + // (and pauses itself when all caches become empty). + mWordCacheExpirationTimer = NS_NewTimer(target); +} + +gfxFontCache::~gfxFontCache() { + // Ensure the user font cache releases its references to font entries, + // so they aren't kept alive after the font instances and font-list + // have been shut down. + gfxUserFontSet::UserFontCache::Shutdown(); + + if (mWordCacheExpirationTimer) { + mWordCacheExpirationTimer->Cancel(); + mWordCacheExpirationTimer = nullptr; + } + + // Expire everything manually so we don't leak them. + Flush(); +} + +bool gfxFontCache::HashEntry::KeyEquals(const KeyTypePointer aKey) const { + const gfxCharacterMap* fontUnicodeRangeMap = mFont->GetUnicodeRangeMap(); + return aKey->mFontEntry == mFont->GetFontEntry() && + aKey->mStyle->Equals(*mFont->GetStyle()) && + ((!aKey->mUnicodeRangeMap && !fontUnicodeRangeMap) || + (aKey->mUnicodeRangeMap && fontUnicodeRangeMap && + aKey->mUnicodeRangeMap->Equals(fontUnicodeRangeMap))); +} + +already_AddRefed<gfxFont> gfxFontCache::Lookup( + const gfxFontEntry* aFontEntry, const gfxFontStyle* aStyle, + const gfxCharacterMap* aUnicodeRangeMap) { + MutexAutoLock lock(mMutex); + + Key key(aFontEntry, aStyle, aUnicodeRangeMap); + HashEntry* entry = mFonts.GetEntry(key); + + Telemetry::Accumulate(Telemetry::FONT_CACHE_HIT, entry != nullptr); + + if (!entry) { + return nullptr; + } + + RefPtr<gfxFont> font = entry->mFont; + if (font->GetExpirationState()->IsTracked()) { + RemoveObjectLocked(font, lock); + } + return font.forget(); +} + +already_AddRefed<gfxFont> gfxFontCache::MaybeInsert(gfxFont* aFont) { + MOZ_ASSERT(aFont); + MutexAutoLock lock(mMutex); + + Key key(aFont->GetFontEntry(), aFont->GetStyle(), + aFont->GetUnicodeRangeMap()); + HashEntry* entry = mFonts.PutEntry(key); + if (!entry) { + return do_AddRef(aFont); + } + + // If it is null, then we are inserting a new entry. Otherwise we are + // attempting to replace an existing font, probably due to a thread race, in + // which case stick with the original font. + if (!entry->mFont) { + entry->mFont = aFont; + // Assert that we can find the entry we just put in (this fails if the key + // has a NaN float value in it, e.g. 'sizeAdjust'). + MOZ_ASSERT(entry == mFonts.GetEntry(key)); + } else { + MOZ_ASSERT(entry->mFont != aFont); + aFont->Destroy(); + if (entry->mFont->GetExpirationState()->IsTracked()) { + RemoveObjectLocked(entry->mFont, lock); + } + } + + return do_AddRef(entry->mFont); +} + +bool gfxFontCache::MaybeDestroy(gfxFont* aFont) { + MOZ_ASSERT(aFont); + MutexAutoLock lock(mMutex); + + // If the font has a non-zero refcount, then we must have lost the race with + // gfxFontCache::Lookup and the same font was reacquired. + if (aFont->GetRefCount() > 0) { + return false; + } + + Key key(aFont->GetFontEntry(), aFont->GetStyle(), + aFont->GetUnicodeRangeMap()); + HashEntry* entry = mFonts.GetEntry(key); + if (!entry || entry->mFont != aFont) { + MOZ_ASSERT(!aFont->GetExpirationState()->IsTracked()); + return true; + } + + // If the font is being tracked, we must have then also lost another race with + // gfxFontCache::MaybeDestroy which re-added it to the tracker. + if (aFont->GetExpirationState()->IsTracked()) { + return false; + } + + // Typically this won't fail, but it may during startup/shutdown if the timer + // service is not available. + nsresult rv = AddObjectLocked(aFont, lock); + if (NS_SUCCEEDED(rv)) { + return false; + } + + mFonts.RemoveEntry(entry); + return true; +} + +void gfxFontCache::NotifyExpiredLocked(gfxFont* aFont, const AutoLock& aLock) { + MOZ_ASSERT(aFont->GetRefCount() == 0); + + RemoveObjectLocked(aFont, aLock); + mTrackerDiscard.AppendElement(aFont); + + Key key(aFont->GetFontEntry(), aFont->GetStyle(), + aFont->GetUnicodeRangeMap()); + HashEntry* entry = mFonts.GetEntry(key); + if (!entry || entry->mFont != aFont) { + MOZ_ASSERT_UNREACHABLE("Invalid font?"); + return; + } + + mFonts.RemoveEntry(entry); +} + +void gfxFontCache::NotifyHandlerEnd() { + nsTArray<gfxFont*> discard; + { + MutexAutoLock lock(mMutex); + discard = std::move(mTrackerDiscard); + } + DestroyDiscard(discard); +} + +void gfxFontCache::DestroyDiscard(nsTArray<gfxFont*>& aDiscard) { + for (auto& font : aDiscard) { + NS_ASSERTION(font->GetRefCount() == 0, + "Destroying with refs outside cache!"); + font->ClearCachedWords(); + font->Destroy(); + } + aDiscard.Clear(); +} + +void gfxFontCache::Flush() { + nsTArray<gfxFont*> discard; + { + MutexAutoLock lock(mMutex); + discard.SetCapacity(mFonts.Count()); + for (auto iter = mFonts.Iter(); !iter.Done(); iter.Next()) { + HashEntry* entry = static_cast<HashEntry*>(iter.Get()); + if (!entry || !entry->mFont) { + MOZ_ASSERT_UNREACHABLE("Invalid font?"); + continue; + } + + if (entry->mFont->GetRefCount() == 0) { + // If we are not tracked, then we must have won the race with + // gfxFont::MaybeDestroy and it is waiting on the mutex. To avoid a + // double free, we let gfxFont::MaybeDestroy handle the freeing when it + // acquires the mutex and discovers there is no matching entry in the + // hashtable. + if (entry->mFont->GetExpirationState()->IsTracked()) { + RemoveObjectLocked(entry->mFont, lock); + discard.AppendElement(entry->mFont); + } + } else { + MOZ_ASSERT(!entry->mFont->GetExpirationState()->IsTracked()); + } + } + MOZ_ASSERT(IsEmptyLocked(lock), + "Cache tracker still has fonts after flush!"); + mFonts.Clear(); + } + DestroyDiscard(discard); +} + +/*static*/ +void gfxFontCache::WordCacheExpirationTimerCallback(nsITimer* aTimer, + void* aCache) { + gfxFontCache* cache = static_cast<gfxFontCache*>(aCache); + cache->AgeCachedWords(); +} + +void gfxFontCache::AgeCachedWords() { + bool allEmpty = true; + { + MutexAutoLock lock(mMutex); + for (const auto& entry : mFonts) { + allEmpty = entry.mFont->AgeCachedWords() && allEmpty; + } + } + if (allEmpty) { + PauseWordCacheExpirationTimer(); + } +} + +void gfxFontCache::FlushShapedWordCaches() { + { + MutexAutoLock lock(mMutex); + for (const auto& entry : mFonts) { + entry.mFont->ClearCachedWords(); + } + } + PauseWordCacheExpirationTimer(); +} + +void gfxFontCache::NotifyGlyphsChanged() { + MutexAutoLock lock(mMutex); + for (const auto& entry : mFonts) { + entry.mFont->NotifyGlyphsChanged(); + } +} + +void gfxFontCache::AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf, + FontCacheSizes* aSizes) const { + // TODO: add the overhead of the expiration tracker (generation arrays) + + MutexAutoLock lock(*const_cast<Mutex*>(&mMutex)); + aSizes->mFontInstances += mFonts.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (const auto& entry : mFonts) { + entry.mFont->AddSizeOfExcludingThis(aMallocSizeOf, aSizes); + } +} + +void gfxFontCache::AddSizeOfIncludingThis(MallocSizeOf aMallocSizeOf, + FontCacheSizes* aSizes) const { + aSizes->mFontInstances += aMallocSizeOf(this); + AddSizeOfExcludingThis(aMallocSizeOf, aSizes); +} + +#define MAX_SSXX_VALUE 99 +#define MAX_CVXX_VALUE 99 + +static void LookupAlternateValues(const gfxFontFeatureValueSet& aFeatureLookup, + const nsACString& aFamily, + const StyleVariantAlternates& aAlternates, + nsTArray<gfxFontFeature>& aFontFeatures) { + using Tag = StyleVariantAlternates::Tag; + + // historical-forms gets handled in nsFont::AddFontFeaturesToStyle. + if (aAlternates.IsHistoricalForms()) { + return; + } + + gfxFontFeature feature; + if (aAlternates.IsCharacterVariant()) { + for (auto& ident : aAlternates.AsCharacterVariant().AsSpan()) { + Span<const uint32_t> values = aFeatureLookup.GetFontFeatureValuesFor( + aFamily, NS_FONT_VARIANT_ALTERNATES_CHARACTER_VARIANT, + ident.AsAtom()); + // nothing defined, skip + if (values.IsEmpty()) { + continue; + } + NS_ASSERTION(values.Length() <= 2, + "too many values allowed for character-variant"); + // character-variant(12 3) ==> 'cv12' = 3 + uint32_t nn = values[0]; + // ignore values greater than 99 + if (nn == 0 || nn > MAX_CVXX_VALUE) { + continue; + } + feature.mValue = values.Length() > 1 ? values[1] : 1; + feature.mTag = HB_TAG('c', 'v', ('0' + nn / 10), ('0' + nn % 10)); + aFontFeatures.AppendElement(feature); + } + return; + } + + if (aAlternates.IsStyleset()) { + for (auto& ident : aAlternates.AsStyleset().AsSpan()) { + Span<const uint32_t> values = aFeatureLookup.GetFontFeatureValuesFor( + aFamily, NS_FONT_VARIANT_ALTERNATES_STYLESET, ident.AsAtom()); + + // styleset(1 2 7) ==> 'ss01' = 1, 'ss02' = 1, 'ss07' = 1 + feature.mValue = 1; + for (uint32_t nn : values) { + if (nn == 0 || nn > MAX_SSXX_VALUE) { + continue; + } + feature.mTag = HB_TAG('s', 's', ('0' + nn / 10), ('0' + nn % 10)); + aFontFeatures.AppendElement(feature); + } + } + return; + } + + uint32_t constant = 0; + nsAtom* name = nullptr; + switch (aAlternates.tag) { + case Tag::Swash: + constant = NS_FONT_VARIANT_ALTERNATES_SWASH; + name = aAlternates.AsSwash().AsAtom(); + break; + case Tag::Stylistic: + constant = NS_FONT_VARIANT_ALTERNATES_STYLISTIC; + name = aAlternates.AsStylistic().AsAtom(); + break; + case Tag::Ornaments: + constant = NS_FONT_VARIANT_ALTERNATES_ORNAMENTS; + name = aAlternates.AsOrnaments().AsAtom(); + break; + case Tag::Annotation: + constant = NS_FONT_VARIANT_ALTERNATES_ANNOTATION; + name = aAlternates.AsAnnotation().AsAtom(); + break; + default: + MOZ_ASSERT_UNREACHABLE("Unknown font-variant-alternates value!"); + return; + } + + Span<const uint32_t> values = + aFeatureLookup.GetFontFeatureValuesFor(aFamily, constant, name); + if (values.IsEmpty()) { + return; + } + MOZ_ASSERT(values.Length() == 1, + "too many values for font-specific font-variant-alternates"); + + feature.mValue = values[0]; + switch (aAlternates.tag) { + case Tag::Swash: // swsh, cswh + feature.mTag = HB_TAG('s', 'w', 's', 'h'); + aFontFeatures.AppendElement(feature); + feature.mTag = HB_TAG('c', 's', 'w', 'h'); + break; + case Tag::Stylistic: // salt + feature.mTag = HB_TAG('s', 'a', 'l', 't'); + break; + case Tag::Ornaments: // ornm + feature.mTag = HB_TAG('o', 'r', 'n', 'm'); + break; + case Tag::Annotation: // nalt + feature.mTag = HB_TAG('n', 'a', 'l', 't'); + break; + default: + MOZ_ASSERT_UNREACHABLE("how?"); + return; + } + aFontFeatures.AppendElement(feature); +} + +/* static */ +void gfxFontShaper::MergeFontFeatures( + const gfxFontStyle* aStyle, const nsTArray<gfxFontFeature>& aFontFeatures, + bool aDisableLigatures, const nsACString& aFamilyName, bool aAddSmallCaps, + void (*aHandleFeature)(const uint32_t&, uint32_t&, void*), + void* aHandleFeatureData) { + const nsTArray<gfxFontFeature>& styleRuleFeatures = aStyle->featureSettings; + + // Bail immediately if nothing to do, which is the common case. + if (styleRuleFeatures.IsEmpty() && aFontFeatures.IsEmpty() && + !aDisableLigatures && + aStyle->variantCaps == NS_FONT_VARIANT_CAPS_NORMAL && + aStyle->variantSubSuper == NS_FONT_VARIANT_POSITION_NORMAL && + aStyle->variantAlternates.IsEmpty()) { + return; + } + + nsTHashMap<nsUint32HashKey, uint32_t> mergedFeatures; + + // add feature values from font + for (const gfxFontFeature& feature : aFontFeatures) { + mergedFeatures.InsertOrUpdate(feature.mTag, feature.mValue); + } + + // font-variant-caps - handled here due to the need for fallback handling + // petite caps cases can fallback to appropriate smallcaps + uint32_t variantCaps = aStyle->variantCaps; + switch (variantCaps) { + case NS_FONT_VARIANT_CAPS_NORMAL: + break; + + case NS_FONT_VARIANT_CAPS_ALLSMALL: + mergedFeatures.InsertOrUpdate(HB_TAG('c', '2', 's', 'c'), 1); + // fall through to the small-caps case + [[fallthrough]]; + + case NS_FONT_VARIANT_CAPS_SMALLCAPS: + mergedFeatures.InsertOrUpdate(HB_TAG('s', 'm', 'c', 'p'), 1); + break; + + case NS_FONT_VARIANT_CAPS_ALLPETITE: + mergedFeatures.InsertOrUpdate(aAddSmallCaps ? HB_TAG('c', '2', 's', 'c') + : HB_TAG('c', '2', 'p', 'c'), + 1); + // fall through to the petite-caps case + [[fallthrough]]; + + case NS_FONT_VARIANT_CAPS_PETITECAPS: + mergedFeatures.InsertOrUpdate(aAddSmallCaps ? HB_TAG('s', 'm', 'c', 'p') + : HB_TAG('p', 'c', 'a', 'p'), + 1); + break; + + case NS_FONT_VARIANT_CAPS_TITLING: + mergedFeatures.InsertOrUpdate(HB_TAG('t', 'i', 't', 'l'), 1); + break; + + case NS_FONT_VARIANT_CAPS_UNICASE: + mergedFeatures.InsertOrUpdate(HB_TAG('u', 'n', 'i', 'c'), 1); + break; + + default: + MOZ_ASSERT_UNREACHABLE("Unexpected variantCaps"); + break; + } + + // font-variant-position - handled here due to the need for fallback + switch (aStyle->variantSubSuper) { + case NS_FONT_VARIANT_POSITION_NORMAL: + break; + case NS_FONT_VARIANT_POSITION_SUPER: + mergedFeatures.InsertOrUpdate(HB_TAG('s', 'u', 'p', 's'), 1); + break; + case NS_FONT_VARIANT_POSITION_SUB: + mergedFeatures.InsertOrUpdate(HB_TAG('s', 'u', 'b', 's'), 1); + break; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected variantSubSuper"); + break; + } + + // add font-specific feature values from style rules + if (aStyle->featureValueLookup && !aStyle->variantAlternates.IsEmpty()) { + AutoTArray<gfxFontFeature, 4> featureList; + + // insert list of alternate feature settings + for (auto& alternate : aStyle->variantAlternates.AsSpan()) { + LookupAlternateValues(*aStyle->featureValueLookup, aFamilyName, alternate, + featureList); + } + + for (const gfxFontFeature& feature : featureList) { + mergedFeatures.InsertOrUpdate(feature.mTag, feature.mValue); + } + } + + auto disableOptionalLigatures = [&]() -> void { + mergedFeatures.InsertOrUpdate(HB_TAG('l', 'i', 'g', 'a'), 0); + mergedFeatures.InsertOrUpdate(HB_TAG('c', 'l', 'i', 'g'), 0); + mergedFeatures.InsertOrUpdate(HB_TAG('d', 'l', 'i', 'g'), 0); + mergedFeatures.InsertOrUpdate(HB_TAG('h', 'l', 'i', 'g'), 0); + }; + + // Add features that are already resolved to tags & values in the style. + if (styleRuleFeatures.IsEmpty()) { + // Disable optional ligatures if non-zero letter-spacing is in effect. + if (aDisableLigatures) { + disableOptionalLigatures(); + } + } else { + for (const gfxFontFeature& feature : styleRuleFeatures) { + // A dummy feature (0,0) is used as a sentinel to separate features + // originating from font-variant-* or other high-level properties from + // those directly specified as font-feature-settings. The high-level + // features may be overridden by aDisableLigatures, while low-level + // features specified directly as tags will come last and therefore + // take precedence over everything else. + if (feature.mTag) { + mergedFeatures.InsertOrUpdate(feature.mTag, feature.mValue); + } else if (aDisableLigatures) { + // Handle ligature-disabling setting at the boundary between high- + // and low-level features. + disableOptionalLigatures(); + } + } + } + + if (mergedFeatures.Count() != 0) { + for (auto iter = mergedFeatures.Iter(); !iter.Done(); iter.Next()) { + aHandleFeature(iter.Key(), iter.Data(), aHandleFeatureData); + } + } +} + +void gfxShapedText::SetupClusterBoundaries(uint32_t aOffset, + const char16_t* aString, + uint32_t aLength) { + if (aLength == 0) { + return; + } + + CompressedGlyph* const glyphs = GetCharacterGlyphs() + aOffset; + CompressedGlyph extendCluster = CompressedGlyph::MakeComplex(false, true); + + // GraphemeClusterBreakIteratorUtf16 won't be able to tell us if the string + // _begins_ with a cluster-extender, so we handle that here + uint32_t ch = aString[0]; + if (aLength > 1 && NS_IS_SURROGATE_PAIR(ch, aString[1])) { + ch = SURROGATE_TO_UCS4(ch, aString[1]); + } + if (IsClusterExtender(ch)) { + glyphs[0] = extendCluster; + } + + intl::GraphemeClusterBreakIteratorUtf16 iter( + Span<const char16_t>(aString, aLength)); + uint32_t pos = 0; + + const char16_t kIdeographicSpace = 0x3000; + // Special case for Bengali: although Virama normally clusters with the + // preceding letter, we *also* want to cluster it with a following Ya + // so that when the Virama+Ya form ya-phala, this is not separated from the + // preceding letter by any letter-spacing or justification. + const char16_t kBengaliVirama = 0x09CD; + const char16_t kBengaliYa = 0x09AF; + while (pos < aLength) { + const char16_t ch = aString[pos]; + if (ch == char16_t(' ') || ch == kIdeographicSpace) { + glyphs[pos].SetIsSpace(); + } else if (ch == kBengaliYa) { + // Unless we're at the start, check for a preceding virama. + if (pos > 0 && aString[pos - 1] == kBengaliVirama) { + glyphs[pos] = extendCluster; + } + } + // advance iter to the next cluster-start (or end of text) + const uint32_t nextPos = *iter.Next(); + // step past the first char of the cluster + ++pos; + // mark all the rest as cluster-continuations + for (; pos < nextPos; ++pos) { + glyphs[pos] = extendCluster; + } + } +} + +void gfxShapedText::SetupClusterBoundaries(uint32_t aOffset, + const uint8_t* aString, + uint32_t aLength) { + CompressedGlyph* glyphs = GetCharacterGlyphs() + aOffset; + const uint8_t* limit = aString + aLength; + + while (aString < limit) { + if (*aString == uint8_t(' ')) { + glyphs->SetIsSpace(); + } + aString++; + glyphs++; + } +} + +gfxShapedText::DetailedGlyph* gfxShapedText::AllocateDetailedGlyphs( + uint32_t aIndex, uint32_t aCount) { + NS_ASSERTION(aIndex < GetLength(), "Index out of range"); + + if (!mDetailedGlyphs) { + mDetailedGlyphs = MakeUnique<DetailedGlyphStore>(); + } + + return mDetailedGlyphs->Allocate(aIndex, aCount); +} + +void gfxShapedText::SetDetailedGlyphs(uint32_t aIndex, uint32_t aGlyphCount, + const DetailedGlyph* aGlyphs) { + CompressedGlyph& g = GetCharacterGlyphs()[aIndex]; + + MOZ_ASSERT(aIndex > 0 || g.IsLigatureGroupStart(), + "First character can't be a ligature continuation!"); + + if (aGlyphCount > 0) { + DetailedGlyph* details = AllocateDetailedGlyphs(aIndex, aGlyphCount); + memcpy(details, aGlyphs, sizeof(DetailedGlyph) * aGlyphCount); + } + + g.SetGlyphCount(aGlyphCount); +} + +#define ZWNJ 0x200C +#define ZWJ 0x200D +static inline bool IsIgnorable(uint32_t aChar) { + return (IsDefaultIgnorable(aChar)) || aChar == ZWNJ || aChar == ZWJ; +} + +void gfxShapedText::SetMissingGlyph(uint32_t aIndex, uint32_t aChar, + gfxFont* aFont) { + CompressedGlyph& g = GetCharacterGlyphs()[aIndex]; + uint8_t category = GetGeneralCategory(aChar); + if (category >= HB_UNICODE_GENERAL_CATEGORY_SPACING_MARK && + category <= HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK) { + g.SetComplex(false, true); + } + + // Leaving advance as zero will prevent drawing the hexbox for ignorables. + int32_t advance = 0; + if (!IsIgnorable(aChar)) { + gfxFloat width = + std::max(aFont->GetMetrics(nsFontMetrics::eHorizontal).aveCharWidth, + gfxFloat(gfxFontMissingGlyphs::GetDesiredMinWidth( + aChar, mAppUnitsPerDevUnit))); + advance = int32_t(width * mAppUnitsPerDevUnit); + } + DetailedGlyph detail = {aChar, advance, gfx::Point()}; + SetDetailedGlyphs(aIndex, 1, &detail); + g.SetMissing(); +} + +bool gfxShapedText::FilterIfIgnorable(uint32_t aIndex, uint32_t aCh) { + if (IsIgnorable(aCh)) { + // There are a few default-ignorables of Letter category (currently, + // just the Hangul filler characters) that we'd better not discard + // if they're followed by additional characters in the same cluster. + // Some fonts use them to carry the width of a whole cluster of + // combining jamos; see bug 1238243. + auto* charGlyphs = GetCharacterGlyphs(); + if (GetGenCategory(aCh) == nsUGenCategory::kLetter && + aIndex + 1 < GetLength() && !charGlyphs[aIndex + 1].IsClusterStart()) { + return false; + } + // A compressedGlyph that is set to MISSING but has no DetailedGlyphs list + // will be zero-width/invisible, which is what we want here. + CompressedGlyph& g = charGlyphs[aIndex]; + g.SetComplex(g.IsClusterStart(), g.IsLigatureGroupStart()).SetMissing(); + return true; + } + return false; +} + +void gfxShapedText::AdjustAdvancesForSyntheticBold(float aSynBoldOffset, + uint32_t aOffset, + uint32_t aLength) { + int32_t synAppUnitOffset = aSynBoldOffset * mAppUnitsPerDevUnit; + CompressedGlyph* charGlyphs = GetCharacterGlyphs(); + for (uint32_t i = aOffset; i < aOffset + aLength; ++i) { + CompressedGlyph* glyphData = charGlyphs + i; + if (glyphData->IsSimpleGlyph()) { + // simple glyphs ==> just add the advance + int32_t advance = glyphData->GetSimpleAdvance(); + if (advance > 0) { + advance += synAppUnitOffset; + if (CompressedGlyph::IsSimpleAdvance(advance)) { + glyphData->SetSimpleGlyph(advance, glyphData->GetSimpleGlyph()); + } else { + // rare case, tested by making this the default + uint32_t glyphIndex = glyphData->GetSimpleGlyph(); + // convert the simple CompressedGlyph to an empty complex record + glyphData->SetComplex(true, true); + // then set its details (glyph ID with its new advance) + DetailedGlyph detail = {glyphIndex, advance, gfx::Point()}; + SetDetailedGlyphs(i, 1, &detail); + } + } + } else { + // complex glyphs ==> add offset at cluster/ligature boundaries + uint32_t detailedLength = glyphData->GetGlyphCount(); + if (detailedLength) { + DetailedGlyph* details = GetDetailedGlyphs(i); + if (!details) { + continue; + } + if (IsRightToLeft()) { + if (details[0].mAdvance > 0) { + details[0].mAdvance += synAppUnitOffset; + } + } else { + if (details[detailedLength - 1].mAdvance > 0) { + details[detailedLength - 1].mAdvance += synAppUnitOffset; + } + } + } + } + } +} + +float gfxFont::AngleForSyntheticOblique() const { + // First check conditions that mean no synthetic slant should be used: + if (mStyle.style == FontSlantStyle::NORMAL) { + return 0.0f; // Requested style is 'normal'. + } + if (!mStyle.allowSyntheticStyle) { + return 0.0f; // Synthetic obliquing is disabled. + } + if (!mFontEntry->MayUseSyntheticSlant()) { + return 0.0f; // The resource supports "real" slant, so don't synthesize. + } + + // If style calls for italic, and face doesn't support it, use default + // oblique angle as a simulation. + if (mStyle.style.IsItalic()) { + return mFontEntry->SupportsItalic() + ? 0.0f + : FontSlantStyle::DEFAULT_OBLIQUE_DEGREES; + } + + // OK, we're going to use synthetic oblique: return the requested angle. + return mStyle.style.ObliqueAngle(); +} + +float gfxFont::SkewForSyntheticOblique() const { + // Precomputed value of tan(kDefaultAngle), the default italic/oblique slant; + // avoids calling tan() at runtime except for custom oblique values. + static const float kTanDefaultAngle = + tan(FontSlantStyle::DEFAULT_OBLIQUE_DEGREES * (M_PI / 180.0)); + + float angle = AngleForSyntheticOblique(); + if (angle == 0.0f) { + return 0.0f; + } else if (angle == FontSlantStyle::DEFAULT_OBLIQUE_DEGREES) { + return kTanDefaultAngle; + } else { + return tan(angle * (M_PI / 180.0)); + } +} + +void gfxFont::RunMetrics::CombineWith(const RunMetrics& aOther, + bool aOtherIsOnLeft) { + mAscent = std::max(mAscent, aOther.mAscent); + mDescent = std::max(mDescent, aOther.mDescent); + if (aOtherIsOnLeft) { + mBoundingBox = (mBoundingBox + gfxPoint(aOther.mAdvanceWidth, 0)) + .Union(aOther.mBoundingBox); + } else { + mBoundingBox = + mBoundingBox.Union(aOther.mBoundingBox + gfxPoint(mAdvanceWidth, 0)); + } + mAdvanceWidth += aOther.mAdvanceWidth; +} + +gfxFont::gfxFont(const RefPtr<UnscaledFont>& aUnscaledFont, + gfxFontEntry* aFontEntry, const gfxFontStyle* aFontStyle, + AntialiasOption anAAOption) + : mFontEntry(aFontEntry), + mLock("gfxFont lock"), + mUnscaledFont(aUnscaledFont), + mStyle(*aFontStyle), + mAdjustedSize(-1.0), // negative to indicate "not yet initialized" + mFUnitsConvFactor(-1.0f), // negative to indicate "not yet initialized" + mAntialiasOption(anAAOption), + mIsValid(true), + mApplySyntheticBold(false), + mKerningEnabled(false), + mMathInitialized(false) { +#ifdef DEBUG_TEXT_RUN_STORAGE_METRICS + ++gFontCount; +#endif + + if (MOZ_UNLIKELY(StaticPrefs::gfx_text_disable_aa_AtStartup())) { + mAntialiasOption = kAntialiasNone; + } + + // Turn off AA for Ahem for testing purposes when requested. + if (MOZ_UNLIKELY(StaticPrefs::gfx_font_rendering_ahem_antialias_none() && + mFontEntry->FamilyName().EqualsLiteral("Ahem"))) { + mAntialiasOption = kAntialiasNone; + } + + mKerningSet = HasFeatureSet(HB_TAG('k', 'e', 'r', 'n'), mKerningEnabled); +} + +gfxFont::~gfxFont() { + mFontEntry->NotifyFontDestroyed(this); + + // Delete objects owned through atomic pointers. (Some of these may be null, + // but that's OK.) + delete mVerticalMetrics.exchange(nullptr); + delete mHarfBuzzShaper.exchange(nullptr); + delete mGraphiteShaper.exchange(nullptr); + delete mMathTable.exchange(nullptr); + delete mNonAAFont.exchange(nullptr); + + if (auto* scaledFont = mAzureScaledFont.exchange(nullptr)) { + scaledFont->Release(); + } + + if (mGlyphChangeObservers) { + for (const auto& key : *mGlyphChangeObservers) { + key->ForgetFont(); + } + } +} + +// Work out whether cairo will snap inter-glyph spacing to pixels. +// +// Layout does not align text to pixel boundaries, so, with font drawing +// backends that snap glyph positions to pixels, it is important that +// inter-glyph spacing within words is always an integer number of pixels. +// This ensures that the drawing backend snaps all of the word's glyphs in the +// same direction and so inter-glyph spacing remains the same. +// +gfxFont::RoundingFlags gfxFont::GetRoundOffsetsToPixels( + DrawTarget* aDrawTarget) { + // Could do something fancy here for ScaleFactors of + // AxisAlignedTransforms, but we leave things simple. + // Not much point rounding if a matrix will mess things up anyway. + // Also check if the font already knows hint metrics is off... + if (aDrawTarget->GetTransform().HasNonTranslation() || !ShouldHintMetrics()) { + return RoundingFlags(0); + } + + cairo_t* cr = static_cast<cairo_t*>( + aDrawTarget->GetNativeSurface(NativeSurfaceType::CAIRO_CONTEXT)); + if (cr) { + cairo_surface_t* target = cairo_get_target(cr); + + // Check whether the cairo surface's font options hint metrics. + cairo_font_options_t* fontOptions = cairo_font_options_create(); + cairo_surface_get_font_options(target, fontOptions); + cairo_hint_metrics_t hintMetrics = + cairo_font_options_get_hint_metrics(fontOptions); + cairo_font_options_destroy(fontOptions); + + switch (hintMetrics) { + case CAIRO_HINT_METRICS_OFF: + return RoundingFlags(0); + case CAIRO_HINT_METRICS_ON: + return RoundingFlags::kRoundX | RoundingFlags::kRoundY; + default: + break; + } + } + + if (ShouldRoundXOffset(cr)) { + return RoundingFlags::kRoundX | RoundingFlags::kRoundY; + } else { + return RoundingFlags::kRoundY; + } +} + +gfxHarfBuzzShaper* gfxFont::GetHarfBuzzShaper() { + if (!mHarfBuzzShaper) { + auto* shaper = new gfxHarfBuzzShaper(this); + shaper->Initialize(); + if (!mHarfBuzzShaper.compareExchange(nullptr, shaper)) { + delete shaper; + } + } + gfxHarfBuzzShaper* shaper = mHarfBuzzShaper; + return shaper->IsInitialized() ? shaper : nullptr; +} + +gfxFloat gfxFont::GetGlyphAdvance(uint16_t aGID, bool aVertical) { + if (!aVertical && ProvidesGlyphWidths()) { + return GetGlyphWidth(aGID) / 65536.0; + } + if (mFUnitsConvFactor < 0.0f) { + // Metrics haven't been initialized; lock while we do that. + AutoWriteLock lock(mLock); + if (mFUnitsConvFactor < 0.0f) { + GetMetrics(nsFontMetrics::eHorizontal); + } + } + NS_ASSERTION(mFUnitsConvFactor >= 0.0f, + "missing font unit conversion factor"); + if (gfxHarfBuzzShaper* shaper = GetHarfBuzzShaper()) { + if (aVertical) { + // Note that GetGlyphVAdvance may return -1 to indicate it was unable + // to retrieve vertical metrics; in that case we fall back to the + // aveCharWidth value as a default advance. + int32_t advance = shaper->GetGlyphVAdvance(aGID); + if (advance < 0) { + return GetMetrics(nsFontMetrics::eVertical).aveCharWidth; + } + return advance / 65536.0; + } + return shaper->GetGlyphHAdvance(aGID) / 65536.0; + } + return 0.0; +} + +gfxFloat gfxFont::GetCharAdvance(uint32_t aUnicode, bool aVertical) { + uint32_t gid = 0; + if (ProvidesGetGlyph()) { + gid = GetGlyph(aUnicode, 0); + } else { + if (gfxHarfBuzzShaper* shaper = GetHarfBuzzShaper()) { + gid = shaper->GetNominalGlyph(aUnicode); + } + } + if (!gid) { + return -1.0; + } + return GetGlyphAdvance(gid, aVertical); +} + +static void CollectLookupsByFeature(hb_face_t* aFace, hb_tag_t aTableTag, + uint32_t aFeatureIndex, + hb_set_t* aLookups) { + uint32_t lookups[32]; + uint32_t i, len, offset; + + offset = 0; + do { + len = ArrayLength(lookups); + hb_ot_layout_feature_get_lookups(aFace, aTableTag, aFeatureIndex, offset, + &len, lookups); + for (i = 0; i < len; i++) { + hb_set_add(aLookups, lookups[i]); + } + offset += len; + } while (len == ArrayLength(lookups)); +} + +static void CollectLookupsByLanguage( + hb_face_t* aFace, hb_tag_t aTableTag, + const nsTHashSet<uint32_t>& aSpecificFeatures, hb_set_t* aOtherLookups, + hb_set_t* aSpecificFeatureLookups, uint32_t aScriptIndex, + uint32_t aLangIndex) { + uint32_t reqFeatureIndex; + if (hb_ot_layout_language_get_required_feature_index( + aFace, aTableTag, aScriptIndex, aLangIndex, &reqFeatureIndex)) { + CollectLookupsByFeature(aFace, aTableTag, reqFeatureIndex, aOtherLookups); + } + + uint32_t featureIndexes[32]; + uint32_t i, len, offset; + + offset = 0; + do { + len = ArrayLength(featureIndexes); + hb_ot_layout_language_get_feature_indexes(aFace, aTableTag, aScriptIndex, + aLangIndex, offset, &len, + featureIndexes); + + for (i = 0; i < len; i++) { + uint32_t featureIndex = featureIndexes[i]; + + // get the feature tag + hb_tag_t featureTag; + uint32_t tagLen = 1; + hb_ot_layout_language_get_feature_tags(aFace, aTableTag, aScriptIndex, + aLangIndex, offset + i, &tagLen, + &featureTag); + + // collect lookups + hb_set_t* lookups = aSpecificFeatures.Contains(featureTag) + ? aSpecificFeatureLookups + : aOtherLookups; + CollectLookupsByFeature(aFace, aTableTag, featureIndex, lookups); + } + offset += len; + } while (len == ArrayLength(featureIndexes)); +} + +static bool HasLookupRuleWithGlyphByScript( + hb_face_t* aFace, hb_tag_t aTableTag, hb_tag_t aScriptTag, + uint32_t aScriptIndex, uint16_t aGlyph, + const nsTHashSet<uint32_t>& aDefaultFeatures, + bool& aHasDefaultFeatureWithGlyph) { + uint32_t numLangs, lang; + hb_set_t* defaultFeatureLookups = hb_set_create(); + hb_set_t* nonDefaultFeatureLookups = hb_set_create(); + + // default lang + CollectLookupsByLanguage(aFace, aTableTag, aDefaultFeatures, + nonDefaultFeatureLookups, defaultFeatureLookups, + aScriptIndex, HB_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX); + + // iterate over langs + numLangs = hb_ot_layout_script_get_language_tags( + aFace, aTableTag, aScriptIndex, 0, nullptr, nullptr); + for (lang = 0; lang < numLangs; lang++) { + CollectLookupsByLanguage(aFace, aTableTag, aDefaultFeatures, + nonDefaultFeatureLookups, defaultFeatureLookups, + aScriptIndex, lang); + } + + // look for the glyph among default feature lookups + aHasDefaultFeatureWithGlyph = false; + hb_set_t* glyphs = hb_set_create(); + hb_codepoint_t index = -1; + while (hb_set_next(defaultFeatureLookups, &index)) { + hb_ot_layout_lookup_collect_glyphs(aFace, aTableTag, index, glyphs, glyphs, + glyphs, nullptr); + if (hb_set_has(glyphs, aGlyph)) { + aHasDefaultFeatureWithGlyph = true; + break; + } + } + + // look for the glyph among non-default feature lookups + // if no default feature lookups contained spaces + bool hasNonDefaultFeatureWithGlyph = false; + if (!aHasDefaultFeatureWithGlyph) { + hb_set_clear(glyphs); + index = -1; + while (hb_set_next(nonDefaultFeatureLookups, &index)) { + hb_ot_layout_lookup_collect_glyphs(aFace, aTableTag, index, glyphs, + glyphs, glyphs, nullptr); + if (hb_set_has(glyphs, aGlyph)) { + hasNonDefaultFeatureWithGlyph = true; + break; + } + } + } + + hb_set_destroy(glyphs); + hb_set_destroy(defaultFeatureLookups); + hb_set_destroy(nonDefaultFeatureLookups); + + return aHasDefaultFeatureWithGlyph || hasNonDefaultFeatureWithGlyph; +} + +static void HasLookupRuleWithGlyph(hb_face_t* aFace, hb_tag_t aTableTag, + bool& aHasGlyph, hb_tag_t aSpecificFeature, + bool& aHasGlyphSpecific, uint16_t aGlyph) { + // iterate over the scripts in the font + uint32_t numScripts, numLangs, script, lang; + hb_set_t* otherLookups = hb_set_create(); + hb_set_t* specificFeatureLookups = hb_set_create(); + nsTHashSet<uint32_t> specificFeature(1); + + specificFeature.Insert(aSpecificFeature); + + numScripts = + hb_ot_layout_table_get_script_tags(aFace, aTableTag, 0, nullptr, nullptr); + + for (script = 0; script < numScripts; script++) { + // default lang + CollectLookupsByLanguage(aFace, aTableTag, specificFeature, otherLookups, + specificFeatureLookups, script, + HB_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX); + + // iterate over langs + numLangs = hb_ot_layout_script_get_language_tags( + aFace, HB_OT_TAG_GPOS, script, 0, nullptr, nullptr); + for (lang = 0; lang < numLangs; lang++) { + CollectLookupsByLanguage(aFace, aTableTag, specificFeature, otherLookups, + specificFeatureLookups, script, lang); + } + } + + // look for the glyph among non-specific feature lookups + hb_set_t* glyphs = hb_set_create(); + hb_codepoint_t index = -1; + while (hb_set_next(otherLookups, &index)) { + hb_ot_layout_lookup_collect_glyphs(aFace, aTableTag, index, glyphs, glyphs, + glyphs, nullptr); + if (hb_set_has(glyphs, aGlyph)) { + aHasGlyph = true; + break; + } + } + + // look for the glyph among specific feature lookups + hb_set_clear(glyphs); + index = -1; + while (hb_set_next(specificFeatureLookups, &index)) { + hb_ot_layout_lookup_collect_glyphs(aFace, aTableTag, index, glyphs, glyphs, + glyphs, nullptr); + if (hb_set_has(glyphs, aGlyph)) { + aHasGlyphSpecific = true; + break; + } + } + + hb_set_destroy(glyphs); + hb_set_destroy(specificFeatureLookups); + hb_set_destroy(otherLookups); +} + +Atomic<nsTHashMap<nsUint32HashKey, intl::Script>*> gfxFont::sScriptTagToCode; +Atomic<nsTHashSet<uint32_t>*> gfxFont::sDefaultFeatures; + +static inline bool HasSubstitution(uint32_t* aBitVector, intl::Script aScript) { + return (aBitVector[static_cast<uint32_t>(aScript) >> 5] & + (1 << (static_cast<uint32_t>(aScript) & 0x1f))) != 0; +} + +// union of all default substitution features across scripts +static const hb_tag_t defaultFeatures[] = { + HB_TAG('a', 'b', 'v', 'f'), HB_TAG('a', 'b', 'v', 's'), + HB_TAG('a', 'k', 'h', 'n'), HB_TAG('b', 'l', 'w', 'f'), + HB_TAG('b', 'l', 'w', 's'), HB_TAG('c', 'a', 'l', 't'), + HB_TAG('c', 'c', 'm', 'p'), HB_TAG('c', 'f', 'a', 'r'), + HB_TAG('c', 'j', 'c', 't'), HB_TAG('c', 'l', 'i', 'g'), + HB_TAG('f', 'i', 'n', '2'), HB_TAG('f', 'i', 'n', '3'), + HB_TAG('f', 'i', 'n', 'a'), HB_TAG('h', 'a', 'l', 'f'), + HB_TAG('h', 'a', 'l', 'n'), HB_TAG('i', 'n', 'i', 't'), + HB_TAG('i', 's', 'o', 'l'), HB_TAG('l', 'i', 'g', 'a'), + HB_TAG('l', 'j', 'm', 'o'), HB_TAG('l', 'o', 'c', 'l'), + HB_TAG('l', 't', 'r', 'a'), HB_TAG('l', 't', 'r', 'm'), + HB_TAG('m', 'e', 'd', '2'), HB_TAG('m', 'e', 'd', 'i'), + HB_TAG('m', 's', 'e', 't'), HB_TAG('n', 'u', 'k', 't'), + HB_TAG('p', 'r', 'e', 'f'), HB_TAG('p', 'r', 'e', 's'), + HB_TAG('p', 's', 't', 'f'), HB_TAG('p', 's', 't', 's'), + HB_TAG('r', 'c', 'l', 't'), HB_TAG('r', 'l', 'i', 'g'), + HB_TAG('r', 'k', 'r', 'f'), HB_TAG('r', 'p', 'h', 'f'), + HB_TAG('r', 't', 'l', 'a'), HB_TAG('r', 't', 'l', 'm'), + HB_TAG('t', 'j', 'm', 'o'), HB_TAG('v', 'a', 't', 'u'), + HB_TAG('v', 'e', 'r', 't'), HB_TAG('v', 'j', 'm', 'o')}; + +void gfxFont::CheckForFeaturesInvolvingSpace() const { + gfxFontEntry::SpaceFeatures flags = gfxFontEntry::SpaceFeatures::None; + + auto setFlags = + MakeScopeExit([&]() { mFontEntry->mHasSpaceFeatures = flags; }); + + bool log = LOG_FONTINIT_ENABLED(); + TimeStamp start; + if (MOZ_UNLIKELY(log)) { + start = TimeStamp::Now(); + } + + uint32_t spaceGlyph = GetSpaceGlyph(); + if (!spaceGlyph) { + return; + } + + auto face(GetFontEntry()->GetHBFace()); + + // GSUB lookups - examine per script + if (hb_ot_layout_has_substitution(face)) { + // Get the script ==> code hashtable, creating it on first use. + nsTHashMap<nsUint32HashKey, Script>* tagToCode = sScriptTagToCode; + if (!tagToCode) { + tagToCode = new nsTHashMap<nsUint32HashKey, Script>( + size_t(Script::NUM_SCRIPT_CODES)); + tagToCode->InsertOrUpdate(HB_TAG('D', 'F', 'L', 'T'), Script::COMMON); + // Ensure that we don't try to look at script codes beyond what the + // current version of ICU (at runtime -- in case of system ICU) + // knows about. + Script scriptCount = Script( + std::min<int>(intl::UnicodeProperties::GetMaxNumberOfScripts() + 1, + int(Script::NUM_SCRIPT_CODES))); + for (Script s = Script::ARABIC; s < scriptCount; + s = Script(static_cast<int>(s) + 1)) { + hb_script_t script = hb_script_t(GetScriptTagForCode(s)); + unsigned int scriptCount = 4; + hb_tag_t scriptTags[4]; + hb_ot_tags_from_script_and_language(script, HB_LANGUAGE_INVALID, + &scriptCount, scriptTags, nullptr, + nullptr); + for (unsigned int i = 0; i < scriptCount; i++) { + tagToCode->InsertOrUpdate(scriptTags[i], s); + } + } + if (!sScriptTagToCode.compareExchange(nullptr, tagToCode)) { + // We lost a race! Discard our new table and use the winner. + delete tagToCode; + tagToCode = sScriptTagToCode; + } + } + + // Set up the default-features hashset on first use. + if (!sDefaultFeatures) { + uint32_t numDefaultFeatures = ArrayLength(defaultFeatures); + auto* set = new nsTHashSet<uint32_t>(numDefaultFeatures); + for (uint32_t i = 0; i < numDefaultFeatures; i++) { + set->Insert(defaultFeatures[i]); + } + if (!sDefaultFeatures.compareExchange(nullptr, set)) { + delete set; + } + } + + // iterate over the scripts in the font + hb_tag_t scriptTags[8]; + + uint32_t len, offset = 0; + do { + len = ArrayLength(scriptTags); + hb_ot_layout_table_get_script_tags(face, HB_OT_TAG_GSUB, offset, &len, + scriptTags); + for (uint32_t i = 0; i < len; i++) { + bool isDefaultFeature = false; + Script s; + if (!HasLookupRuleWithGlyphByScript( + face, HB_OT_TAG_GSUB, scriptTags[i], offset + i, spaceGlyph, + *sDefaultFeatures, isDefaultFeature) || + !tagToCode->Get(scriptTags[i], &s)) { + continue; + } + flags = flags | gfxFontEntry::SpaceFeatures::HasFeatures; + uint32_t index = static_cast<uint32_t>(s) >> 5; + uint32_t bit = static_cast<uint32_t>(s) & 0x1f; + if (isDefaultFeature) { + mFontEntry->mDefaultSubSpaceFeatures[index] |= (1 << bit); + } else { + mFontEntry->mNonDefaultSubSpaceFeatures[index] |= (1 << bit); + } + } + offset += len; + } while (len == ArrayLength(scriptTags)); + } + + // spaces in default features of default script? + // ==> can't use word cache, skip GPOS analysis + bool canUseWordCache = true; + if (HasSubstitution(mFontEntry->mDefaultSubSpaceFeatures, Script::COMMON)) { + canUseWordCache = false; + } + + // GPOS lookups - distinguish kerning from non-kerning features + if (canUseWordCache && hb_ot_layout_has_positioning(face)) { + bool hasKerning = false, hasNonKerning = false; + HasLookupRuleWithGlyph(face, HB_OT_TAG_GPOS, hasNonKerning, + HB_TAG('k', 'e', 'r', 'n'), hasKerning, spaceGlyph); + if (hasKerning) { + flags |= gfxFontEntry::SpaceFeatures::HasFeatures | + gfxFontEntry::SpaceFeatures::Kerning; + } + if (hasNonKerning) { + flags |= gfxFontEntry::SpaceFeatures::HasFeatures | + gfxFontEntry::SpaceFeatures::NonKerning; + } + } + + if (MOZ_UNLIKELY(log)) { + TimeDuration elapsed = TimeStamp::Now() - start; + LOG_FONTINIT(( + "(fontinit-spacelookups) font: %s - " + "subst default: %8.8x %8.8x %8.8x %8.8x " + "subst non-default: %8.8x %8.8x %8.8x %8.8x " + "kerning: %s non-kerning: %s time: %6.3f\n", + mFontEntry->Name().get(), mFontEntry->mDefaultSubSpaceFeatures[3], + mFontEntry->mDefaultSubSpaceFeatures[2], + mFontEntry->mDefaultSubSpaceFeatures[1], + mFontEntry->mDefaultSubSpaceFeatures[0], + mFontEntry->mNonDefaultSubSpaceFeatures[3], + mFontEntry->mNonDefaultSubSpaceFeatures[2], + mFontEntry->mNonDefaultSubSpaceFeatures[1], + mFontEntry->mNonDefaultSubSpaceFeatures[0], + (mFontEntry->mHasSpaceFeatures & gfxFontEntry::SpaceFeatures::Kerning + ? "true" + : "false"), + (mFontEntry->mHasSpaceFeatures & gfxFontEntry::SpaceFeatures::NonKerning + ? "true" + : "false"), + elapsed.ToMilliseconds())); + } +} + +bool gfxFont::HasSubstitutionRulesWithSpaceLookups(Script aRunScript) const { + NS_ASSERTION(GetFontEntry()->mHasSpaceFeatures != + gfxFontEntry::SpaceFeatures::Uninitialized, + "need to initialize space lookup flags"); + NS_ASSERTION(aRunScript < Script::NUM_SCRIPT_CODES, "weird script code"); + if (aRunScript == Script::INVALID || aRunScript >= Script::NUM_SCRIPT_CODES) { + return false; + } + + // default features have space lookups ==> true + if (HasSubstitution(mFontEntry->mDefaultSubSpaceFeatures, Script::COMMON) || + HasSubstitution(mFontEntry->mDefaultSubSpaceFeatures, aRunScript)) { + return true; + } + + // non-default features have space lookups and some type of + // font feature, in font or style is specified ==> true + if ((HasSubstitution(mFontEntry->mNonDefaultSubSpaceFeatures, + Script::COMMON) || + HasSubstitution(mFontEntry->mNonDefaultSubSpaceFeatures, aRunScript)) && + (!mStyle.featureSettings.IsEmpty() || + !mFontEntry->mFeatureSettings.IsEmpty())) { + return true; + } + + return false; +} + +tainted_boolean_hint gfxFont::SpaceMayParticipateInShaping( + Script aRunScript) const { + // avoid checking fonts known not to include default space-dependent features + if (MOZ_UNLIKELY(mFontEntry->mSkipDefaultFeatureSpaceCheck)) { + if (!mKerningSet && mStyle.featureSettings.IsEmpty() && + mFontEntry->mFeatureSettings.IsEmpty()) { + return false; + } + } + + if (FontCanSupportGraphite()) { + if (gfxPlatform::GetPlatform()->UseGraphiteShaping()) { + return mFontEntry->HasGraphiteSpaceContextuals(); + } + } + + // We record the presence of space-dependent features in the font entry + // so that subsequent instantiations for the same font face won't + // require us to re-check the tables; however, the actual check is done + // by gfxFont because not all font entry subclasses know how to create + // a harfbuzz face for introspection. + gfxFontEntry::SpaceFeatures flags = mFontEntry->mHasSpaceFeatures; + if (flags == gfxFontEntry::SpaceFeatures::Uninitialized) { + CheckForFeaturesInvolvingSpace(); + flags = mFontEntry->mHasSpaceFeatures; + } + + if (!(flags & gfxFontEntry::SpaceFeatures::HasFeatures)) { + return false; + } + + // if font has substitution rules or non-kerning positioning rules + // that involve spaces, bypass + if (HasSubstitutionRulesWithSpaceLookups(aRunScript) || + (flags & gfxFontEntry::SpaceFeatures::NonKerning)) { + return true; + } + + // if kerning explicitly enabled/disabled via font-feature-settings or + // font-kerning and kerning rules use spaces, only bypass when enabled + if (mKerningSet && (flags & gfxFontEntry::SpaceFeatures::Kerning)) { + return mKerningEnabled; + } + + return false; +} + +bool gfxFont::SupportsFeature(Script aScript, uint32_t aFeatureTag) { + if (mGraphiteShaper && gfxPlatform::GetPlatform()->UseGraphiteShaping()) { + return GetFontEntry()->SupportsGraphiteFeature(aFeatureTag); + } + return GetFontEntry()->SupportsOpenTypeFeature(aScript, aFeatureTag); +} + +bool gfxFont::SupportsVariantCaps(Script aScript, uint32_t aVariantCaps, + bool& aFallbackToSmallCaps, + bool& aSyntheticLowerToSmallCaps, + bool& aSyntheticUpperToSmallCaps) { + bool ok = true; // cases without fallback are fine + aFallbackToSmallCaps = false; + aSyntheticLowerToSmallCaps = false; + aSyntheticUpperToSmallCaps = false; + switch (aVariantCaps) { + case NS_FONT_VARIANT_CAPS_SMALLCAPS: + ok = SupportsFeature(aScript, HB_TAG('s', 'm', 'c', 'p')); + if (!ok) { + aSyntheticLowerToSmallCaps = true; + } + break; + case NS_FONT_VARIANT_CAPS_ALLSMALL: + ok = SupportsFeature(aScript, HB_TAG('s', 'm', 'c', 'p')) && + SupportsFeature(aScript, HB_TAG('c', '2', 's', 'c')); + if (!ok) { + aSyntheticLowerToSmallCaps = true; + aSyntheticUpperToSmallCaps = true; + } + break; + case NS_FONT_VARIANT_CAPS_PETITECAPS: + ok = SupportsFeature(aScript, HB_TAG('p', 'c', 'a', 'p')); + if (!ok) { + ok = SupportsFeature(aScript, HB_TAG('s', 'm', 'c', 'p')); + aFallbackToSmallCaps = ok; + } + if (!ok) { + aSyntheticLowerToSmallCaps = true; + } + break; + case NS_FONT_VARIANT_CAPS_ALLPETITE: + ok = SupportsFeature(aScript, HB_TAG('p', 'c', 'a', 'p')) && + SupportsFeature(aScript, HB_TAG('c', '2', 'p', 'c')); + if (!ok) { + ok = SupportsFeature(aScript, HB_TAG('s', 'm', 'c', 'p')) && + SupportsFeature(aScript, HB_TAG('c', '2', 's', 'c')); + aFallbackToSmallCaps = ok; + } + if (!ok) { + aSyntheticLowerToSmallCaps = true; + aSyntheticUpperToSmallCaps = true; + } + break; + default: + break; + } + + NS_ASSERTION( + !(ok && (aSyntheticLowerToSmallCaps || aSyntheticUpperToSmallCaps)), + "shouldn't use synthetic features if we found real ones"); + + NS_ASSERTION(!(!ok && aFallbackToSmallCaps), + "if we found a usable fallback, that counts as ok"); + + return ok; +} + +bool gfxFont::SupportsSubSuperscript(uint32_t aSubSuperscript, + const uint8_t* aString, uint32_t aLength, + Script aRunScript) { + NS_ConvertASCIItoUTF16 unicodeString(reinterpret_cast<const char*>(aString), + aLength); + return SupportsSubSuperscript(aSubSuperscript, unicodeString.get(), aLength, + aRunScript); +} + +bool gfxFont::SupportsSubSuperscript(uint32_t aSubSuperscript, + const char16_t* aString, uint32_t aLength, + Script aRunScript) { + NS_ASSERTION(aSubSuperscript == NS_FONT_VARIANT_POSITION_SUPER || + aSubSuperscript == NS_FONT_VARIANT_POSITION_SUB, + "unknown value of font-variant-position"); + + uint32_t feature = aSubSuperscript == NS_FONT_VARIANT_POSITION_SUPER + ? HB_TAG('s', 'u', 'p', 's') + : HB_TAG('s', 'u', 'b', 's'); + + if (!SupportsFeature(aRunScript, feature)) { + return false; + } + + // xxx - for graphite, don't really know how to sniff lookups so bail + if (mGraphiteShaper && gfxPlatform::GetPlatform()->UseGraphiteShaping()) { + return true; + } + + gfxHarfBuzzShaper* shaper = GetHarfBuzzShaper(); + if (!shaper) { + return false; + } + + // get the hbset containing input glyphs for the feature + const hb_set_t* inputGlyphs = + mFontEntry->InputsForOpenTypeFeature(aRunScript, feature); + + // create an hbset containing default glyphs for the script run + hb_set_t* defaultGlyphsInRun = hb_set_create(); + + // for each character, get the glyph id + for (uint32_t i = 0; i < aLength; i++) { + uint32_t ch = aString[i]; + + if (i + 1 < aLength && NS_IS_SURROGATE_PAIR(ch, aString[i + 1])) { + i++; + ch = SURROGATE_TO_UCS4(ch, aString[i]); + } + + hb_codepoint_t gid = shaper->GetNominalGlyph(ch); + hb_set_add(defaultGlyphsInRun, gid); + } + + // intersect with input glyphs, if size is not the same ==> fallback + uint32_t origSize = hb_set_get_population(defaultGlyphsInRun); + hb_set_intersect(defaultGlyphsInRun, inputGlyphs); + uint32_t intersectionSize = hb_set_get_population(defaultGlyphsInRun); + hb_set_destroy(defaultGlyphsInRun); + + return origSize == intersectionSize; +} + +bool gfxFont::FeatureWillHandleChar(Script aRunScript, uint32_t aFeature, + uint32_t aUnicode) { + if (!SupportsFeature(aRunScript, aFeature)) { + return false; + } + + // xxx - for graphite, don't really know how to sniff lookups so bail + if (mGraphiteShaper && gfxPlatform::GetPlatform()->UseGraphiteShaping()) { + return true; + } + + if (gfxHarfBuzzShaper* shaper = GetHarfBuzzShaper()) { + // get the hbset containing input glyphs for the feature + const hb_set_t* inputGlyphs = + mFontEntry->InputsForOpenTypeFeature(aRunScript, aFeature); + + hb_codepoint_t gid = shaper->GetNominalGlyph(aUnicode); + return hb_set_has(inputGlyphs, gid); + } + + return false; +} + +bool gfxFont::HasFeatureSet(uint32_t aFeature, bool& aFeatureOn) { + aFeatureOn = false; + + if (mStyle.featureSettings.IsEmpty() && + GetFontEntry()->mFeatureSettings.IsEmpty()) { + return false; + } + + // add feature values from font + bool featureSet = false; + uint32_t i, count; + + nsTArray<gfxFontFeature>& fontFeatures = GetFontEntry()->mFeatureSettings; + count = fontFeatures.Length(); + for (i = 0; i < count; i++) { + const gfxFontFeature& feature = fontFeatures.ElementAt(i); + if (feature.mTag == aFeature) { + featureSet = true; + aFeatureOn = (feature.mValue != 0); + } + } + + // add feature values from style rules + nsTArray<gfxFontFeature>& styleFeatures = mStyle.featureSettings; + count = styleFeatures.Length(); + for (i = 0; i < count; i++) { + const gfxFontFeature& feature = styleFeatures.ElementAt(i); + if (feature.mTag == aFeature) { + featureSet = true; + aFeatureOn = (feature.mValue != 0); + } + } + + return featureSet; +} + +already_AddRefed<mozilla::gfx::ScaledFont> gfxFont::GetScaledFont( + mozilla::gfx::DrawTarget* aDrawTarget) { + TextRunDrawParams params; + params.dt = aDrawTarget; + return GetScaledFont(params); +} + +void gfxFont::InitializeScaledFont( + const RefPtr<mozilla::gfx::ScaledFont>& aScaledFont) { + if (!aScaledFont) { + return; + } + + float angle = AngleForSyntheticOblique(); + if (angle != 0.0f) { + aScaledFont->SetSyntheticObliqueAngle(angle); + } +} + +/** + * A helper function in case we need to do any rounding or other + * processing here. + */ +#define ToDeviceUnits(aAppUnits, aDevUnitsPerAppUnit) \ + (double(aAppUnits) * double(aDevUnitsPerAppUnit)) + +static AntialiasMode Get2DAAMode(gfxFont::AntialiasOption aAAOption) { + switch (aAAOption) { + case gfxFont::kAntialiasSubpixel: + return AntialiasMode::SUBPIXEL; + case gfxFont::kAntialiasGrayscale: + return AntialiasMode::GRAY; + case gfxFont::kAntialiasNone: + return AntialiasMode::NONE; + default: + return AntialiasMode::DEFAULT; + } +} + +class GlyphBufferAzure { +#define AUTO_BUFFER_SIZE (2048 / sizeof(Glyph)) + + typedef mozilla::image::imgDrawingParams imgDrawingParams; + + public: + GlyphBufferAzure(const TextRunDrawParams& aRunParams, + const FontDrawParams& aFontParams) + : mRunParams(aRunParams), + mFontParams(aFontParams), + mBuffer(*mAutoBuffer.addr()), + mBufSize(AUTO_BUFFER_SIZE), + mCapacity(0), + mNumGlyphs(0) {} + + ~GlyphBufferAzure() { + if (mNumGlyphs > 0) { + FlushGlyphs(); + } + + if (mBuffer != *mAutoBuffer.addr()) { + free(mBuffer); + } + } + + // Ensure the buffer has enough space for aGlyphCount glyphs to be added, + // considering the supplied strike multipler aStrikeCount. + // This MUST be called before OutputGlyph is used to actually store glyph + // records in the buffer. It may be called repeated to add further capacity + // in case we don't know up-front exactly what will be needed. + void AddCapacity(uint32_t aGlyphCount, uint32_t aStrikeCount) { + // Calculate the new capacity and ensure it will fit within the maximum + // allowed capacity. + static const uint64_t kMaxCapacity = 64 * 1024; + mCapacity = uint32_t(std::min( + kMaxCapacity, + uint64_t(mCapacity) + uint64_t(aGlyphCount) * uint64_t(aStrikeCount))); + // See if the required capacity fits within the already-allocated space + if (mCapacity <= mBufSize) { + return; + } + // We need to grow the buffer: determine a new size, allocate, and + // copy the existing data over if we didn't use realloc (which would + // do it automatically). + mBufSize = std::max(mCapacity, mBufSize * 2); + if (mBuffer == *mAutoBuffer.addr()) { + // switching from autobuffer to malloc, so we need to copy + mBuffer = reinterpret_cast<Glyph*>(moz_xmalloc(mBufSize * sizeof(Glyph))); + std::memcpy(mBuffer, *mAutoBuffer.addr(), mNumGlyphs * sizeof(Glyph)); + } else { + mBuffer = reinterpret_cast<Glyph*>( + moz_xrealloc(mBuffer, mBufSize * sizeof(Glyph))); + } + } + + void OutputGlyph(uint32_t aGlyphID, const gfx::Point& aPt) { + // If the buffer is full, flush to make room for the new glyph. + if (mNumGlyphs >= mCapacity) { + // Check that AddCapacity has been used appropriately! + MOZ_ASSERT(mCapacity > 0 && mNumGlyphs == mCapacity); + Flush(); + } + Glyph* glyph = mBuffer + mNumGlyphs++; + glyph->mIndex = aGlyphID; + glyph->mPosition = aPt; + } + + void Flush() { + if (mNumGlyphs > 0) { + FlushGlyphs(); + mNumGlyphs = 0; + } + } + + const TextRunDrawParams& mRunParams; + const FontDrawParams& mFontParams; + + private: + static DrawMode GetStrokeMode(DrawMode aMode) { + return aMode & (DrawMode::GLYPH_STROKE | DrawMode::GLYPH_STROKE_UNDERNEATH); + } + + // Render the buffered glyphs to the draw target. + void FlushGlyphs() { + gfx::GlyphBuffer buf; + buf.mGlyphs = mBuffer; + buf.mNumGlyphs = mNumGlyphs; + + const gfxContext::AzureState& state = mRunParams.context->CurrentState(); + + // Draw stroke first if the UNDERNEATH flag is set in drawMode. + if (mRunParams.strokeOpts && + GetStrokeMode(mRunParams.drawMode) == + (DrawMode::GLYPH_STROKE | DrawMode::GLYPH_STROKE_UNDERNEATH)) { + DrawStroke(state, buf); + } + + if (mRunParams.drawMode & DrawMode::GLYPH_FILL) { + if (state.pattern || mFontParams.contextPaint) { + Pattern* pat; + + RefPtr<gfxPattern> fillPattern; + if (mFontParams.contextPaint) { + imgDrawingParams imgParams; + fillPattern = mFontParams.contextPaint->GetFillPattern( + mRunParams.context->GetDrawTarget(), + mRunParams.context->CurrentMatrixDouble(), imgParams); + } + if (!fillPattern) { + if (state.pattern) { + RefPtr<gfxPattern> statePattern = + mRunParams.context->CurrentState().pattern; + pat = statePattern->GetPattern(mRunParams.dt, + state.patternTransformChanged + ? &state.patternTransform + : nullptr); + } else { + pat = nullptr; + } + } else { + pat = fillPattern->GetPattern(mRunParams.dt); + } + + if (pat) { + mRunParams.dt->FillGlyphs(mFontParams.scaledFont, buf, *pat, + mFontParams.drawOptions); + } + } else { + mRunParams.dt->FillGlyphs(mFontParams.scaledFont, buf, + ColorPattern(state.color), + mFontParams.drawOptions); + } + } + + // Draw stroke if the UNDERNEATH flag is not set. + if (mRunParams.strokeOpts && + GetStrokeMode(mRunParams.drawMode) == DrawMode::GLYPH_STROKE) { + DrawStroke(state, buf); + } + + if (mRunParams.drawMode & DrawMode::GLYPH_PATH) { + mRunParams.context->EnsurePathBuilder(); + Matrix mat = mRunParams.dt->GetTransform(); + mFontParams.scaledFont->CopyGlyphsToBuilder( + buf, mRunParams.context->mPathBuilder, &mat); + } + } + + void DrawStroke(const gfxContext::AzureState& aState, + gfx::GlyphBuffer& aBuffer) { + if (mRunParams.textStrokePattern) { + Pattern* pat = mRunParams.textStrokePattern->GetPattern( + mRunParams.dt, + aState.patternTransformChanged ? &aState.patternTransform : nullptr); + + if (pat) { + FlushStroke(aBuffer, *pat); + } + } else { + FlushStroke(aBuffer, + ColorPattern(ToDeviceColor(mRunParams.textStrokeColor))); + } + } + + void FlushStroke(gfx::GlyphBuffer& aBuf, const Pattern& aPattern) { + mRunParams.dt->StrokeGlyphs(mFontParams.scaledFont, aBuf, aPattern, + *mRunParams.strokeOpts, + mFontParams.drawOptions); + } + + // We use an "inline" buffer automatically allocated (on the stack) as part + // of the GlyphBufferAzure object to hold the glyphs in most cases, falling + // back to a separately-allocated heap buffer if the count of buffered + // glyphs gets too big. + // + // This is basically a rudimentary AutoTArray; so why not use AutoTArray + // itself? + // + // If we used an AutoTArray, we'd want to avoid using SetLength or + // AppendElements to allocate the space we actually need, because those + // methods would default-construct the new elements. + // + // Could we use SetCapacity to reserve the necessary buffer space without + // default-constructing all the Glyph records? No, because of a failure + // that could occur when we need to grow the buffer, which happens when we + // encounter a DetailedGlyph in the textrun that refers to a sequence of + // several real glyphs. At that point, we need to add some extra capacity + // to the buffer we initially allocated based on the length of the textrun + // range we're rendering. + // + // This buffer growth would work fine as long as it still fits within the + // array's inline buffer (we just use a bit more of it), or if the buffer + // was already heap-allocated (in which case AutoTArray will use realloc(), + // preserving its contents). But a problem will arise when the initial + // capacity we allocated (based on the length of the run) fits within the + // array's inline buffer, but subsequently we need to extend the buffer + // beyond the inline buffer size, so we reallocate to the heap. Because we + // haven't "officially" filled the array with SetLength or AppendElements, + // its mLength is still zero; as far as it's concerned the buffer is just + // uninitialized space, and when it switches to use a malloc'd buffer it + // won't copy the existing contents. + + // Allocate space for a buffer of Glyph records, without initializing them. + AlignedStorage2<Glyph[AUTO_BUFFER_SIZE]> mAutoBuffer; + + // Pointer to the buffer we're currently using -- initially mAutoBuffer, + // but may be changed to a malloc'd buffer, in which case that buffer must + // be free'd on destruction. + Glyph* mBuffer; + + uint32_t mBufSize; // size of allocated buffer; capacity can grow to + // this before reallocation is needed + uint32_t mCapacity; // amount of buffer size reserved + uint32_t mNumGlyphs; // number of glyphs actually present in the buffer + +#undef AUTO_BUFFER_SIZE +}; + +// Bug 674909. When synthetic bolding text by drawing twice, need to +// render using a pixel offset in device pixels, otherwise text +// doesn't appear bolded, it appears as if a bad text shadow exists +// when a non-identity transform exists. Use an offset factor so that +// the second draw occurs at a constant offset in device pixels. + +gfx::Float gfxFont::CalcXScale(DrawTarget* aDrawTarget) { + // determine magnitude of a 1px x offset in device space + Size t = aDrawTarget->GetTransform().TransformSize(Size(1.0, 0.0)); + if (t.width == 1.0 && t.height == 0.0) { + // short-circuit the most common case to avoid sqrt() and division + return 1.0; + } + + gfx::Float m = sqrtf(t.width * t.width + t.height * t.height); + + NS_ASSERTION(m != 0.0, "degenerate transform while synthetic bolding"); + if (m == 0.0) { + return 0.0; // effectively disables offset + } + + // scale factor so that offsets are 1px in device pixels + return 1.0 / m; +} + +// Draw a run of CharacterGlyph records from the given offset in aShapedText. +// Returns true if glyph paths were actually emitted. +template <gfxFont::FontComplexityT FC, gfxFont::SpacingT S> +bool gfxFont::DrawGlyphs(const gfxShapedText* aShapedText, + uint32_t aOffset, // offset in the textrun + uint32_t aCount, // length of run to draw + gfx::Point* aPt, + const gfx::Matrix* aOffsetMatrix, // may be null + GlyphBufferAzure& aBuffer) { + float& inlineCoord = + aBuffer.mFontParams.isVerticalFont ? aPt->y.value : aPt->x.value; + + const gfxShapedText::CompressedGlyph* glyphData = + &aShapedText->GetCharacterGlyphs()[aOffset]; + + if (S == SpacingT::HasSpacing) { + float space = aBuffer.mRunParams.spacing[0].mBefore * + aBuffer.mFontParams.advanceDirection; + inlineCoord += space; + } + + // Allocate buffer space for the run, assuming all simple glyphs. + uint32_t capacityMult = 1 + aBuffer.mFontParams.extraStrikes; + aBuffer.AddCapacity(aCount, capacityMult); + + bool emittedGlyphs = false; + + for (uint32_t i = 0; i < aCount; ++i, ++glyphData) { + if (glyphData->IsSimpleGlyph()) { + float advance = + glyphData->GetSimpleAdvance() * aBuffer.mFontParams.advanceDirection; + if (aBuffer.mRunParams.isRTL) { + inlineCoord += advance; + } + DrawOneGlyph<FC>(glyphData->GetSimpleGlyph(), *aPt, aBuffer, + &emittedGlyphs); + if (!aBuffer.mRunParams.isRTL) { + inlineCoord += advance; + } + } else { + uint32_t glyphCount = glyphData->GetGlyphCount(); + if (glyphCount > 0) { + // Add extra buffer capacity to allow for multiple-glyph entry. + aBuffer.AddCapacity(glyphCount - 1, capacityMult); + const gfxShapedText::DetailedGlyph* details = + aShapedText->GetDetailedGlyphs(aOffset + i); + MOZ_ASSERT(details, "missing DetailedGlyph!"); + for (uint32_t j = 0; j < glyphCount; ++j, ++details) { + float advance = + details->mAdvance * aBuffer.mFontParams.advanceDirection; + if (aBuffer.mRunParams.isRTL) { + inlineCoord += advance; + } + if (glyphData->IsMissing()) { + if (!DrawMissingGlyph(aBuffer.mRunParams, aBuffer.mFontParams, + details, *aPt)) { + return false; + } + } else { + gfx::Point glyphPt( + *aPt + (aOffsetMatrix + ? aOffsetMatrix->TransformPoint(details->mOffset) + : details->mOffset)); + DrawOneGlyph<FC>(details->mGlyphID, glyphPt, aBuffer, + &emittedGlyphs); + } + if (!aBuffer.mRunParams.isRTL) { + inlineCoord += advance; + } + } + } + } + + if (S == SpacingT::HasSpacing) { + float space = aBuffer.mRunParams.spacing[i].mAfter; + if (i + 1 < aCount) { + space += aBuffer.mRunParams.spacing[i + 1].mBefore; + } + space *= aBuffer.mFontParams.advanceDirection; + inlineCoord += space; + } + } + + return emittedGlyphs; +} + +// Draw an individual glyph at a specific location. +// *aPt is the glyph position in appUnits; it is converted to device +// coordinates (devPt) here. +template <gfxFont::FontComplexityT FC> +void gfxFont::DrawOneGlyph(uint32_t aGlyphID, const gfx::Point& aPt, + GlyphBufferAzure& aBuffer, bool* aEmittedGlyphs) { + const TextRunDrawParams& runParams(aBuffer.mRunParams); + + gfx::Point devPt(ToDeviceUnits(aPt.x, runParams.devPerApp), + ToDeviceUnits(aPt.y, runParams.devPerApp)); + + auto* textDrawer = runParams.textDrawer; + if (textDrawer) { + // If the glyph is entirely outside the clip rect, we don't need to draw it + // at all. (We check the font extents here rather than the individual glyph + // bounds because that's cheaper to look up, and provides a conservative + // "worst case" for where this glyph might want to draw.) + LayoutDeviceRect extents = + LayoutDeviceRect::FromUnknownRect(aBuffer.mFontParams.fontExtents); + extents.MoveBy(LayoutDevicePoint::FromUnknownPoint(devPt)); + if (!extents.Intersects(runParams.clipRect)) { + return; + } + } + + if (FC == FontComplexityT::ComplexFont) { + const FontDrawParams& fontParams(aBuffer.mFontParams); + + gfxContextMatrixAutoSaveRestore matrixRestore; + + if (fontParams.obliqueSkew != 0.0f && fontParams.isVerticalFont && + !textDrawer) { + // We have to flush each glyph individually when doing + // synthetic-oblique for vertical-upright text, because + // the skew transform needs to be applied to a separate + // origin for each glyph, not once for the whole run. + aBuffer.Flush(); + matrixRestore.SetContext(runParams.context); + gfx::Point skewPt( + devPt.x + GetMetrics(nsFontMetrics::eVertical).emHeight / 2, devPt.y); + gfx::Matrix mat = + runParams.context->CurrentMatrix() + .PreTranslate(skewPt) + .PreMultiply(gfx::Matrix(1, fontParams.obliqueSkew, 0, 1, 0, 0)) + .PreTranslate(-skewPt); + runParams.context->SetMatrix(mat); + } + + if (fontParams.haveSVGGlyphs) { + if (!runParams.paintSVGGlyphs) { + return; + } + NS_WARNING_ASSERTION( + runParams.drawMode != DrawMode::GLYPH_PATH, + "Rendering SVG glyph despite request for glyph path"); + if (RenderSVGGlyph(runParams.context, textDrawer, devPt, aGlyphID, + fontParams.contextPaint, runParams.callbacks, + *aEmittedGlyphs)) { + return; + } + } + + if (fontParams.haveColorGlyphs && !UseNativeColrFontSupport() && + RenderColorGlyph(runParams.dt, runParams.context, textDrawer, + fontParams, devPt, aGlyphID)) { + return; + } + + aBuffer.OutputGlyph(aGlyphID, devPt); + + // Synthetic bolding (if required) by multi-striking. + for (int32_t i = 0; i < fontParams.extraStrikes; ++i) { + if (fontParams.isVerticalFont) { + devPt.y += fontParams.synBoldOnePixelOffset; + } else { + devPt.x += fontParams.synBoldOnePixelOffset; + } + aBuffer.OutputGlyph(aGlyphID, devPt); + } + + if (fontParams.obliqueSkew != 0.0f && fontParams.isVerticalFont && + !textDrawer) { + aBuffer.Flush(); + } + } else { + aBuffer.OutputGlyph(aGlyphID, devPt); + } + + *aEmittedGlyphs = true; +} + +bool gfxFont::DrawMissingGlyph(const TextRunDrawParams& aRunParams, + const FontDrawParams& aFontParams, + const gfxShapedText::DetailedGlyph* aDetails, + const gfx::Point& aPt) { + // Default-ignorable chars will have zero advance width; + // we don't have to draw the hexbox for them. + float advance = aDetails->mAdvance; + if (aRunParams.drawMode != DrawMode::GLYPH_PATH && advance > 0) { + auto* textDrawer = aRunParams.textDrawer; + const Matrix* matPtr = nullptr; + Matrix mat; + if (textDrawer) { + // Generate an orientation matrix for the current writing mode + wr::FontInstanceFlags flags = textDrawer->GetWRGlyphFlags(); + if (flags & wr::FontInstanceFlags::TRANSPOSE) { + std::swap(mat._11, mat._12); + std::swap(mat._21, mat._22); + } + mat.PostScale(flags & wr::FontInstanceFlags::FLIP_X ? -1.0f : 1.0f, + flags & wr::FontInstanceFlags::FLIP_Y ? -1.0f : 1.0f); + matPtr = &mat; + } + + Point pt(Float(ToDeviceUnits(aPt.x, aRunParams.devPerApp)), + Float(ToDeviceUnits(aPt.y, aRunParams.devPerApp))); + Float advanceDevUnits = Float(ToDeviceUnits(advance, aRunParams.devPerApp)); + Float height = GetMetrics(nsFontMetrics::eHorizontal).maxAscent; + // Horizontally center if drawing vertically upright with no sideways + // transform. + Rect glyphRect = + aFontParams.isVerticalFont && !mat.HasNonAxisAlignedTransform() + ? Rect(pt.x - height / 2, pt.y, height, advanceDevUnits) + : Rect(pt.x, pt.y - height, advanceDevUnits, height); + + // If there's a fake-italic skew in effect as part + // of the drawTarget's transform, we need to undo + // this before drawing the hexbox. (Bug 983985) + gfxContextMatrixAutoSaveRestore matrixRestore; + if (aFontParams.obliqueSkew != 0.0f && !aFontParams.isVerticalFont && + !textDrawer) { + matrixRestore.SetContext(aRunParams.context); + gfx::Matrix mat = + aRunParams.context->CurrentMatrix() + .PreTranslate(pt) + .PreMultiply(gfx::Matrix(1, 0, aFontParams.obliqueSkew, 1, 0, 0)) + .PreTranslate(-pt); + aRunParams.context->SetMatrix(mat); + } + + gfxFontMissingGlyphs::DrawMissingGlyph( + aDetails->mGlyphID, glyphRect, *aRunParams.dt, + PatternFromState(aRunParams.context), matPtr); + } + return true; +} + +// This method is mostly parallel to DrawGlyphs. +void gfxFont::DrawEmphasisMarks(const gfxTextRun* aShapedText, gfx::Point* aPt, + uint32_t aOffset, uint32_t aCount, + const EmphasisMarkDrawParams& aParams) { + float& inlineCoord = aParams.isVertical ? aPt->y.value : aPt->x.value; + gfxTextRun::Range markRange(aParams.mark); + gfxTextRun::DrawParams params(aParams.context); + + float clusterStart = -std::numeric_limits<float>::infinity(); + bool shouldDrawEmphasisMark = false; + for (uint32_t i = 0, idx = aOffset; i < aCount; ++i, ++idx) { + if (aParams.spacing) { + inlineCoord += aParams.direction * aParams.spacing[i].mBefore; + } + if (aShapedText->IsClusterStart(idx) || + clusterStart == -std::numeric_limits<float>::infinity()) { + clusterStart = inlineCoord; + } + if (aShapedText->CharMayHaveEmphasisMark(idx)) { + shouldDrawEmphasisMark = true; + } + inlineCoord += aParams.direction * aShapedText->GetAdvanceForGlyph(idx); + if (shouldDrawEmphasisMark && + (i + 1 == aCount || aShapedText->IsClusterStart(idx + 1))) { + float clusterAdvance = inlineCoord - clusterStart; + // Move the coord backward to get the needed start point. + float delta = (clusterAdvance + aParams.advance) / 2; + inlineCoord -= delta; + aParams.mark->Draw(markRange, *aPt, params); + inlineCoord += delta; + shouldDrawEmphasisMark = false; + } + if (aParams.spacing) { + inlineCoord += aParams.direction * aParams.spacing[i].mAfter; + } + } +} + +nsTArray<mozilla::gfx::sRGBColor>* TextRunDrawParams::GetPaletteFor( + const gfxFont* aFont) { + auto entry = mPaletteCache.Lookup(aFont); + if (!entry) { + CacheData newData; + newData.mKey = aFont; + + gfxFontEntry* fe = aFont->GetFontEntry(); + gfxFontEntry::AutoHBFace face = fe->GetHBFace(); + newData.mPalette = COLRFonts::SetupColorPalette( + face, paletteValueSet, fontPalette, fe->FamilyName()); + + entry.Set(std::move(newData)); + } + return entry.Data().mPalette.get(); +} + +void gfxFont::Draw(const gfxTextRun* aTextRun, uint32_t aStart, uint32_t aEnd, + gfx::Point* aPt, TextRunDrawParams& aRunParams, + gfx::ShapedTextFlags aOrientation) { + NS_ASSERTION(aRunParams.drawMode == DrawMode::GLYPH_PATH || + !(int(aRunParams.drawMode) & int(DrawMode::GLYPH_PATH)), + "GLYPH_PATH cannot be used with GLYPH_FILL, GLYPH_STROKE or " + "GLYPH_STROKE_UNDERNEATH"); + + if (aStart >= aEnd) { + return; + } + + FontDrawParams fontParams; + + if (aRunParams.drawOpts) { + fontParams.drawOptions = *aRunParams.drawOpts; + } + + fontParams.scaledFont = GetScaledFont(aRunParams); + if (!fontParams.scaledFont) { + return; + } + auto* textDrawer = aRunParams.textDrawer; + + fontParams.obliqueSkew = SkewForSyntheticOblique(); + fontParams.haveSVGGlyphs = GetFontEntry()->TryGetSVGData(this); + fontParams.haveColorGlyphs = GetFontEntry()->TryGetColorGlyphs(); + fontParams.contextPaint = aRunParams.runContextPaint; + + if (fontParams.haveColorGlyphs && !UseNativeColrFontSupport()) { + DeviceColor ctxColor; + fontParams.currentColor = aRunParams.context->GetDeviceColor(ctxColor) + ? sRGBColor::FromABGR(ctxColor.ToABGR()) + : sRGBColor::OpaqueBlack(); + fontParams.palette = aRunParams.GetPaletteFor(this); + } + + if (textDrawer) { + fontParams.isVerticalFont = aRunParams.isVerticalRun; + } else { + fontParams.isVerticalFont = + aOrientation == gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_UPRIGHT; + } + + gfxContextMatrixAutoSaveRestore matrixRestore; + layout::TextDrawTarget::AutoRestoreWRGlyphFlags glyphFlagsRestore; + + // Save the current baseline offset for restoring later, in case it is + // modified. + float& baseline = fontParams.isVerticalFont ? aPt->x.value : aPt->y.value; + float origBaseline = baseline; + + // The point may be advanced in local-space, while the resulting point on + // return must be advanced in transformed space. So save the original point so + // we can properly transform the advance later. + gfx::Point origPt = *aPt; + const gfx::Matrix* offsetMatrix = nullptr; + + // Default to advancing along the +X direction (-X if RTL). + fontParams.advanceDirection = aRunParams.isRTL ? -1.0f : 1.0f; + // Default to offsetting baseline downward along the +Y direction. + float baselineDir = 1.0f; + // The direction of sideways rotation, if applicable. + // -1 for rotating left/counter-clockwise + // 1 for rotating right/clockwise + // 0 for no rotation + float sidewaysDir = + (aOrientation == gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_SIDEWAYS_LEFT + ? -1.0f + : (aOrientation == + gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_SIDEWAYS_RIGHT + ? 1.0f + : 0.0f)); + // If we're rendering a sideways run, we need to push a rotation transform to + // the context. + if (sidewaysDir != 0.0f) { + if (textDrawer) { + // For WebRender, we can't use a DrawTarget transform and must instead use + // flags that locally transform the glyph, without affecting the glyph + // origin. The glyph origins must thus be offset in the transformed + // directions (instead of local-space directions). Modify the advance and + // baseline directions to account for the indicated transform. + + // The default text orientation is down being +Y and right being +X. + // Rotating 90 degrees left/CCW makes down be +X and right be -Y. + // Rotating 90 degrees right/CW makes down be -X and right be +Y. + // Thus the advance direction (moving right) is just sidewaysDir, + // i.e. negative along Y axis if rotated left and positive if + // rotated right. + fontParams.advanceDirection *= sidewaysDir; + // The baseline direction (moving down) is negated relative to the + // advance direction for sideways transforms. + baselineDir *= -sidewaysDir; + + glyphFlagsRestore.Save(textDrawer); + // Set the transform flags accordingly. Both sideways rotations transpose + // X and Y, while left rotation flips the resulting Y axis, and right + // rotation flips the resulting X axis. + textDrawer->SetWRGlyphFlags( + textDrawer->GetWRGlyphFlags() | wr::FontInstanceFlags::TRANSPOSE | + (aOrientation == + gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_SIDEWAYS_LEFT + ? wr::FontInstanceFlags::FLIP_Y + : wr::FontInstanceFlags::FLIP_X)); + // We also need to set up a transform for the glyph offset vector that + // may be present in DetailedGlyph records. + static const gfx::Matrix kSidewaysLeft = {0, -1, 1, 0, 0, 0}; + static const gfx::Matrix kSidewaysRight = {0, 1, -1, 0, 0, 0}; + offsetMatrix = + (aOrientation == ShapedTextFlags::TEXT_ORIENT_VERTICAL_SIDEWAYS_LEFT) + ? &kSidewaysLeft + : &kSidewaysRight; + } else { + // For non-WebRender targets, just push a rotation transform. + matrixRestore.SetContext(aRunParams.context); + gfxPoint p(aPt->x * aRunParams.devPerApp, aPt->y * aRunParams.devPerApp); + // Get a matrix we can use to draw the (horizontally-shaped) textrun + // with 90-degree CW rotation. + const gfxFloat rotation = sidewaysDir * M_PI / 2.0f; + gfxMatrix mat = aRunParams.context->CurrentMatrixDouble() + .PreTranslate(p) + . // translate origin for rotation + PreRotate(rotation) + . // turn 90deg CCW (sideways-left) or CW (*-right) + PreTranslate(-p); // undo the translation + + aRunParams.context->SetMatrixDouble(mat); + } + + // If we're drawing rotated horizontal text for an element styled + // text-orientation:mixed, the dominant baseline will be vertical- + // centered. So in this case, we need to adjust the position so that + // the rotated horizontal text (which uses an alphabetic baseline) will + // look OK when juxtaposed with upright glyphs (rendered on a centered + // vertical baseline). The adjustment here is somewhat ad hoc; we + // should eventually look for baseline tables[1] in the fonts and use + // those if available. + // [1] See http://www.microsoft.com/typography/otspec/base.htm + if (aTextRun->UseCenterBaseline()) { + const Metrics& metrics = GetMetrics(nsFontMetrics::eHorizontal); + float baseAdj = (metrics.emAscent - metrics.emDescent) / 2; + baseline += baseAdj * aTextRun->GetAppUnitsPerDevUnit() * baselineDir; + } + } else if (textDrawer && + aOrientation == ShapedTextFlags::TEXT_ORIENT_VERTICAL_UPRIGHT) { + glyphFlagsRestore.Save(textDrawer); + textDrawer->SetWRGlyphFlags(textDrawer->GetWRGlyphFlags() | + wr::FontInstanceFlags::VERTICAL); + } + + if (fontParams.obliqueSkew != 0.0f && !fontParams.isVerticalFont && + !textDrawer) { + // Adjust matrix for synthetic-oblique, except if we're doing vertical- + // upright text, in which case this will be handled for each glyph + // individually in DrawOneGlyph. + if (!matrixRestore.HasMatrix()) { + matrixRestore.SetContext(aRunParams.context); + } + gfx::Point p(aPt->x * aRunParams.devPerApp, aPt->y * aRunParams.devPerApp); + gfx::Matrix mat = + aRunParams.context->CurrentMatrix() + .PreTranslate(p) + .PreMultiply(gfx::Matrix(1, 0, -fontParams.obliqueSkew, 1, 0, 0)) + .PreTranslate(-p); + aRunParams.context->SetMatrix(mat); + } + + RefPtr<SVGContextPaint> contextPaint; + if (fontParams.haveSVGGlyphs && !fontParams.contextPaint) { + // If no pattern is specified for fill, use the current pattern + NS_ASSERTION((int(aRunParams.drawMode) & int(DrawMode::GLYPH_STROKE)) == 0, + "no pattern supplied for stroking text"); + RefPtr<gfxPattern> fillPattern = aRunParams.context->GetPattern(); + contextPaint = new SimpleTextContextPaint( + fillPattern, nullptr, aRunParams.context->CurrentMatrixDouble()); + fontParams.contextPaint = contextPaint.get(); + } + + // Synthetic-bold strikes are each offset one device pixel in run direction + // (these values are only needed if ApplySyntheticBold() is true). + // If drawing via webrender, it will do multistrike internally so we don't + // need to handle it here. + bool doMultistrikeBold = ApplySyntheticBold() && !textDrawer; + if (doMultistrikeBold) { + // For screen display, we want to try and repeat strikes with an offset of + // one device pixel, accounting for zoom or other transforms that may be + // in effect, so compute x-axis scale factor from the drawtarget. + // However, when generating PDF output the drawtarget's transform does not + // really bear any relation to "device pixels", and may result in an + // excessively large offset relative to the font size (bug 1823888), so + // we limit it based on the used font size to avoid this. + // The constant 48.0 reflects the threshold where the calculation in + // gfxFont::GetSyntheticBoldOffset() switches to a simple origin-based + // slope, though the exact value is somewhat arbitrary; it's selected to + // allow a visible amount of boldness while preventing the offset from + // becoming "large" in relation to the glyphs. + Float xscale = + std::min<Float>(GetAdjustedSize() / 48.0, + CalcXScale(aRunParams.context->GetDrawTarget())); + fontParams.synBoldOnePixelOffset = aRunParams.direction * xscale; + if (xscale != 0.0) { + static const int32_t kMaxExtraStrikes = 128; + gfxFloat extraStrikes = GetSyntheticBoldOffset() / xscale; + if (extraStrikes > kMaxExtraStrikes) { + // if too many strikes are required, limit them and increase the step + // size to compensate + fontParams.extraStrikes = kMaxExtraStrikes; + fontParams.synBoldOnePixelOffset = aRunParams.direction * + GetSyntheticBoldOffset() / + fontParams.extraStrikes; + } else { + // use as many strikes as needed for the increased advance + fontParams.extraStrikes = NS_lroundf(std::max(1.0, extraStrikes)); + } + } else { + // Degenerate transform?! + fontParams.extraStrikes = 0; + } + } else { + fontParams.synBoldOnePixelOffset = 0; + fontParams.extraStrikes = 0; + } + + // Figure out the maximum extents for the font, accounting for synthetic + // oblique and bold. + if (mFUnitsConvFactor > 0.0) { + fontParams.fontExtents = GetFontEntry()->GetFontExtents(mFUnitsConvFactor); + } else { + // Was it not an sfnt? Maybe on Linux... use arbitrary huge extents, so we + // don't inadvertently clip stuff. A bit less efficient than true extents, + // but this should be extremely rare. + auto size = GetAdjustedSize(); + fontParams.fontExtents = Rect(-2 * size, -2 * size, 5 * size, 5 * size); + } + if (fontParams.obliqueSkew != 0.0f) { + gfx::Point p(fontParams.fontExtents.x, fontParams.fontExtents.y); + gfx::Matrix skew(1, 0, fontParams.obliqueSkew, 1, 0, 0); + fontParams.fontExtents = skew.TransformBounds(fontParams.fontExtents); + } + if (fontParams.extraStrikes) { + if (fontParams.isVerticalFont) { + fontParams.fontExtents.height += + float(fontParams.extraStrikes) * fontParams.synBoldOnePixelOffset; + } else { + fontParams.fontExtents.width += + float(fontParams.extraStrikes) * fontParams.synBoldOnePixelOffset; + } + } + + bool oldSubpixelAA = aRunParams.dt->GetPermitSubpixelAA(); + if (!AllowSubpixelAA()) { + aRunParams.dt->SetPermitSubpixelAA(false); + } + + Matrix mat; + Matrix oldMat = aRunParams.dt->GetTransform(); + + fontParams.drawOptions.mAntialiasMode = Get2DAAMode(mAntialiasOption); + + if (mStyle.baselineOffset != 0.0) { + baseline += + mStyle.baselineOffset * aTextRun->GetAppUnitsPerDevUnit() * baselineDir; + } + + bool emittedGlyphs; + { + // Select appropriate version of the templated DrawGlyphs method + // to output glyphs to the buffer, depending on complexity needed + // for the type of font, and whether added inter-glyph spacing + // is specified. + GlyphBufferAzure buffer(aRunParams, fontParams); + if (fontParams.haveSVGGlyphs || fontParams.haveColorGlyphs || + fontParams.extraStrikes || + (fontParams.obliqueSkew != 0.0f && fontParams.isVerticalFont && + !textDrawer)) { + if (aRunParams.spacing) { + emittedGlyphs = + DrawGlyphs<FontComplexityT::ComplexFont, SpacingT::HasSpacing>( + aTextRun, aStart, aEnd - aStart, aPt, offsetMatrix, buffer); + } else { + emittedGlyphs = + DrawGlyphs<FontComplexityT::ComplexFont, SpacingT::NoSpacing>( + aTextRun, aStart, aEnd - aStart, aPt, offsetMatrix, buffer); + } + } else { + if (aRunParams.spacing) { + emittedGlyphs = + DrawGlyphs<FontComplexityT::SimpleFont, SpacingT::HasSpacing>( + aTextRun, aStart, aEnd - aStart, aPt, offsetMatrix, buffer); + } else { + emittedGlyphs = + DrawGlyphs<FontComplexityT::SimpleFont, SpacingT::NoSpacing>( + aTextRun, aStart, aEnd - aStart, aPt, offsetMatrix, buffer); + } + } + } + + baseline = origBaseline; + + if (aRunParams.callbacks && emittedGlyphs) { + aRunParams.callbacks->NotifyGlyphPathEmitted(); + } + + aRunParams.dt->SetTransform(oldMat); + aRunParams.dt->SetPermitSubpixelAA(oldSubpixelAA); + + if (sidewaysDir != 0.0f && !textDrawer) { + // Adjust updated aPt to account for the transform we were using. + // The advance happened horizontally in local-space, but the transformed + // sideways advance is actually vertical, with sign depending on the + // direction of rotation. + float advance = aPt->x - origPt.x; + *aPt = gfx::Point(origPt.x, origPt.y + advance * sidewaysDir); + } +} + +bool gfxFont::RenderSVGGlyph(gfxContext* aContext, + layout::TextDrawTarget* aTextDrawer, + gfx::Point aPoint, uint32_t aGlyphId, + SVGContextPaint* aContextPaint) const { + if (!GetFontEntry()->HasSVGGlyph(aGlyphId)) { + return false; + } + + if (aTextDrawer) { + // WebRender doesn't support SVG Glyphs. + // (pretend to succeed, output doesn't matter, we will emit a blob) + aTextDrawer->FoundUnsupportedFeature(); + return true; + } + + const gfxFloat devUnitsPerSVGUnit = + GetAdjustedSize() / GetFontEntry()->UnitsPerEm(); + gfxContextMatrixAutoSaveRestore matrixRestore(aContext); + + aContext->SetMatrix(aContext->CurrentMatrix() + .PreTranslate(aPoint.x, aPoint.y) + .PreScale(devUnitsPerSVGUnit, devUnitsPerSVGUnit)); + + aContextPaint->InitStrokeGeometry(aContext, devUnitsPerSVGUnit); + + GetFontEntry()->RenderSVGGlyph(aContext, aGlyphId, aContextPaint); + aContext->NewPath(); + return true; +} + +bool gfxFont::RenderSVGGlyph(gfxContext* aContext, + layout::TextDrawTarget* aTextDrawer, + gfx::Point aPoint, uint32_t aGlyphId, + SVGContextPaint* aContextPaint, + gfxTextRunDrawCallbacks* aCallbacks, + bool& aEmittedGlyphs) const { + if (aCallbacks && aEmittedGlyphs) { + aCallbacks->NotifyGlyphPathEmitted(); + aEmittedGlyphs = false; + } + return RenderSVGGlyph(aContext, aTextDrawer, aPoint, aGlyphId, aContextPaint); +} + +bool gfxFont::RenderColorGlyph(DrawTarget* aDrawTarget, gfxContext* aContext, + layout::TextDrawTarget* aTextDrawer, + const FontDrawParams& aFontParams, + const Point& aPoint, uint32_t aGlyphId) { + if (const auto* paintGraph = + COLRFonts::GetGlyphPaintGraph(GetFontEntry()->GetCOLR(), aGlyphId)) { + const auto* hbShaper = GetHarfBuzzShaper(); + if (hbShaper && hbShaper->IsInitialized()) { + return COLRFonts::PaintGlyphGraph( + GetFontEntry()->GetCOLR(), hbShaper->GetHBFont(), paintGraph, + aDrawTarget, aTextDrawer, aFontParams.scaledFont, + aFontParams.drawOptions, aPoint, aFontParams.currentColor, + aFontParams.palette, aGlyphId, mFUnitsConvFactor); + } + } + + if (const auto* layers = + COLRFonts::GetGlyphLayers(GetFontEntry()->GetCOLR(), aGlyphId)) { + auto face(GetFontEntry()->GetHBFace()); + bool ok = COLRFonts::PaintGlyphLayers( + GetFontEntry()->GetCOLR(), face, layers, aDrawTarget, aTextDrawer, + aFontParams.scaledFont, aFontParams.drawOptions, aPoint, + aFontParams.currentColor, aFontParams.palette); + return ok; + } + + return false; +} + +bool gfxFont::HasColorGlyphFor(uint32_t aCh, uint32_t aNextCh) { + // Bitmap fonts are assumed to provide "color" glyphs for all supported chars. + gfxFontEntry* fe = GetFontEntry(); + if (fe->HasColorBitmapTable()) { + return true; + } + // Use harfbuzz shaper to look up the default glyph ID for the character. + auto* shaper = GetHarfBuzzShaper(); + if (!shaper) { + return false; + } + uint32_t gid = 0; + if (gfxFontUtils::IsVarSelector(aNextCh)) { + gid = shaper->GetVariationGlyph(aCh, aNextCh); + } + if (!gid) { + gid = shaper->GetNominalGlyph(aCh); + } + if (!gid) { + return false; + } + + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1801521: + // Emoji special-case: flag sequences NOT based on Regional Indicator pairs + // use the BLACK FLAG character plus a series of plane-14 TAG LETTERs, e.g. + // England = <black-flag, tag-G, tag-B, tag-E, tag-N, tag-G, tag-cancel> + // Here, we don't check for support of the entire sequence (too much + // expensive lookahead), but we check that the font at least supports the + // first of the tag letter codes, because if it doesn't, we're at risk of + // just getting an undifferentiated black flag glyph. + if (gfxFontUtils::IsEmojiFlagAndTag(aCh, aNextCh)) { + if (!shaper->GetNominalGlyph(aNextCh)) { + return false; + } + } + + // Check if there is a COLR/CPAL or SVG glyph for this ID. + if (fe->TryGetColorGlyphs() && + (COLRFonts::GetGlyphPaintGraph(fe->GetCOLR(), gid) || + COLRFonts::GetGlyphLayers(fe->GetCOLR(), gid))) { + return true; + } + if (fe->TryGetSVGData(this) && fe->HasSVGGlyph(gid)) { + return true; + } + return false; +} + +static void UnionRange(gfxFloat aX, gfxFloat* aDestMin, gfxFloat* aDestMax) { + *aDestMin = std::min(*aDestMin, aX); + *aDestMax = std::max(*aDestMax, aX); +} + +// We get precise glyph extents if the textrun creator requested them, or +// if the font is a user font --- in which case the author may be relying +// on overflowing glyphs. +static bool NeedsGlyphExtents(gfxFont* aFont, const gfxTextRun* aTextRun) { + return (aTextRun->GetFlags() & + gfx::ShapedTextFlags::TEXT_NEED_BOUNDING_BOX) || + aFont->GetFontEntry()->IsUserFont(); +} + +bool gfxFont::IsSpaceGlyphInvisible(DrawTarget* aRefDrawTarget, + const gfxTextRun* aTextRun) { + gfxFontEntry::LazyFlag flag = mFontEntry->mSpaceGlyphIsInvisible; + if (flag == gfxFontEntry::LazyFlag::Uninitialized && + GetAdjustedSize() >= 1.0) { + gfxGlyphExtents* extents = + GetOrCreateGlyphExtents(aTextRun->GetAppUnitsPerDevUnit()); + gfxRect glyphExtents; + flag = extents->GetTightGlyphExtentsAppUnits( + this, aRefDrawTarget, GetSpaceGlyph(), &glyphExtents) && + glyphExtents.IsEmpty() + ? gfxFontEntry::LazyFlag::Yes + : gfxFontEntry::LazyFlag::No; + mFontEntry->mSpaceGlyphIsInvisible = flag; + } + return flag == gfxFontEntry::LazyFlag::Yes; +} + +bool gfxFont::MeasureGlyphs(const gfxTextRun* aTextRun, uint32_t aStart, + uint32_t aEnd, BoundingBoxType aBoundingBoxType, + DrawTarget* aRefDrawTarget, Spacing* aSpacing, + gfxGlyphExtents* aExtents, bool aIsRTL, + bool aNeedsGlyphExtents, RunMetrics& aMetrics, + gfxFloat* aAdvanceMin, gfxFloat* aAdvanceMax) { + const gfxTextRun::CompressedGlyph* charGlyphs = + aTextRun->GetCharacterGlyphs(); + double x = 0; + if (aSpacing) { + x += aSpacing[0].mBefore; + } + uint32_t spaceGlyph = GetSpaceGlyph(); + bool allGlyphsInvisible = true; + + AutoReadLock lock(aExtents->mLock); + + for (uint32_t i = aStart; i < aEnd; ++i) { + const gfxTextRun::CompressedGlyph* glyphData = &charGlyphs[i]; + if (glyphData->IsSimpleGlyph()) { + double advance = glyphData->GetSimpleAdvance(); + uint32_t glyphIndex = glyphData->GetSimpleGlyph(); + if (allGlyphsInvisible) { + if (glyphIndex != spaceGlyph) { + allGlyphsInvisible = false; + } else { + gfxFontEntry::LazyFlag flag = mFontEntry->mSpaceGlyphIsInvisible; + if (flag == gfxFontEntry::LazyFlag::Uninitialized && + GetAdjustedSize() >= 1.0) { + gfxRect glyphExtents; + flag = aExtents->GetTightGlyphExtentsAppUnitsLocked( + this, aRefDrawTarget, spaceGlyph, &glyphExtents) && + glyphExtents.IsEmpty() + ? gfxFontEntry::LazyFlag::Yes + : gfxFontEntry::LazyFlag::No; + mFontEntry->mSpaceGlyphIsInvisible = flag; + } + if (flag == gfxFontEntry::LazyFlag::No) { + allGlyphsInvisible = false; + } + } + } + // Only get the real glyph horizontal extent if we were asked + // for the tight bounding box or we're in quality mode + if (aBoundingBoxType != LOOSE_INK_EXTENTS || aNeedsGlyphExtents) { + uint16_t extentsWidth = + aExtents->GetContainedGlyphWidthAppUnitsLocked(glyphIndex); + if (extentsWidth != gfxGlyphExtents::INVALID_WIDTH && + aBoundingBoxType == LOOSE_INK_EXTENTS) { + UnionRange(x, aAdvanceMin, aAdvanceMax); + UnionRange(x + extentsWidth, aAdvanceMin, aAdvanceMax); + } else { + gfxRect glyphRect; + if (!aExtents->GetTightGlyphExtentsAppUnitsLocked( + this, aRefDrawTarget, glyphIndex, &glyphRect)) { + glyphRect = gfxRect(0, aMetrics.mBoundingBox.Y(), advance, + aMetrics.mBoundingBox.Height()); + } + if (aIsRTL) { + // In effect, swap left and right sidebearings of the glyph, for + // proper accumulation of potentially-overlapping glyph rects. + glyphRect.MoveToX(advance - glyphRect.XMost()); + } + glyphRect.MoveByX(x); + aMetrics.mBoundingBox = aMetrics.mBoundingBox.Union(glyphRect); + } + } + x += advance; + } else { + allGlyphsInvisible = false; + uint32_t glyphCount = glyphData->GetGlyphCount(); + if (glyphCount > 0) { + const gfxTextRun::DetailedGlyph* details = + aTextRun->GetDetailedGlyphs(i); + NS_ASSERTION(details != nullptr, + "detailedGlyph record should not be missing!"); + uint32_t j; + for (j = 0; j < glyphCount; ++j, ++details) { + uint32_t glyphIndex = details->mGlyphID; + double advance = details->mAdvance; + gfxRect glyphRect; + if (glyphData->IsMissing() || + !aExtents->GetTightGlyphExtentsAppUnitsLocked( + this, aRefDrawTarget, glyphIndex, &glyphRect)) { + // We might have failed to get glyph extents due to + // OOM or something + glyphRect = gfxRect(0, -aMetrics.mAscent, advance, + aMetrics.mAscent + aMetrics.mDescent); + } + if (aIsRTL) { + // Swap left/right sidebearings of the glyph, because we're doing + // mirrored measurement. + glyphRect.MoveToX(advance - glyphRect.XMost()); + // Move to current x position, mirroring any x-offset amount. + glyphRect.MoveByX(x - details->mOffset.x); + } else { + glyphRect.MoveByX(x + details->mOffset.x); + } + glyphRect.MoveByY(details->mOffset.y); + aMetrics.mBoundingBox = aMetrics.mBoundingBox.Union(glyphRect); + x += advance; + } + } + } + if (aSpacing) { + double space = aSpacing[i - aStart].mAfter; + if (i + 1 < aEnd) { + space += aSpacing[i + 1 - aStart].mBefore; + } + x += space; + } + } + + aMetrics.mAdvanceWidth = x; + return allGlyphsInvisible; +} + +bool gfxFont::MeasureGlyphs(const gfxTextRun* aTextRun, uint32_t aStart, + uint32_t aEnd, BoundingBoxType aBoundingBoxType, + DrawTarget* aRefDrawTarget, Spacing* aSpacing, + bool aIsRTL, RunMetrics& aMetrics) { + const gfxTextRun::CompressedGlyph* charGlyphs = + aTextRun->GetCharacterGlyphs(); + double x = 0; + if (aSpacing) { + x += aSpacing[0].mBefore; + } + uint32_t spaceGlyph = GetSpaceGlyph(); + bool allGlyphsInvisible = true; + + for (uint32_t i = aStart; i < aEnd; ++i) { + const gfxTextRun::CompressedGlyph* glyphData = &charGlyphs[i]; + if (glyphData->IsSimpleGlyph()) { + double advance = glyphData->GetSimpleAdvance(); + uint32_t glyphIndex = glyphData->GetSimpleGlyph(); + if (allGlyphsInvisible && + (glyphIndex != spaceGlyph || + !IsSpaceGlyphInvisible(aRefDrawTarget, aTextRun))) { + allGlyphsInvisible = false; + } + x += advance; + } else { + allGlyphsInvisible = false; + uint32_t glyphCount = glyphData->GetGlyphCount(); + if (glyphCount > 0) { + const gfxTextRun::DetailedGlyph* details = + aTextRun->GetDetailedGlyphs(i); + NS_ASSERTION(details != nullptr, + "detailedGlyph record should not be missing!"); + uint32_t j; + for (j = 0; j < glyphCount; ++j, ++details) { + double advance = details->mAdvance; + gfxRect glyphRect(0, -aMetrics.mAscent, advance, + aMetrics.mAscent + aMetrics.mDescent); + if (aIsRTL) { + // Swap left/right sidebearings of the glyph, because we're doing + // mirrored measurement. + glyphRect.MoveToX(advance - glyphRect.XMost()); + // Move to current x position, mirroring any x-offset amount. + glyphRect.MoveByX(x - details->mOffset.x); + } else { + glyphRect.MoveByX(x + details->mOffset.x); + } + glyphRect.MoveByY(details->mOffset.y); + aMetrics.mBoundingBox = aMetrics.mBoundingBox.Union(glyphRect); + x += advance; + } + } + } + if (aSpacing) { + double space = aSpacing[i - aStart].mAfter; + if (i + 1 < aEnd) { + space += aSpacing[i + 1 - aStart].mBefore; + } + x += space; + } + } + + aMetrics.mAdvanceWidth = x; + return allGlyphsInvisible; +} + +gfxFont::RunMetrics gfxFont::Measure(const gfxTextRun* aTextRun, + uint32_t aStart, uint32_t aEnd, + BoundingBoxType aBoundingBoxType, + DrawTarget* aRefDrawTarget, + Spacing* aSpacing, + gfx::ShapedTextFlags aOrientation) { + // If aBoundingBoxType is TIGHT_HINTED_OUTLINE_EXTENTS + // and the underlying cairo font may be antialiased, + // we need to create a copy in order to avoid getting cached extents. + // This is only used by MathML layout at present. + if (aBoundingBoxType == TIGHT_HINTED_OUTLINE_EXTENTS && + mAntialiasOption != kAntialiasNone) { + gfxFont* nonAA = mNonAAFont; + if (!nonAA) { + nonAA = CopyWithAntialiasOption(kAntialiasNone); + if (nonAA) { + if (!mNonAAFont.compareExchange(nullptr, nonAA)) { + delete nonAA; + nonAA = mNonAAFont; + } + } + } + // if font subclass doesn't implement CopyWithAntialiasOption(), + // it will return null and we'll proceed to use the existing font + if (nonAA) { + return nonAA->Measure(aTextRun, aStart, aEnd, + TIGHT_HINTED_OUTLINE_EXTENTS, aRefDrawTarget, + aSpacing, aOrientation); + } + } + + const int32_t appUnitsPerDevUnit = aTextRun->GetAppUnitsPerDevUnit(); + // Current position in appunits + Orientation orientation = + aOrientation == gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_UPRIGHT + ? nsFontMetrics::eVertical + : nsFontMetrics::eHorizontal; + const gfxFont::Metrics& fontMetrics = GetMetrics(orientation); + + gfxFloat baselineOffset = 0; + if (aTextRun->UseCenterBaseline() && + orientation == nsFontMetrics::eHorizontal) { + // For a horizontal font being used in vertical writing mode with + // text-orientation:mixed, the overall metrics we're accumulating + // will be aimed at a center baseline. But this font's metrics were + // based on the alphabetic baseline. So we compute a baseline offset + // that will be applied to ascent/descent values and glyph rects + // to effectively shift them relative to the baseline. + // XXX Eventually we should probably use the BASE table, if present. + // But it usually isn't, so we need an ad hoc adjustment for now. + baselineOffset = + appUnitsPerDevUnit * (fontMetrics.emAscent - fontMetrics.emDescent) / 2; + } + + RunMetrics metrics; + metrics.mAscent = fontMetrics.maxAscent * appUnitsPerDevUnit; + metrics.mDescent = fontMetrics.maxDescent * appUnitsPerDevUnit; + + if (aStart == aEnd) { + // exit now before we look at aSpacing[0], which is undefined + metrics.mAscent -= baselineOffset; + metrics.mDescent += baselineOffset; + metrics.mBoundingBox = + gfxRect(0, -metrics.mAscent, 0, metrics.mAscent + metrics.mDescent); + return metrics; + } + + gfxFloat advanceMin = 0, advanceMax = 0; + bool isRTL = aTextRun->IsRightToLeft(); + bool needsGlyphExtents = NeedsGlyphExtents(this, aTextRun); + gfxGlyphExtents* extents = + ((aBoundingBoxType == LOOSE_INK_EXTENTS && !needsGlyphExtents && + !aTextRun->HasDetailedGlyphs()) || + MOZ_UNLIKELY(GetStyle()->AdjustedSizeMustBeZero())) + ? nullptr + : GetOrCreateGlyphExtents(aTextRun->GetAppUnitsPerDevUnit()); + + bool allGlyphsInvisible; + if (extents) { + allGlyphsInvisible = MeasureGlyphs( + aTextRun, aStart, aEnd, aBoundingBoxType, aRefDrawTarget, aSpacing, + extents, isRTL, needsGlyphExtents, metrics, &advanceMin, &advanceMax); + } else { + allGlyphsInvisible = + MeasureGlyphs(aTextRun, aStart, aEnd, aBoundingBoxType, aRefDrawTarget, + aSpacing, isRTL, metrics); + } + + if (allGlyphsInvisible) { + metrics.mBoundingBox.SetEmpty(); + } else if (aBoundingBoxType == LOOSE_INK_EXTENTS) { + UnionRange(metrics.mAdvanceWidth, &advanceMin, &advanceMax); + gfxRect fontBox(advanceMin, -metrics.mAscent, advanceMax - advanceMin, + metrics.mAscent + metrics.mDescent); + metrics.mBoundingBox = metrics.mBoundingBox.Union(fontBox); + } + + if (isRTL) { + // Reverse the effect of having swapped each glyph's sidebearings, to get + // the correct sidebearings of the merged bounding box. + metrics.mBoundingBox.MoveToX(metrics.mAdvanceWidth - + metrics.mBoundingBox.XMost()); + } + + // If the font may be rendered with a fake-italic effect, we need to allow + // for the top-right of the glyphs being skewed to the right, and the + // bottom-left being skewed further left. + gfxFloat skew = SkewForSyntheticOblique(); + if (skew != 0.0) { + gfxFloat extendLeftEdge, extendRightEdge; + if (orientation == nsFontMetrics::eVertical) { + // The glyph will actually be skewed vertically, but "left" and "right" + // here refer to line-left (physical top) and -right (bottom), so these + // are still the directions in which we need to extend the box. + extendLeftEdge = skew < 0.0 ? ceil(-skew * metrics.mBoundingBox.XMost()) + : ceil(skew * -metrics.mBoundingBox.X()); + extendRightEdge = skew < 0.0 ? ceil(-skew * -metrics.mBoundingBox.X()) + : ceil(skew * metrics.mBoundingBox.XMost()); + } else { + extendLeftEdge = skew < 0.0 ? ceil(-skew * -metrics.mBoundingBox.Y()) + : ceil(skew * metrics.mBoundingBox.YMost()); + extendRightEdge = skew < 0.0 ? ceil(-skew * metrics.mBoundingBox.YMost()) + : ceil(skew * -metrics.mBoundingBox.Y()); + } + metrics.mBoundingBox.SetWidth(metrics.mBoundingBox.Width() + + extendLeftEdge + extendRightEdge); + metrics.mBoundingBox.MoveByX(-extendLeftEdge); + } + + if (baselineOffset != 0) { + metrics.mAscent -= baselineOffset; + metrics.mDescent += baselineOffset; + metrics.mBoundingBox.MoveByY(baselineOffset); + } + + return metrics; +} + +bool gfxFont::AgeCachedWords() { + mozilla::AutoWriteLock lock(mLock); + if (mWordCache) { + for (auto it = mWordCache->Iter(); !it.Done(); it.Next()) { + CacheHashEntry* entry = it.Get(); + if (!entry->mShapedWord) { + NS_ASSERTION(entry->mShapedWord, "cache entry has no gfxShapedWord!"); + it.Remove(); + } else if (entry->mShapedWord->IncrementAge() == kShapedWordCacheMaxAge) { + it.Remove(); + } + } + return mWordCache->IsEmpty(); + } + return true; +} + +void gfxFont::NotifyGlyphsChanged() const { + AutoReadLock lock(mLock); + uint32_t i, count = mGlyphExtentsArray.Length(); + for (i = 0; i < count; ++i) { + // Flush cached extents array + mGlyphExtentsArray[i]->NotifyGlyphsChanged(); + } + + if (mGlyphChangeObservers) { + for (const auto& key : *mGlyphChangeObservers) { + key->NotifyGlyphsChanged(); + } + } +} + +// If aChar is a "word boundary" for shaped-word caching purposes, return it; +// else return 0. +static char16_t IsBoundarySpace(char16_t aChar, char16_t aNextChar) { + if ((aChar == ' ' || aChar == 0x00A0) && !IsClusterExtender(aNextChar)) { + return aChar; + } + return 0; +} + +#ifdef __GNUC__ +# define GFX_MAYBE_UNUSED __attribute__((unused)) +#else +# define GFX_MAYBE_UNUSED +#endif + +template <typename T, typename Func> +bool gfxFont::ProcessShapedWordInternal( + DrawTarget* aDrawTarget, const T* aText, uint32_t aLength, uint32_t aHash, + Script aRunScript, nsAtom* aLanguage, bool aVertical, + int32_t aAppUnitsPerDevUnit, gfx::ShapedTextFlags aFlags, + RoundingFlags aRounding, gfxTextPerfMetrics* aTextPerf GFX_MAYBE_UNUSED, + Func aCallback) { + CacheHashKey key(aText, aLength, aHash, aRunScript, aLanguage, + aAppUnitsPerDevUnit, aFlags, aRounding); + { + // If we have a word cache, attempt to look up the word in it. + AutoReadLock lock(mLock); + if (mWordCache) { + // if there's a cached entry for this word, just return it + if (CacheHashEntry* entry = mWordCache->GetEntry(key)) { + gfxShapedWord* sw = entry->mShapedWord.get(); + sw->ResetAge(); +#ifndef RELEASE_OR_BETA + if (aTextPerf) { + // XXX we should make sure this is atomic + aTextPerf->current.wordCacheHit++; + } +#endif + aCallback(sw); + return true; + } + } + } + + // We didn't find a cached word (or don't even have a cache yet), so create + // a new gfxShapedWord and cache it. We don't have to lock during shaping, + // only when it comes time to cache the new entry. + + gfxShapedWord* sw = + gfxShapedWord::Create(aText, aLength, aRunScript, aLanguage, + aAppUnitsPerDevUnit, aFlags, aRounding); + if (!sw) { + NS_WARNING("failed to create gfxShapedWord - expect missing text"); + return false; + } + DebugOnly<bool> ok = ShapeText(aDrawTarget, aText, 0, aLength, aRunScript, + aLanguage, aVertical, aRounding, sw); + NS_WARNING_ASSERTION(ok, "failed to shape word - expect garbled text"); + + { + // We're going to cache the new shaped word, so lock for writing now. + AutoWriteLock lock(mLock); + if (!mWordCache) { + mWordCache = MakeUnique<nsTHashtable<CacheHashEntry>>(); + } else { + // If the cache is getting too big, flush it and start over. + uint32_t wordCacheMaxEntries = + gfxPlatform::GetPlatform()->WordCacheMaxEntries(); + if (mWordCache->Count() > wordCacheMaxEntries) { + // Flush the cache if it is getting overly big. + NS_WARNING("flushing shaped-word cache"); + ClearCachedWordsLocked(); + } + } + CacheHashEntry* entry = mWordCache->PutEntry(key, fallible); + if (!entry) { + NS_WARNING("failed to create word cache entry - expect missing text"); + delete sw; + return false; + } + + // It's unlikely, but maybe another thread got there before us... + if (entry->mShapedWord) { + // Just discard the newly-created word, and use the existing one. + delete sw; + sw = entry->mShapedWord.get(); + sw->ResetAge(); +#ifndef RELEASE_OR_BETA + if (aTextPerf) { + aTextPerf->current.wordCacheHit++; + } +#endif + aCallback(sw); + return true; + } + + entry->mShapedWord.reset(sw); + +#ifndef RELEASE_OR_BETA + if (aTextPerf) { + aTextPerf->current.wordCacheMiss++; + } +#endif + aCallback(sw); + } + + gfxFontCache::GetCache()->RunWordCacheExpirationTimer(); + return true; +} + +bool gfxFont::CacheHashEntry::KeyEquals(const KeyTypePointer aKey) const { + const gfxShapedWord* sw = mShapedWord.get(); + if (!sw) { + return false; + } + if (sw->GetLength() != aKey->mLength || sw->GetFlags() != aKey->mFlags || + sw->GetRounding() != aKey->mRounding || + sw->GetAppUnitsPerDevUnit() != aKey->mAppUnitsPerDevUnit || + sw->GetScript() != aKey->mScript || + sw->GetLanguage() != aKey->mLanguage) { + return false; + } + if (sw->TextIs8Bit()) { + if (aKey->mTextIs8Bit) { + return (0 == memcmp(sw->Text8Bit(), aKey->mText.mSingle, + aKey->mLength * sizeof(uint8_t))); + } + // The key has 16-bit text, even though all the characters are < 256, + // so the TEXT_IS_8BIT flag was set and the cached ShapedWord we're + // comparing with will have 8-bit text. + const uint8_t* s1 = sw->Text8Bit(); + const char16_t* s2 = aKey->mText.mDouble; + const char16_t* s2end = s2 + aKey->mLength; + while (s2 < s2end) { + if (*s1++ != *s2++) { + return false; + } + } + return true; + } + NS_ASSERTION(!(aKey->mFlags & gfx::ShapedTextFlags::TEXT_IS_8BIT) && + !aKey->mTextIs8Bit, + "didn't expect 8-bit text here"); + return (0 == memcmp(sw->TextUnicode(), aKey->mText.mDouble, + aKey->mLength * sizeof(char16_t))); +} + +bool gfxFont::ProcessSingleSpaceShapedWord( + DrawTarget* aDrawTarget, bool aVertical, int32_t aAppUnitsPerDevUnit, + gfx::ShapedTextFlags aFlags, RoundingFlags aRounding, + const std::function<void(gfxShapedWord*)>& aCallback) { + static const uint8_t space = ' '; + return ProcessShapedWordInternal( + aDrawTarget, &space, 1, gfxShapedWord::HashMix(0, ' '), Script::LATIN, + /* aLanguage = */ nullptr, aVertical, aAppUnitsPerDevUnit, aFlags, + aRounding, nullptr, aCallback); +} + +bool gfxFont::ShapeText(DrawTarget* aDrawTarget, const uint8_t* aText, + uint32_t aOffset, uint32_t aLength, Script aScript, + nsAtom* aLanguage, bool aVertical, + RoundingFlags aRounding, gfxShapedText* aShapedText) { + nsDependentCSubstring ascii((const char*)aText, aLength); + nsAutoString utf16; + AppendASCIItoUTF16(ascii, utf16); + if (utf16.Length() != aLength) { + return false; + } + return ShapeText(aDrawTarget, utf16.BeginReading(), aOffset, aLength, aScript, + aLanguage, aVertical, aRounding, aShapedText); +} + +bool gfxFont::ShapeText(DrawTarget* aDrawTarget, const char16_t* aText, + uint32_t aOffset, uint32_t aLength, Script aScript, + nsAtom* aLanguage, bool aVertical, + RoundingFlags aRounding, gfxShapedText* aShapedText) { + // XXX Currently, we do all vertical shaping through harfbuzz. + // Vertical graphite support may be wanted as a future enhancement. + // XXX Graphite shaping currently only supported on the main thread! + // Worker-thread shaping (offscreen canvas) will always go via harfbuzz. + if (FontCanSupportGraphite() && !aVertical && NS_IsMainThread()) { + if (gfxPlatform::GetPlatform()->UseGraphiteShaping()) { + gfxGraphiteShaper* shaper = mGraphiteShaper; + if (!shaper) { + shaper = new gfxGraphiteShaper(this); + if (mGraphiteShaper.compareExchange(nullptr, shaper)) { + Telemetry::ScalarAdd(Telemetry::ScalarID::BROWSER_USAGE_GRAPHITE, 1); + } else { + delete shaper; + shaper = mGraphiteShaper; + } + } + if (shaper->ShapeText(aDrawTarget, aText, aOffset, aLength, aScript, + aLanguage, aVertical, aRounding, aShapedText)) { + PostShapingFixup(aDrawTarget, aText, aOffset, aLength, aVertical, + aShapedText); + return true; + } + } + } + + gfxHarfBuzzShaper* shaper = GetHarfBuzzShaper(); + if (shaper && + shaper->ShapeText(aDrawTarget, aText, aOffset, aLength, aScript, + aLanguage, aVertical, aRounding, aShapedText)) { + PostShapingFixup(aDrawTarget, aText, aOffset, aLength, aVertical, + aShapedText); + if (GetFontEntry()->HasTrackingTable()) { + // Convert font size from device pixels back to CSS px + // to use in selecting tracking value + float trackSize = GetAdjustedSize() * + aShapedText->GetAppUnitsPerDevUnit() / + AppUnitsPerCSSPixel(); + float tracking = + GetFontEntry()->TrackingForCSSPx(trackSize) * mFUnitsConvFactor; + // Applying tracking is a lot like the adjustment we do for + // synthetic bold: we want to apply between clusters, not to + // non-spacing glyphs within a cluster. So we can reuse that + // helper here. + aShapedText->AdjustAdvancesForSyntheticBold(tracking, aOffset, aLength); + } + return true; + } + + NS_WARNING_ASSERTION(false, "shaper failed, expect scrambled/missing text"); + return false; +} + +void gfxFont::PostShapingFixup(DrawTarget* aDrawTarget, const char16_t* aText, + uint32_t aOffset, uint32_t aLength, + bool aVertical, gfxShapedText* aShapedText) { + if (ApplySyntheticBold()) { + const Metrics& metrics = GetMetrics(aVertical ? nsFontMetrics::eVertical + : nsFontMetrics::eHorizontal); + if (metrics.maxAdvance > metrics.aveCharWidth) { + aShapedText->AdjustAdvancesForSyntheticBold(GetSyntheticBoldOffset(), + aOffset, aLength); + } + } +} + +#define MAX_SHAPING_LENGTH \ + 32760 // slightly less than 32K, trying to avoid + // over-stressing platform shapers +#define BACKTRACK_LIMIT \ + 16 // backtrack this far looking for a good place + // to split into fragments for separate shaping + +template <typename T> +bool gfxFont::ShapeFragmentWithoutWordCache(DrawTarget* aDrawTarget, + const T* aText, uint32_t aOffset, + uint32_t aLength, Script aScript, + nsAtom* aLanguage, bool aVertical, + RoundingFlags aRounding, + gfxTextRun* aTextRun) { + aTextRun->SetupClusterBoundaries(aOffset, aText, aLength); + + bool ok = true; + + while (ok && aLength > 0) { + uint32_t fragLen = aLength; + + // limit the length of text we pass to shapers in a single call + if (fragLen > MAX_SHAPING_LENGTH) { + fragLen = MAX_SHAPING_LENGTH; + + // in the 8-bit case, there are no multi-char clusters, + // so we don't need to do this check + if constexpr (sizeof(T) == sizeof(char16_t)) { + uint32_t i; + for (i = 0; i < BACKTRACK_LIMIT; ++i) { + if (aTextRun->IsClusterStart(aOffset + fragLen - i)) { + fragLen -= i; + break; + } + } + if (i == BACKTRACK_LIMIT) { + // if we didn't find any cluster start while backtracking, + // just check that we're not in the middle of a surrogate + // pair; back up by one code unit if we are. + if (NS_IS_SURROGATE_PAIR(aText[fragLen - 1], aText[fragLen])) { + --fragLen; + } + } + } + } + + ok = ShapeText(aDrawTarget, aText, aOffset, fragLen, aScript, aLanguage, + aVertical, aRounding, aTextRun); + + aText += fragLen; + aOffset += fragLen; + aLength -= fragLen; + } + + return ok; +} + +// Check if aCh is an unhandled control character that should be displayed +// as a hexbox rather than rendered by some random font on the system. +// We exclude \r as stray s are rather common (bug 941940). +// Note that \n and \t don't come through here, as they have specific +// meanings that have already been handled. +static bool IsInvalidControlChar(uint32_t aCh) { + return aCh != '\r' && ((aCh & 0x7f) < 0x20 || aCh == 0x7f); +} + +template <typename T> +bool gfxFont::ShapeTextWithoutWordCache(DrawTarget* aDrawTarget, const T* aText, + uint32_t aOffset, uint32_t aLength, + Script aScript, nsAtom* aLanguage, + bool aVertical, RoundingFlags aRounding, + gfxTextRun* aTextRun) { + uint32_t fragStart = 0; + bool ok = true; + + for (uint32_t i = 0; i <= aLength && ok; ++i) { + T ch = (i < aLength) ? aText[i] : '\n'; + bool invalid = gfxFontGroup::IsInvalidChar(ch); + uint32_t length = i - fragStart; + + // break into separate fragments when we hit an invalid char + if (!invalid) { + continue; + } + + if (length > 0) { + ok = ShapeFragmentWithoutWordCache( + aDrawTarget, aText + fragStart, aOffset + fragStart, length, aScript, + aLanguage, aVertical, aRounding, aTextRun); + } + + if (i == aLength) { + break; + } + + // fragment was terminated by an invalid char: skip it, + // unless it's a control char that we want to show as a hexbox, + // but record where TAB or NEWLINE occur + if (ch == '\t') { + aTextRun->SetIsTab(aOffset + i); + } else if (ch == '\n') { + aTextRun->SetIsNewline(aOffset + i); + } else if (GetGeneralCategory(ch) == HB_UNICODE_GENERAL_CATEGORY_FORMAT) { + aTextRun->SetIsFormattingControl(aOffset + i); + } else if (IsInvalidControlChar(ch) && + !(aTextRun->GetFlags() & + gfx::ShapedTextFlags::TEXT_HIDE_CONTROL_CHARACTERS)) { + if (GetFontEntry()->IsUserFont() && HasCharacter(ch)) { + ShapeFragmentWithoutWordCache(aDrawTarget, aText + i, aOffset + i, 1, + aScript, aLanguage, aVertical, aRounding, + aTextRun); + } else { + aTextRun->SetMissingGlyph(aOffset + i, ch, this); + } + } + fragStart = i + 1; + } + + NS_WARNING_ASSERTION(ok, "failed to shape text - expect garbled text"); + return ok; +} + +#ifndef RELEASE_OR_BETA +# define TEXT_PERF_INCR(tp, m) (tp ? (tp)->current.m++ : 0) +#else +# define TEXT_PERF_INCR(tp, m) +#endif + +inline static bool IsChar8Bit(uint8_t /*aCh*/) { return true; } +inline static bool IsChar8Bit(char16_t aCh) { return aCh < 0x100; } + +inline static bool HasSpaces(const uint8_t* aString, uint32_t aLen) { + return memchr(aString, 0x20, aLen) != nullptr; +} + +inline static bool HasSpaces(const char16_t* aString, uint32_t aLen) { + for (const char16_t* ch = aString; ch < aString + aLen; ch++) { + if (*ch == 0x20) { + return true; + } + } + return false; +} + +template <typename T> +bool gfxFont::SplitAndInitTextRun( + DrawTarget* aDrawTarget, gfxTextRun* aTextRun, + const T* aString, // text for this font run + uint32_t aRunStart, // position in the textrun + uint32_t aRunLength, Script aRunScript, nsAtom* aLanguage, + ShapedTextFlags aOrientation) { + if (aRunLength == 0) { + return true; + } + + gfxTextPerfMetrics* tp = nullptr; + RoundingFlags rounding = GetRoundOffsetsToPixels(aDrawTarget); + +#ifndef RELEASE_OR_BETA + tp = aTextRun->GetFontGroup()->GetTextPerfMetrics(); + if (tp) { + if (mStyle.systemFont) { + tp->current.numChromeTextRuns++; + } else { + tp->current.numContentTextRuns++; + } + tp->current.numChars += aRunLength; + if (aRunLength > tp->current.maxTextRunLen) { + tp->current.maxTextRunLen = aRunLength; + } + } +#endif + + uint32_t wordCacheCharLimit = + gfxPlatform::GetPlatform()->WordCacheCharLimit(); + + bool vertical = aOrientation == ShapedTextFlags::TEXT_ORIENT_VERTICAL_UPRIGHT; + + // If spaces can participate in shaping (e.g. within lookups for automatic + // fractions), need to shape without using the word cache which segments + // textruns on space boundaries. Word cache can be used if the textrun + // is short enough to fit in the word cache and it lacks spaces. + tainted_boolean_hint t_canParticipate = + SpaceMayParticipateInShaping(aRunScript); + bool canParticipate = t_canParticipate.unverified_safe_because( + "We need to ensure that this function operates safely independent of " + "t_canParticipate. The worst that can happen here is that the decision " + "to use the cache is incorrectly made, resulting in a bad " + "rendering/slowness. However, this would not compromise the memory " + "safety of Firefox in any way, and can thus be permitted"); + + if (canParticipate) { + if (aRunLength > wordCacheCharLimit || HasSpaces(aString, aRunLength)) { + TEXT_PERF_INCR(tp, wordCacheSpaceRules); + return ShapeTextWithoutWordCache(aDrawTarget, aString, aRunStart, + aRunLength, aRunScript, aLanguage, + vertical, rounding, aTextRun); + } + } + + // the only flags we care about for ShapedWord construction/caching + gfx::ShapedTextFlags flags = aTextRun->GetFlags(); + flags &= (gfx::ShapedTextFlags::TEXT_IS_RTL | + gfx::ShapedTextFlags::TEXT_DISABLE_OPTIONAL_LIGATURES | + gfx::ShapedTextFlags::TEXT_USE_MATH_SCRIPT | + gfx::ShapedTextFlags::TEXT_ORIENT_MASK); + if constexpr (sizeof(T) == sizeof(uint8_t)) { + flags |= gfx::ShapedTextFlags::TEXT_IS_8BIT; + } + + uint32_t wordStart = 0; + uint32_t hash = 0; + bool wordIs8Bit = true; + int32_t appUnitsPerDevUnit = aTextRun->GetAppUnitsPerDevUnit(); + + T nextCh = aString[0]; + for (uint32_t i = 0; i <= aRunLength; ++i) { + T ch = nextCh; + nextCh = (i < aRunLength - 1) ? aString[i + 1] : '\n'; + T boundary = IsBoundarySpace(ch, nextCh); + bool invalid = !boundary && gfxFontGroup::IsInvalidChar(ch); + uint32_t length = i - wordStart; + + // break into separate ShapedWords when we hit an invalid char, + // or a boundary space (always handled individually), + // or the first non-space after a space + if (!boundary && !invalid) { + if (!IsChar8Bit(ch)) { + wordIs8Bit = false; + } + // include this character in the hash, and move on to next + hash = gfxShapedWord::HashMix(hash, ch); + continue; + } + + // We've decided to break here (i.e. we're at the end of a "word"); + // shape the word and add it to the textrun. + // For words longer than the limit, we don't use the + // font's word cache but just shape directly into the textrun. + if (length > wordCacheCharLimit) { + TEXT_PERF_INCR(tp, wordCacheLong); + bool ok = ShapeFragmentWithoutWordCache( + aDrawTarget, aString + wordStart, aRunStart + wordStart, length, + aRunScript, aLanguage, vertical, rounding, aTextRun); + if (!ok) { + return false; + } + } else if (length > 0) { + gfx::ShapedTextFlags wordFlags = flags; + // in the 8-bit version of this method, TEXT_IS_8BIT was + // already set as part of |flags|, so no need for a per-word + // adjustment here + if (sizeof(T) == sizeof(char16_t)) { + if (wordIs8Bit) { + wordFlags |= gfx::ShapedTextFlags::TEXT_IS_8BIT; + } + } + bool processed = ProcessShapedWordInternal( + aDrawTarget, aString + wordStart, length, hash, aRunScript, aLanguage, + vertical, appUnitsPerDevUnit, wordFlags, rounding, tp, + [&](gfxShapedWord* aShapedWord) { + aTextRun->CopyGlyphDataFrom(aShapedWord, aRunStart + wordStart); + }); + if (!processed) { + return false; // failed, presumably out of memory? + } + } + + if (boundary) { + // word was terminated by a space: add that to the textrun + MOZ_ASSERT(aOrientation != ShapedTextFlags::TEXT_ORIENT_VERTICAL_MIXED, + "text-orientation:mixed should be resolved earlier"); + if (boundary != ' ' || !aTextRun->SetSpaceGlyphIfSimple( + this, aRunStart + i, ch, aOrientation)) { + // Currently, the only "boundary" characters we recognize are + // space and no-break space, which are both 8-bit, so we force + // that flag (below). If we ever change IsBoundarySpace, we + // may need to revise this. + // Avoid tautological-constant-out-of-range-compare in 8-bit: + DebugOnly<char16_t> boundary16 = boundary; + NS_ASSERTION(boundary16 < 256, "unexpected boundary!"); + bool processed = ProcessShapedWordInternal( + aDrawTarget, &boundary, 1, gfxShapedWord::HashMix(0, boundary), + aRunScript, aLanguage, vertical, appUnitsPerDevUnit, + flags | gfx::ShapedTextFlags::TEXT_IS_8BIT, rounding, tp, + [&](gfxShapedWord* aShapedWord) { + aTextRun->CopyGlyphDataFrom(aShapedWord, aRunStart + i); + if (boundary == ' ') { + aTextRun->GetCharacterGlyphs()[aRunStart + i].SetIsSpace(); + } + }); + if (!processed) { + return false; + } + } + hash = 0; + wordStart = i + 1; + wordIs8Bit = true; + continue; + } + + if (i == aRunLength) { + break; + } + + NS_ASSERTION(invalid, "how did we get here except via an invalid char?"); + + // word was terminated by an invalid char: skip it, + // unless it's a control char that we want to show as a hexbox, + // but record where TAB or NEWLINE occur + if (ch == '\t') { + aTextRun->SetIsTab(aRunStart + i); + } else if (ch == '\n') { + aTextRun->SetIsNewline(aRunStart + i); + } else if (GetGeneralCategory(ch) == HB_UNICODE_GENERAL_CATEGORY_FORMAT) { + aTextRun->SetIsFormattingControl(aRunStart + i); + } else if (IsInvalidControlChar(ch) && + !(aTextRun->GetFlags() & + gfx::ShapedTextFlags::TEXT_HIDE_CONTROL_CHARACTERS)) { + if (GetFontEntry()->IsUserFont() && HasCharacter(ch)) { + ShapeFragmentWithoutWordCache(aDrawTarget, aString + i, aRunStart + i, + 1, aRunScript, aLanguage, vertical, + rounding, aTextRun); + } else { + aTextRun->SetMissingGlyph(aRunStart + i, ch, this); + } + } + + hash = 0; + wordStart = i + 1; + wordIs8Bit = true; + } + + return true; +} + +// Explicit instantiations of SplitAndInitTextRun, to avoid libxul link failure +template bool gfxFont::SplitAndInitTextRun( + DrawTarget* aDrawTarget, gfxTextRun* aTextRun, const uint8_t* aString, + uint32_t aRunStart, uint32_t aRunLength, Script aRunScript, + nsAtom* aLanguage, ShapedTextFlags aOrientation); +template bool gfxFont::SplitAndInitTextRun( + DrawTarget* aDrawTarget, gfxTextRun* aTextRun, const char16_t* aString, + uint32_t aRunStart, uint32_t aRunLength, Script aRunScript, + nsAtom* aLanguage, ShapedTextFlags aOrientation); + +template <> +bool gfxFont::InitFakeSmallCapsRun( + nsPresContext* aPresContext, DrawTarget* aDrawTarget, gfxTextRun* aTextRun, + const char16_t* aText, uint32_t aOffset, uint32_t aLength, + FontMatchType aMatchType, gfx::ShapedTextFlags aOrientation, Script aScript, + nsAtom* aLanguage, bool aSyntheticLower, bool aSyntheticUpper) { + bool ok = true; + + RefPtr<gfxFont> smallCapsFont = GetSmallCapsFont(); + if (!smallCapsFont) { + NS_WARNING("failed to get reduced-size font for smallcaps!"); + smallCapsFont = this; + } + + bool isCJK = gfxTextRun::IsCJKScript(aScript); + + enum RunCaseAction { kNoChange, kUppercaseReduce, kUppercase }; + + RunCaseAction runAction = kNoChange; + uint32_t runStart = 0; + + for (uint32_t i = 0; i <= aLength; ++i) { + uint32_t extraCodeUnits = 0; // Will be set to 1 if we need to consume + // a trailing surrogate as well as the + // current code unit. + RunCaseAction chAction = kNoChange; + // Unless we're at the end, figure out what treatment the current + // character will need. + if (i < aLength) { + uint32_t ch = aText[i]; + if (i < aLength - 1 && NS_IS_SURROGATE_PAIR(ch, aText[i + 1])) { + ch = SURROGATE_TO_UCS4(ch, aText[i + 1]); + extraCodeUnits = 1; + } + // Characters that aren't the start of a cluster are ignored here. + // They get added to whatever lowercase/non-lowercase run we're in. + if (IsClusterExtender(ch)) { + chAction = runAction; + } else { + if (ch != ToUpperCase(ch) || SpecialUpper(ch)) { + // ch is lower case + chAction = (aSyntheticLower ? kUppercaseReduce : kNoChange); + } else if (ch != ToLowerCase(ch)) { + // ch is upper case + chAction = (aSyntheticUpper ? kUppercaseReduce : kNoChange); + if (aLanguage == nsGkAtoms::el) { + // In Greek, check for characters that will be modified by + // the GreekUpperCase mapping - this catches accented + // capitals where the accent is to be removed (bug 307039). + // These are handled by using the full-size font with the + // uppercasing transform. + mozilla::GreekCasing::State state; + bool markEta, updateEta; + uint32_t ch2 = + mozilla::GreekCasing::UpperCase(ch, state, markEta, updateEta); + if ((ch != ch2 || markEta) && !aSyntheticUpper) { + chAction = kUppercase; + } + } + } + } + } + + // At the end of the text or when the current character needs different + // casing treatment from the current run, finish the run-in-progress + // and prepare to accumulate a new run. + // Note that we do not look at any source data for offset [i] here, + // as that would be invalid in the case where i==length. + if ((i == aLength || runAction != chAction) && runStart < i) { + uint32_t runLength = i - runStart; + gfxFont* f = this; + switch (runAction) { + case kNoChange: + // just use the current font and the existing string + aTextRun->AddGlyphRun(f, aMatchType, aOffset + runStart, true, + aOrientation, isCJK); + if (!f->SplitAndInitTextRun(aDrawTarget, aTextRun, aText + runStart, + aOffset + runStart, runLength, aScript, + aLanguage, aOrientation)) { + ok = false; + } + break; + + case kUppercaseReduce: + // use reduced-size font, then fall through to uppercase the text + f = smallCapsFont; + [[fallthrough]]; + + case kUppercase: + // apply uppercase transform to the string + nsDependentSubstring origString(aText + runStart, runLength); + nsAutoString convertedString; + AutoTArray<bool, 50> charsToMergeArray; + AutoTArray<bool, 50> deletedCharsArray; + + StyleTextTransform globalTransform{StyleTextTransformCase::Uppercase, + {}}; + // No mask needed; we're doing case conversion, not password-hiding. + const char16_t maskChar = 0; + bool mergeNeeded = nsCaseTransformTextRunFactory::TransformString( + origString, convertedString, Some(globalTransform), maskChar, + /* aCaseTransformsOnly = */ false, aLanguage, charsToMergeArray, + deletedCharsArray); + + if (mergeNeeded) { + // This is the hard case: the transformation caused chars + // to be inserted or deleted, so we can't shape directly + // into the destination textrun but have to handle the + // mismatch of character positions. + gfxTextRunFactory::Parameters params = { + aDrawTarget, nullptr, nullptr, + nullptr, 0, aTextRun->GetAppUnitsPerDevUnit()}; + RefPtr<gfxTextRun> tempRun(gfxTextRun::Create( + ¶ms, convertedString.Length(), aTextRun->GetFontGroup(), + gfx::ShapedTextFlags(), nsTextFrameUtils::Flags())); + tempRun->AddGlyphRun(f, aMatchType, 0, true, aOrientation, isCJK); + if (!f->SplitAndInitTextRun(aDrawTarget, tempRun.get(), + convertedString.BeginReading(), 0, + convertedString.Length(), aScript, + aLanguage, aOrientation)) { + ok = false; + } else { + RefPtr<gfxTextRun> mergedRun(gfxTextRun::Create( + ¶ms, runLength, aTextRun->GetFontGroup(), + gfx::ShapedTextFlags(), nsTextFrameUtils::Flags())); + MergeCharactersInTextRun(mergedRun.get(), tempRun.get(), + charsToMergeArray.Elements(), + deletedCharsArray.Elements()); + gfxTextRun::Range runRange(0, runLength); + aTextRun->CopyGlyphDataFrom(mergedRun.get(), runRange, + aOffset + runStart); + } + } else { + aTextRun->AddGlyphRun(f, aMatchType, aOffset + runStart, true, + aOrientation, isCJK); + if (!f->SplitAndInitTextRun(aDrawTarget, aTextRun, + convertedString.BeginReading(), + aOffset + runStart, runLength, aScript, + aLanguage, aOrientation)) { + ok = false; + } + } + break; + } + + runStart = i; + } + + i += extraCodeUnits; + if (i < aLength) { + runAction = chAction; + } + } + + return ok; +} + +template <> +bool gfxFont::InitFakeSmallCapsRun( + nsPresContext* aPresContext, DrawTarget* aDrawTarget, gfxTextRun* aTextRun, + const uint8_t* aText, uint32_t aOffset, uint32_t aLength, + FontMatchType aMatchType, gfx::ShapedTextFlags aOrientation, Script aScript, + nsAtom* aLanguage, bool aSyntheticLower, bool aSyntheticUpper) { + NS_ConvertASCIItoUTF16 unicodeString(reinterpret_cast<const char*>(aText), + aLength); + return InitFakeSmallCapsRun(aPresContext, aDrawTarget, aTextRun, + static_cast<const char16_t*>(unicodeString.get()), + aOffset, aLength, aMatchType, aOrientation, + aScript, aLanguage, aSyntheticLower, + aSyntheticUpper); +} + +already_AddRefed<gfxFont> gfxFont::GetSmallCapsFont() const { + gfxFontStyle style(*GetStyle()); + style.size *= SMALL_CAPS_SCALE_FACTOR; + style.variantCaps = NS_FONT_VARIANT_CAPS_NORMAL; + gfxFontEntry* fe = GetFontEntry(); + return fe->FindOrMakeFont(&style, mUnicodeRangeMap); +} + +already_AddRefed<gfxFont> gfxFont::GetSubSuperscriptFont( + int32_t aAppUnitsPerDevPixel) const { + gfxFontStyle style(*GetStyle()); + style.AdjustForSubSuperscript(aAppUnitsPerDevPixel); + gfxFontEntry* fe = GetFontEntry(); + return fe->FindOrMakeFont(&style, mUnicodeRangeMap); +} + +gfxGlyphExtents* gfxFont::GetOrCreateGlyphExtents(int32_t aAppUnitsPerDevUnit) { + uint32_t readCount; + { + AutoReadLock lock(mLock); + readCount = mGlyphExtentsArray.Length(); + for (uint32_t i = 0; i < readCount; ++i) { + if (mGlyphExtentsArray[i]->GetAppUnitsPerDevUnit() == aAppUnitsPerDevUnit) + return mGlyphExtentsArray[i].get(); + } + } + AutoWriteLock lock(mLock); + // Re-check in case of race. + uint32_t count = mGlyphExtentsArray.Length(); + for (uint32_t i = readCount; i < count; ++i) { + if (mGlyphExtentsArray[i]->GetAppUnitsPerDevUnit() == aAppUnitsPerDevUnit) + return mGlyphExtentsArray[i].get(); + } + gfxGlyphExtents* glyphExtents = new gfxGlyphExtents(aAppUnitsPerDevUnit); + if (glyphExtents) { + mGlyphExtentsArray.AppendElement(glyphExtents); + // Initialize the extents of a space glyph, assuming that spaces don't + // render anything! + glyphExtents->SetContainedGlyphWidthAppUnits(GetSpaceGlyph(), 0); + } + return glyphExtents; +} + +void gfxFont::SetupGlyphExtents(DrawTarget* aDrawTarget, uint32_t aGlyphID, + bool aNeedTight, gfxGlyphExtents* aExtents) { + gfxRect svgBounds; + if (mFontEntry->TryGetSVGData(this) && mFontEntry->HasSVGGlyph(aGlyphID) && + mFontEntry->GetSVGGlyphExtents(aDrawTarget, aGlyphID, GetAdjustedSize(), + &svgBounds)) { + gfxFloat d2a = aExtents->GetAppUnitsPerDevUnit(); + aExtents->SetTightGlyphExtents( + aGlyphID, gfxRect(svgBounds.X() * d2a, svgBounds.Y() * d2a, + svgBounds.Width() * d2a, svgBounds.Height() * d2a)); + return; + } + + if (mFontEntry->TryGetColorGlyphs() && mFontEntry->mCOLR && + COLRFonts::GetColrTableVersion(mFontEntry->mCOLR) == 1) { + auto* shaper = GetHarfBuzzShaper(); + if (shaper && shaper->IsInitialized()) { + RefPtr scaledFont = GetScaledFont(aDrawTarget); + Rect r = COLRFonts::GetColorGlyphBounds( + mFontEntry->mCOLR, shaper->GetHBFont(), aGlyphID, aDrawTarget, + scaledFont, mFUnitsConvFactor); + if (!r.IsEmpty()) { + gfxFloat d2a = aExtents->GetAppUnitsPerDevUnit(); + aExtents->SetTightGlyphExtents( + aGlyphID, gfxRect(r.X() * d2a, r.Y() * d2a, r.Width() * d2a, + r.Height() * d2a)); + return; + } + } + } + + gfxRect bounds; + GetGlyphBounds(aGlyphID, &bounds, mAntialiasOption == kAntialiasNone); + + const Metrics& fontMetrics = GetMetrics(nsFontMetrics::eHorizontal); + int32_t appUnitsPerDevUnit = aExtents->GetAppUnitsPerDevUnit(); + if (!aNeedTight && bounds.x >= 0.0 && bounds.y >= -fontMetrics.maxAscent && + bounds.height + bounds.y <= fontMetrics.maxDescent) { + uint32_t appUnitsWidth = + uint32_t(ceil((bounds.x + bounds.width) * appUnitsPerDevUnit)); + if (appUnitsWidth < gfxGlyphExtents::INVALID_WIDTH) { + aExtents->SetContainedGlyphWidthAppUnits(aGlyphID, + uint16_t(appUnitsWidth)); + return; + } + } +#ifdef DEBUG_TEXT_RUN_STORAGE_METRICS + if (!aNeedTight) { + ++gGlyphExtentsSetupFallBackToTight; + } +#endif + + gfxFloat d2a = appUnitsPerDevUnit; + aExtents->SetTightGlyphExtents( + aGlyphID, gfxRect(bounds.x * d2a, bounds.y * d2a, bounds.width * d2a, + bounds.height * d2a)); +} + +// Try to initialize font metrics by reading sfnt tables directly; +// set mIsValid=TRUE and return TRUE on success. +// Return FALSE if the gfxFontEntry subclass does not +// implement GetFontTable(), or for non-sfnt fonts where tables are +// not available. +// If this returns TRUE without setting the mIsValid flag, then we -did- +// apparently find an sfnt, but it was too broken to be used. +bool gfxFont::InitMetricsFromSfntTables(Metrics& aMetrics) { + mIsValid = false; // font is NOT valid in case of early return + + const uint32_t kHheaTableTag = TRUETYPE_TAG('h', 'h', 'e', 'a'); + const uint32_t kOS_2TableTag = TRUETYPE_TAG('O', 'S', '/', '2'); + + uint32_t len; + + if (mFUnitsConvFactor < 0.0) { + // If the conversion factor from FUnits is not yet set, + // get the unitsPerEm from the 'head' table via the font entry + uint16_t unitsPerEm = GetFontEntry()->UnitsPerEm(); + if (unitsPerEm == gfxFontEntry::kInvalidUPEM) { + return false; + } + mFUnitsConvFactor = GetAdjustedSize() / unitsPerEm; + } + + // 'hhea' table is required for the advanceWidthMax field + gfxFontEntry::AutoTable hheaTable(mFontEntry, kHheaTableTag); + if (!hheaTable) { + return false; // no 'hhea' table -> not an sfnt + } + const MetricsHeader* hhea = + reinterpret_cast<const MetricsHeader*>(hb_blob_get_data(hheaTable, &len)); + if (len < sizeof(MetricsHeader)) { + return false; + } + +#define SET_UNSIGNED(field, src) \ + aMetrics.field = uint16_t(src) * mFUnitsConvFactor +#define SET_SIGNED(field, src) aMetrics.field = int16_t(src) * mFUnitsConvFactor + + SET_UNSIGNED(maxAdvance, hhea->advanceWidthMax); + + // 'OS/2' table is optional, if not found we'll estimate xHeight + // and aveCharWidth by measuring glyphs + gfxFontEntry::AutoTable os2Table(mFontEntry, kOS_2TableTag); + if (os2Table) { + const OS2Table* os2 = + reinterpret_cast<const OS2Table*>(hb_blob_get_data(os2Table, &len)); + // this should always be present in any valid OS/2 of any version + if (len >= offsetof(OS2Table, xAvgCharWidth) + sizeof(int16_t)) { + SET_SIGNED(aveCharWidth, os2->xAvgCharWidth); + } + } + +#undef SET_SIGNED +#undef SET_UNSIGNED + + hb_font_t* hbFont = gfxHarfBuzzShaper::CreateHBFont(this); + hb_position_t position; + + auto FixedToFloat = [](hb_position_t f) -> gfxFloat { return f / 65536.0; }; + + if (hb_ot_metrics_get_position(hbFont, HB_OT_METRICS_TAG_HORIZONTAL_ASCENDER, + &position)) { + aMetrics.maxAscent = FixedToFloat(position); + } + if (hb_ot_metrics_get_position(hbFont, HB_OT_METRICS_TAG_HORIZONTAL_DESCENDER, + &position)) { + aMetrics.maxDescent = -FixedToFloat(position); + } + if (hb_ot_metrics_get_position(hbFont, HB_OT_METRICS_TAG_HORIZONTAL_LINE_GAP, + &position)) { + aMetrics.externalLeading = FixedToFloat(position); + } + + if (hb_ot_metrics_get_position(hbFont, HB_OT_METRICS_TAG_UNDERLINE_OFFSET, + &position)) { + aMetrics.underlineOffset = FixedToFloat(position); + } + if (hb_ot_metrics_get_position(hbFont, HB_OT_METRICS_TAG_UNDERLINE_SIZE, + &position)) { + aMetrics.underlineSize = FixedToFloat(position); + } + if (hb_ot_metrics_get_position(hbFont, HB_OT_METRICS_TAG_STRIKEOUT_OFFSET, + &position)) { + aMetrics.strikeoutOffset = FixedToFloat(position); + } + if (hb_ot_metrics_get_position(hbFont, HB_OT_METRICS_TAG_STRIKEOUT_SIZE, + &position)) { + aMetrics.strikeoutSize = FixedToFloat(position); + } + + // Although sxHeight and sCapHeight are signed fields, we consider + // zero/negative values to be erroneous and just ignore them. + if (hb_ot_metrics_get_position(hbFont, HB_OT_METRICS_TAG_X_HEIGHT, + &position) && + position > 0) { + aMetrics.xHeight = FixedToFloat(position); + } + if (hb_ot_metrics_get_position(hbFont, HB_OT_METRICS_TAG_CAP_HEIGHT, + &position) && + position > 0) { + aMetrics.capHeight = FixedToFloat(position); + } + hb_font_destroy(hbFont); + + mIsValid = true; + + return true; +} + +static double RoundToNearestMultiple(double aValue, double aFraction) { + return floor(aValue / aFraction + 0.5) * aFraction; +} + +void gfxFont::CalculateDerivedMetrics(Metrics& aMetrics) { + aMetrics.maxAscent = + ceil(RoundToNearestMultiple(aMetrics.maxAscent, 1 / 1024.0)); + aMetrics.maxDescent = + ceil(RoundToNearestMultiple(aMetrics.maxDescent, 1 / 1024.0)); + + if (aMetrics.xHeight <= 0) { + // only happens if we couldn't find either font metrics + // or a char to measure; + // pick an arbitrary value that's better than zero + aMetrics.xHeight = aMetrics.maxAscent * DEFAULT_XHEIGHT_FACTOR; + } + + // If we have a font that doesn't provide a capHeight value, use maxAscent + // as a reasonable fallback. + if (aMetrics.capHeight <= 0) { + aMetrics.capHeight = aMetrics.maxAscent; + } + + aMetrics.maxHeight = aMetrics.maxAscent + aMetrics.maxDescent; + + if (aMetrics.maxHeight - aMetrics.emHeight > 0.0) { + aMetrics.internalLeading = aMetrics.maxHeight - aMetrics.emHeight; + } else { + aMetrics.internalLeading = 0.0; + } + + aMetrics.emAscent = + aMetrics.maxAscent * aMetrics.emHeight / aMetrics.maxHeight; + aMetrics.emDescent = aMetrics.emHeight - aMetrics.emAscent; + + if (GetFontEntry()->IsFixedPitch()) { + // Some Quartz fonts are fixed pitch, but there's some glyph with a bigger + // advance than the average character width... this forces + // those fonts to be recognized like fixed pitch fonts by layout. + aMetrics.maxAdvance = aMetrics.aveCharWidth; + } + + if (!aMetrics.strikeoutOffset) { + aMetrics.strikeoutOffset = aMetrics.xHeight * 0.5; + } + if (!aMetrics.strikeoutSize) { + aMetrics.strikeoutSize = aMetrics.underlineSize; + } +} + +void gfxFont::SanitizeMetrics(gfxFont::Metrics* aMetrics, + bool aIsBadUnderlineFont) { + // Even if this font size is zero, this font is created with non-zero size. + // However, for layout and others, we should return the metrics of zero size + // font. + if (mStyle.AdjustedSizeMustBeZero()) { + memset(aMetrics, 0, sizeof(gfxFont::Metrics)); + return; + } + + // If the font entry has ascent/descent/lineGap-override values, + // replace the metrics from the font with the overrides. + gfxFloat adjustedSize = GetAdjustedSize(); + if (mFontEntry->mAscentOverride >= 0.0) { + aMetrics->maxAscent = mFontEntry->mAscentOverride * adjustedSize; + aMetrics->maxHeight = aMetrics->maxAscent + aMetrics->maxDescent; + aMetrics->internalLeading = + std::max(0.0, aMetrics->maxHeight - aMetrics->emHeight); + } + if (mFontEntry->mDescentOverride >= 0.0) { + aMetrics->maxDescent = mFontEntry->mDescentOverride * adjustedSize; + aMetrics->maxHeight = aMetrics->maxAscent + aMetrics->maxDescent; + aMetrics->internalLeading = + std::max(0.0, aMetrics->maxHeight - aMetrics->emHeight); + } + if (mFontEntry->mLineGapOverride >= 0.0) { + aMetrics->externalLeading = mFontEntry->mLineGapOverride * adjustedSize; + } + + aMetrics->underlineSize = std::max(1.0, aMetrics->underlineSize); + aMetrics->strikeoutSize = std::max(1.0, aMetrics->strikeoutSize); + + aMetrics->underlineOffset = std::min(aMetrics->underlineOffset, -1.0); + + if (aMetrics->maxAscent < 1.0) { + // We cannot draw strikeout line and overline in the ascent... + aMetrics->underlineSize = 0; + aMetrics->underlineOffset = 0; + aMetrics->strikeoutSize = 0; + aMetrics->strikeoutOffset = 0; + return; + } + + /** + * Some CJK fonts have bad underline offset. Therefore, if this is such font, + * we need to lower the underline offset to bottom of *em* descent. + * However, if this is system font, we should not do this for the rendering + * compatibility with another application's UI on the platform. + * XXX Should not use this hack if the font size is too small? + * Such text cannot be read, this might be used for tight CSS + * rendering? (E.g., Acid2) + */ + if (!mStyle.systemFont && aIsBadUnderlineFont) { + // First, we need 2 pixels between baseline and underline at least. Because + // many CJK characters put their glyphs on the baseline, so, 1 pixel is too + // close for CJK characters. + aMetrics->underlineOffset = std::min(aMetrics->underlineOffset, -2.0); + + // Next, we put the underline to bottom of below of the descent space. + if (aMetrics->internalLeading + aMetrics->externalLeading > + aMetrics->underlineSize) { + aMetrics->underlineOffset = + std::min(aMetrics->underlineOffset, -aMetrics->emDescent); + } else { + aMetrics->underlineOffset = + std::min(aMetrics->underlineOffset, + aMetrics->underlineSize - aMetrics->emDescent); + } + } + // If underline positioned is too far from the text, descent position is + // preferred so that underline will stay within the boundary. + else if (aMetrics->underlineSize - aMetrics->underlineOffset > + aMetrics->maxDescent) { + if (aMetrics->underlineSize > aMetrics->maxDescent) + aMetrics->underlineSize = std::max(aMetrics->maxDescent, 1.0); + // The max underlineOffset is 1px (the min underlineSize is 1px, and min + // maxDescent is 0px.) + aMetrics->underlineOffset = aMetrics->underlineSize - aMetrics->maxDescent; + } + + // If strikeout line is overflowed from the ascent, the line should be resized + // and moved for that being in the ascent space. Note that the strikeoutOffset + // is *middle* of the strikeout line position. + gfxFloat halfOfStrikeoutSize = floor(aMetrics->strikeoutSize / 2.0 + 0.5); + if (halfOfStrikeoutSize + aMetrics->strikeoutOffset > aMetrics->maxAscent) { + if (aMetrics->strikeoutSize > aMetrics->maxAscent) { + aMetrics->strikeoutSize = std::max(aMetrics->maxAscent, 1.0); + halfOfStrikeoutSize = floor(aMetrics->strikeoutSize / 2.0 + 0.5); + } + gfxFloat ascent = floor(aMetrics->maxAscent + 0.5); + aMetrics->strikeoutOffset = std::max(halfOfStrikeoutSize, ascent / 2.0); + } + + // If overline is larger than the ascent, the line should be resized. + if (aMetrics->underlineSize > aMetrics->maxAscent) { + aMetrics->underlineSize = aMetrics->maxAscent; + } +} + +// Create a Metrics record to be used for vertical layout. This should never +// fail, as we've already decided this is a valid font. We do not have the +// option of marking it invalid (as can happen if we're unable to read +// horizontal metrics), because that could break a font that we're already +// using for horizontal text. +// So we will synthesize *something* usable here even if there aren't any of the +// usual font tables (which can happen in the case of a legacy bitmap or Type1 +// font for which the platform-specific backend used platform APIs instead of +// sfnt tables to create the horizontal metrics). +void gfxFont::CreateVerticalMetrics() { + const uint32_t kHheaTableTag = TRUETYPE_TAG('h', 'h', 'e', 'a'); + const uint32_t kVheaTableTag = TRUETYPE_TAG('v', 'h', 'e', 'a'); + const uint32_t kPostTableTag = TRUETYPE_TAG('p', 'o', 's', 't'); + const uint32_t kOS_2TableTag = TRUETYPE_TAG('O', 'S', '/', '2'); + uint32_t len; + + auto* metrics = new Metrics(); + ::memset(metrics, 0, sizeof(Metrics)); + + // Some basic defaults, in case the font lacks any real metrics tables. + // TODO: consider what rounding (if any) we should apply to these. + metrics->emHeight = GetAdjustedSize(); + metrics->emAscent = metrics->emHeight / 2; + metrics->emDescent = metrics->emHeight - metrics->emAscent; + + metrics->maxAscent = metrics->emAscent; + metrics->maxDescent = metrics->emDescent; + + const float UNINITIALIZED_LEADING = -10000.0f; + metrics->externalLeading = UNINITIALIZED_LEADING; + + if (mFUnitsConvFactor < 0.0) { + uint16_t upem = GetFontEntry()->UnitsPerEm(); + if (upem != gfxFontEntry::kInvalidUPEM) { + AutoWriteLock lock(mLock); + mFUnitsConvFactor = GetAdjustedSize() / upem; + } + } + +#define SET_UNSIGNED(field, src) \ + metrics->field = uint16_t(src) * mFUnitsConvFactor +#define SET_SIGNED(field, src) metrics->field = int16_t(src) * mFUnitsConvFactor + + gfxFontEntry::AutoTable os2Table(mFontEntry, kOS_2TableTag); + if (os2Table && mFUnitsConvFactor >= 0.0) { + const OS2Table* os2 = + reinterpret_cast<const OS2Table*>(hb_blob_get_data(os2Table, &len)); + // These fields should always be present in any valid OS/2 table + if (len >= offsetof(OS2Table, sTypoLineGap) + sizeof(int16_t)) { + SET_SIGNED(strikeoutSize, os2->yStrikeoutSize); + // Use ascent+descent from the horizontal metrics as the default + // advance (aveCharWidth) in vertical mode + gfxFloat ascentDescent = + gfxFloat(mFUnitsConvFactor) * + (int16_t(os2->sTypoAscender) - int16_t(os2->sTypoDescender)); + metrics->aveCharWidth = std::max(metrics->emHeight, ascentDescent); + // Use xAvgCharWidth from horizontal metrics as minimum font extent + // for vertical layout, applying half of it to ascent and half to + // descent (to work with a default centered baseline). + gfxFloat halfCharWidth = + int16_t(os2->xAvgCharWidth) * gfxFloat(mFUnitsConvFactor) / 2; + metrics->maxAscent = std::max(metrics->maxAscent, halfCharWidth); + metrics->maxDescent = std::max(metrics->maxDescent, halfCharWidth); + } + } + + // If we didn't set aveCharWidth from OS/2, try to read 'hhea' metrics + // and use the line height from its ascent/descent. + if (!metrics->aveCharWidth) { + gfxFontEntry::AutoTable hheaTable(mFontEntry, kHheaTableTag); + if (hheaTable && mFUnitsConvFactor >= 0.0) { + const MetricsHeader* hhea = reinterpret_cast<const MetricsHeader*>( + hb_blob_get_data(hheaTable, &len)); + if (len >= sizeof(MetricsHeader)) { + SET_SIGNED(aveCharWidth, + int16_t(hhea->ascender) - int16_t(hhea->descender)); + metrics->maxAscent = metrics->aveCharWidth / 2; + metrics->maxDescent = metrics->aveCharWidth - metrics->maxAscent; + } + } + } + + // Read real vertical metrics if available. + metrics->ideographicWidth = -1.0; + metrics->zeroWidth = -1.0; + gfxFontEntry::AutoTable vheaTable(mFontEntry, kVheaTableTag); + if (vheaTable && mFUnitsConvFactor >= 0.0) { + const MetricsHeader* vhea = reinterpret_cast<const MetricsHeader*>( + hb_blob_get_data(vheaTable, &len)); + if (len >= sizeof(MetricsHeader)) { + SET_UNSIGNED(maxAdvance, vhea->advanceWidthMax); + // Redistribute space between ascent/descent because we want a + // centered vertical baseline by default. + gfxFloat halfExtent = + 0.5 * gfxFloat(mFUnitsConvFactor) * + (int16_t(vhea->ascender) + std::abs(int16_t(vhea->descender))); + // Some bogus fonts have ascent and descent set to zero in 'vhea'. + // In that case we just ignore them and keep our synthetic values + // from above. + if (halfExtent > 0) { + metrics->maxAscent = halfExtent; + metrics->maxDescent = halfExtent; + SET_SIGNED(externalLeading, vhea->lineGap); + } + // Call gfxHarfBuzzShaper::GetGlyphVAdvance directly, as GetCharAdvance + // would potentially recurse if no v-advance is available and it attempts + // to fall back to a value from mVerticalMetrics. + if (gfxHarfBuzzShaper* shaper = GetHarfBuzzShaper()) { + uint32_t gid = ProvidesGetGlyph() + ? GetGlyph(kWaterIdeograph, 0) + : shaper->GetNominalGlyph(kWaterIdeograph); + if (gid) { + int32_t advance = shaper->GetGlyphVAdvance(gid); + // Convert 16.16 fixed-point advance from the shaper to a float. + metrics->ideographicWidth = + advance < 0 ? metrics->aveCharWidth : advance / 65536.0; + } + gid = ProvidesGetGlyph() ? GetGlyph('0', 0) + : shaper->GetNominalGlyph('0'); + if (gid) { + int32_t advance = shaper->GetGlyphVAdvance(gid); + metrics->zeroWidth = + advance < 0 ? metrics->aveCharWidth : advance / 65536.0; + } + } + } + } + + // If we didn't set aveCharWidth above, we must be dealing with a non-sfnt + // font of some kind (Type1, bitmap, vector, ...), so fall back to using + // whatever the platform backend figured out for horizontal layout. + // And if we haven't set externalLeading yet, then copy that from the + // horizontal metrics as well, to help consistency of CSS line-height. + if (!metrics->aveCharWidth || + metrics->externalLeading == UNINITIALIZED_LEADING) { + const Metrics& horizMetrics = GetHorizontalMetrics(); + if (!metrics->aveCharWidth) { + metrics->aveCharWidth = horizMetrics.maxAscent + horizMetrics.maxDescent; + } + if (metrics->externalLeading == UNINITIALIZED_LEADING) { + metrics->externalLeading = horizMetrics.externalLeading; + } + } + + // Get underline thickness from the 'post' table if available. + // We also read the underline position, although in vertical-upright mode + // this will not be appropriate to use directly (see nsTextFrame.cpp). + gfxFontEntry::AutoTable postTable(mFontEntry, kPostTableTag); + if (postTable) { + const PostTable* post = + reinterpret_cast<const PostTable*>(hb_blob_get_data(postTable, &len)); + if (len >= offsetof(PostTable, underlineThickness) + sizeof(uint16_t)) { + static_assert(offsetof(PostTable, underlinePosition) < + offsetof(PostTable, underlineThickness), + "broken PostTable struct?"); + SET_SIGNED(underlineOffset, post->underlinePosition); + SET_UNSIGNED(underlineSize, post->underlineThickness); + // Also use for strikeout if we didn't find that in OS/2 above. + if (!metrics->strikeoutSize) { + metrics->strikeoutSize = metrics->underlineSize; + } + } + } + +#undef SET_UNSIGNED +#undef SET_SIGNED + + // If we didn't read this from a vhea table, it will still be zero. + // In any case, let's make sure it is not less than the value we've + // come up with for aveCharWidth. + metrics->maxAdvance = std::max(metrics->maxAdvance, metrics->aveCharWidth); + + // Thickness of underline and strikeout may have been read from tables, + // but in case they were not present, ensure a minimum of 1 pixel. + metrics->underlineSize = std::max(1.0, metrics->underlineSize); + + metrics->strikeoutSize = std::max(1.0, metrics->strikeoutSize); + metrics->strikeoutOffset = -0.5 * metrics->strikeoutSize; + + // Somewhat arbitrary values for now, subject to future refinement... + metrics->spaceWidth = metrics->aveCharWidth; + metrics->maxHeight = metrics->maxAscent + metrics->maxDescent; + metrics->xHeight = metrics->emHeight / 2; + metrics->capHeight = metrics->maxAscent; + + if (metrics->zeroWidth < 0.0) { + metrics->zeroWidth = metrics->aveCharWidth; + } + + if (!mVerticalMetrics.compareExchange(nullptr, metrics)) { + delete metrics; + } +} + +gfxFloat gfxFont::SynthesizeSpaceWidth(uint32_t aCh) { + // return an appropriate width for various Unicode space characters + // that we "fake" if they're not actually present in the font; + // returns negative value if the char is not a known space. + switch (aCh) { + case 0x2000: // en quad + case 0x2002: + return GetAdjustedSize() / 2; // en space + case 0x2001: // em quad + case 0x2003: + return GetAdjustedSize(); // em space + case 0x2004: + return GetAdjustedSize() / 3; // three-per-em space + case 0x2005: + return GetAdjustedSize() / 4; // four-per-em space + case 0x2006: + return GetAdjustedSize() / 6; // six-per-em space + case 0x2007: + return GetMetrics(nsFontMetrics::eHorizontal) + .ZeroOrAveCharWidth(); // figure space + case 0x2008: + return GetMetrics(nsFontMetrics::eHorizontal) + .spaceWidth; // punctuation space + case 0x2009: + return GetAdjustedSize() / 5; // thin space + case 0x200a: + return GetAdjustedSize() / 10; // hair space + case 0x202f: + return GetAdjustedSize() / 5; // narrow no-break space + case 0x3000: + return GetAdjustedSize(); // ideographic space + default: + return -1.0; + } +} + +void gfxFont::AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf, + FontCacheSizes* aSizes) const { + AutoReadLock lock(mLock); + for (uint32_t i = 0; i < mGlyphExtentsArray.Length(); ++i) { + aSizes->mFontInstances += + mGlyphExtentsArray[i]->SizeOfIncludingThis(aMallocSizeOf); + } + if (mWordCache) { + aSizes->mShapedWords += mWordCache->SizeOfIncludingThis(aMallocSizeOf); + } +} + +void gfxFont::AddSizeOfIncludingThis(MallocSizeOf aMallocSizeOf, + FontCacheSizes* aSizes) const { + aSizes->mFontInstances += aMallocSizeOf(this); + AddSizeOfExcludingThis(aMallocSizeOf, aSizes); +} + +void gfxFont::AddGlyphChangeObserver(GlyphChangeObserver* aObserver) { + AutoWriteLock lock(mLock); + if (!mGlyphChangeObservers) { + mGlyphChangeObservers = MakeUnique<nsTHashSet<GlyphChangeObserver*>>(); + } + mGlyphChangeObservers->Insert(aObserver); +} + +void gfxFont::RemoveGlyphChangeObserver(GlyphChangeObserver* aObserver) { + AutoWriteLock lock(mLock); + NS_ASSERTION(mGlyphChangeObservers, "No observers registered"); + NS_ASSERTION(mGlyphChangeObservers->Contains(aObserver), + "Observer not registered"); + mGlyphChangeObservers->Remove(aObserver); +} + +#define DEFAULT_PIXEL_FONT_SIZE 16.0f + +gfxFontStyle::gfxFontStyle() + : size(DEFAULT_PIXEL_FONT_SIZE), + sizeAdjust(0.0f), + baselineOffset(0.0f), + languageOverride(NO_FONT_LANGUAGE_OVERRIDE), + fontSmoothingBackgroundColor(NS_RGBA(0, 0, 0, 0)), + weight(FontWeight::NORMAL), + stretch(FontStretch::NORMAL), + style(FontSlantStyle::NORMAL), + variantCaps(NS_FONT_VARIANT_CAPS_NORMAL), + variantSubSuper(NS_FONT_VARIANT_POSITION_NORMAL), + sizeAdjustBasis(uint8_t(FontSizeAdjust::Tag::None)), + systemFont(true), + printerFont(false), + useGrayscaleAntialiasing(false), + allowSyntheticWeight(true), + allowSyntheticStyle(true), + allowSyntheticSmallCaps(true), + noFallbackVariantFeatures(true) {} + +gfxFontStyle::gfxFontStyle(FontSlantStyle aStyle, FontWeight aWeight, + FontStretch aStretch, gfxFloat aSize, + const FontSizeAdjust& aSizeAdjust, bool aSystemFont, + bool aPrinterFont, bool aAllowWeightSynthesis, + bool aAllowStyleSynthesis, + bool aAllowSmallCapsSynthesis, + uint32_t aLanguageOverride) + : size(aSize), + baselineOffset(0.0f), + languageOverride(aLanguageOverride), + fontSmoothingBackgroundColor(NS_RGBA(0, 0, 0, 0)), + weight(aWeight), + stretch(aStretch), + style(aStyle), + variantCaps(NS_FONT_VARIANT_CAPS_NORMAL), + variantSubSuper(NS_FONT_VARIANT_POSITION_NORMAL), + systemFont(aSystemFont), + printerFont(aPrinterFont), + useGrayscaleAntialiasing(false), + allowSyntheticWeight(aAllowWeightSynthesis), + allowSyntheticStyle(aAllowStyleSynthesis), + allowSyntheticSmallCaps(aAllowSmallCapsSynthesis), + noFallbackVariantFeatures(true) { + MOZ_ASSERT(!std::isnan(size)); + + switch (aSizeAdjust.tag) { + case FontSizeAdjust::Tag::None: + sizeAdjust = 0.0f; + break; + case FontSizeAdjust::Tag::ExHeight: + sizeAdjust = aSizeAdjust.AsExHeight(); + break; + case FontSizeAdjust::Tag::CapHeight: + sizeAdjust = aSizeAdjust.AsCapHeight(); + break; + case FontSizeAdjust::Tag::ChWidth: + sizeAdjust = aSizeAdjust.AsChWidth(); + break; + case FontSizeAdjust::Tag::IcWidth: + sizeAdjust = aSizeAdjust.AsIcWidth(); + break; + case FontSizeAdjust::Tag::IcHeight: + sizeAdjust = aSizeAdjust.AsIcHeight(); + break; + } + MOZ_ASSERT(!std::isnan(sizeAdjust)); + + sizeAdjustBasis = uint8_t(aSizeAdjust.tag); + // sizeAdjustBasis is currently a small bitfield, so let's assert that the + // tag value was not truncated. + MOZ_ASSERT(FontSizeAdjust::Tag(sizeAdjustBasis) == aSizeAdjust.tag, + "gfxFontStyle.sizeAdjustBasis too small?"); + + if (weight > FontWeight::FromInt(1000)) { + weight = FontWeight::FromInt(1000); + } + if (weight < FontWeight::FromInt(1)) { + weight = FontWeight::FromInt(1); + } + + if (size >= FONT_MAX_SIZE) { + size = FONT_MAX_SIZE; + sizeAdjust = 0.0f; + sizeAdjustBasis = uint8_t(FontSizeAdjust::Tag::None); + } else if (size < 0.0) { + NS_WARNING("negative font size"); + size = 0.0; + } +} + +PLDHashNumber gfxFontStyle::Hash() const { + uint32_t hash = variationSettings.IsEmpty() + ? 0 + : mozilla::HashBytes(variationSettings.Elements(), + variationSettings.Length() * + sizeof(gfxFontVariation)); + return mozilla::AddToHash(hash, systemFont, style.Raw(), stretch.Raw(), + weight.Raw(), size, int32_t(sizeAdjust * 1000.0f)); +} + +void gfxFontStyle::AdjustForSubSuperscript(int32_t aAppUnitsPerDevPixel) { + MOZ_ASSERT( + variantSubSuper != NS_FONT_VARIANT_POSITION_NORMAL && baselineOffset == 0, + "can't adjust this style for sub/superscript"); + + // calculate the baseline offset (before changing the size) + if (variantSubSuper == NS_FONT_VARIANT_POSITION_SUPER) { + baselineOffset = size * -NS_FONT_SUPERSCRIPT_OFFSET_RATIO; + } else { + baselineOffset = size * NS_FONT_SUBSCRIPT_OFFSET_RATIO; + } + + // calculate reduced size, roughly mimicing behavior of font-size: smaller + float cssSize = size * aAppUnitsPerDevPixel / AppUnitsPerCSSPixel(); + if (cssSize < NS_FONT_SUB_SUPER_SMALL_SIZE) { + size *= NS_FONT_SUB_SUPER_SIZE_RATIO_SMALL; + } else if (cssSize >= NS_FONT_SUB_SUPER_LARGE_SIZE) { + size *= NS_FONT_SUB_SUPER_SIZE_RATIO_LARGE; + } else { + gfxFloat t = (cssSize - NS_FONT_SUB_SUPER_SMALL_SIZE) / + (NS_FONT_SUB_SUPER_LARGE_SIZE - NS_FONT_SUB_SUPER_SMALL_SIZE); + size *= (1.0 - t) * NS_FONT_SUB_SUPER_SIZE_RATIO_SMALL + + t * NS_FONT_SUB_SUPER_SIZE_RATIO_LARGE; + } + + // clear the variant field + variantSubSuper = NS_FONT_VARIANT_POSITION_NORMAL; +} + +bool gfxFont::TryGetMathTable() { + if (mMathInitialized) { + return !!mMathTable; + } + + auto face(GetFontEntry()->GetHBFace()); + if (hb_ot_math_has_data(face)) { + auto* mathTable = new gfxMathTable(face, GetAdjustedSize()); + if (!mMathTable.compareExchange(nullptr, mathTable)) { + delete mathTable; + } + } + mMathInitialized = true; + + return !!mMathTable; +} |