/* -*- 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 #include #include 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( FONT_TIMEOUT_SECONDS * 1000, "gfxFontCache", aEventTarget) { nsCOMPtr 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 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 font = entry->mFont; MarkUsedLocked(font, lock); return font.forget(); } already_AddRefed gfxFontCache::MaybeInsert(RefPtr&& aFont) { MOZ_ASSERT(aFont); MutexAutoLock lock(mMutex); Key key(aFont->GetFontEntry(), aFont->GetStyle(), aFont->GetUnicodeRangeMap()); HashEntry* entry = mFonts.PutEntry(key); if (!entry) { return aFont.forget(); } // 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 = std::move(aFont); AddObjectLocked(entry->mFont, lock); // 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 { MarkUsedLocked(entry->mFont, lock); } return do_AddRef(entry->mFont); } void gfxFontCache::NotifyExpiredLocked(gfxFont* aFont, const AutoLock& aLock) { // If there are outstanding references to the font outside the cache, we // should just mark it as used. if (aFont->GetRefCount() > 1) { MarkUsedLocked(aFont, aLock); return; } // We should have the last reference to the font. RemoveObjectLocked(aFont, aLock); Key key(aFont->GetFontEntry(), aFont->GetStyle(), aFont->GetUnicodeRangeMap()); HashEntry* entry = mFonts.GetEntry(key); if (!entry || entry->mFont != aFont) { MOZ_ASSERT_UNREACHABLE("Invalid font?"); return; } mTrackerDiscard.AppendElement(std::move(entry->mFont)); mFonts.RemoveEntry(entry); } void gfxFontCache::NotifyHandlerEnd() { nsTArray> discard; { MutexAutoLock lock(mMutex); discard = std::move(mTrackerDiscard); } DestroyDiscard(discard); } void gfxFontCache::DestroyDiscard(nsTArray>& aDiscard) { for (auto& font : aDiscard) { NS_ASSERTION(font->GetRefCount() == 1, "Destroying with refs outside cache!"); font->ClearCachedWords(); } aDiscard.Clear(); } void gfxFontCache::Flush() { nsTHashtable discard; { MutexAutoLock lock(mMutex); for (auto iter = mFonts.Iter(); !iter.Done(); iter.Next()) { HashEntry* entry = static_cast(iter.Get()); RemoveObjectLocked(entry->mFont, lock); } MOZ_ASSERT(IsEmptyLocked(lock), "Cache tracker still has fonts after flush!"); discard = std::move(mFonts); } } /*static*/ void gfxFontCache::WordCacheExpirationTimerCallback(nsITimer* aTimer, void* aCache) { gfxFontCache* cache = static_cast(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(&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& 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 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 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 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& aFontFeatures, bool aDisableLigatures, const nsACString& aFamilyName, bool aAddSmallCaps, void (*aHandleFeature)(const uint32_t&, uint32_t&, void*), void* aHandleFeatureData) { const nsTArray& 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 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 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(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(); } 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) { uint32_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 { // If the style doesn't call for italic/oblique, or if the face already // provides it, no synthetic style should be added. if (mStyle.style == FontSlantStyle::NORMAL || !mStyle.allowSyntheticStyle || !mFontEntry->IsUpright()) { return 0.0f; } // 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; } // Default or custom oblique 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& 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( 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& 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& 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 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*> gfxFont::sScriptTagToCode; Atomic*> gfxFont::sDefaultFeatures; static inline bool HasSubstitution(uint32_t* aBitVector, intl::Script aScript) { return (aBitVector[static_cast(aScript) >> 5] & (1 << (static_cast(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* tagToCode = sScriptTagToCode; if (!tagToCode) { tagToCode = new nsTHashMap( 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(intl::UnicodeProperties::GetMaxNumberOfScripts() + 1, int(Script::NUM_SCRIPT_CODES))); for (Script s = Script::ARABIC; s < scriptCount; s = Script(static_cast(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(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(s) >> 5; uint32_t bit = static_cast(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(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& 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& 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 gfxFont::GetScaledFont( mozilla::gfx::DrawTarget* aDrawTarget) { TextRunDrawParams params; params.dt = aDrawTarget; return GetScaledFont(params); } void gfxFont::InitializeScaledFont( const RefPtr& 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(moz_xmalloc(mBufSize * sizeof(Glyph))); std::memcpy(mBuffer, *mAutoBuffer.addr(), mNumGlyphs * sizeof(Glyph)); } else { mBuffer = reinterpret_cast( 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 fillPattern; if (mFontParams.contextPaint) { imgDrawingParams imgParams; fillPattern = mFontParams.contextPaint->GetFillPattern( mRunParams.context->GetDrawTarget(), mRunParams.context->CurrentMatrixDouble(), imgParams); } if (!fillPattern) { if (state.pattern) { RefPtr 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 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 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(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(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 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)); if (FC == FontComplexityT::ComplexFont) { const FontDrawParams& fontParams(aBuffer.mFontParams); auto* textDrawer = runParams.context->GetTextDrawer(); 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.context->GetTextDrawer(); 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), 1.0 / aRunParams.devPerApp, 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::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::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; } } } void gfxFont::Draw(const gfxTextRun* aTextRun, uint32_t aStart, uint32_t aEnd, gfx::Point* aPt, const 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.context->GetTextDrawer(); 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(); gfxFontEntry::AutoHBFace face = GetFontEntry()->GetHBFace(); fontParams.palette = COLRFonts::SetupColorPalette( face, aRunParams.paletteValueSet, aRunParams.fontPalette, GetFontEntry()->FamilyName()); } 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 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 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) { gfx::Float xscale = 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 { fontParams.synBoldOnePixelOffset = 0; fontParams.extraStrikes = 0; } 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( aTextRun, aStart, aEnd - aStart, aPt, offsetMatrix, buffer); } else { emittedGlyphs = DrawGlyphs( aTextRun, aStart, aEnd - aStart, aPt, offsetMatrix, buffer); } } else { if (aRunParams.spacing) { emittedGlyphs = DrawGlyphs( aTextRun, aStart, aEnd - aStart, aPt, offsetMatrix, buffer); } else { emittedGlyphs = DrawGlyphs( 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.get(), 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.get()); 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 = // 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 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 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>(); } 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& 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) { float synBoldOffset = GetSyntheticBoldOffset() * CalcXScale(aDrawTarget); aShapedText->AdjustAdvancesForSyntheticBold(synBoldOffset, 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 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 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 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 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 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 charsToMergeArray; AutoTArray deletedCharsArray; StyleTextTransform globalTransform{StyleTextTransformCase::Uppercase, {}}; bool mergeNeeded = nsCaseTransformTextRunFactory::TransformString( origString, convertedString, Some(globalTransform), /* 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 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 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(aText), aLength); return InitFakeSmallCapsRun(aPresContext, aDrawTarget, aTextRun, static_cast(unicodeString.get()), aOffset, aLength, aMatchType, aOrientation, aScript, aLanguage, aSyntheticLower, aSyntheticUpper); } already_AddRefed 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::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(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(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(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( 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; gfxFontEntry::AutoTable vheaTable(mFontEntry, kVheaTableTag); if (vheaTable && mFUnitsConvFactor >= 0.0) { const MetricsHeader* vhea = reinterpret_cast( 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); 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; } } } // 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(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->zeroWidth = metrics->aveCharWidth; metrics->maxHeight = metrics->maxAscent + metrics->maxDescent; metrics->xHeight = metrics->emHeight / 2; metrics->capHeight = metrics->maxAscent; 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>(); } 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(!mozilla::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(!mozilla::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; }