/* -*- 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 "nsContentUtils.h"
#include "nsSpecialCasingData.h"
#include "nsTextRunTransformations.h"
#include "nsUGenCategory.h"
#include "nsUnicodeProperties.h"
#include "nsStyleConsts.h"
#include "mozilla/AppUnits.h"
#include "mozilla/HashTable.h"
#include "mozilla/Likely.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/glean/GfxMetrics.h"
#include "gfxMathTable.h"
#include "gfxSVGGlyphs.h"
#include "gfx2DGlue.h"
#include "TextDrawTarget.h"
#include "ThebesRLBox.h"
#include "GreekCasing.h"
#include "cairo.h"
#ifdef XP_WIN
# include
"cairo-win32.h"
# include
"gfxWindowsPlatform.h"
#endif
#include "harfbuzz/hb.h"
#include "harfbuzz/hb-ot.h"
#include <algorithm>
#include <limits>
#include <cmath>
using namespace mozilla;
using namespace mozilla::gfx;
using namespace mozilla::unicode;
using mozilla::services::GetObserverService;
gfxFontCache* gfxFontCache::gGlobalCache = nullptr;
#ifdef DEBUG_roc
# define DEBUG_TEXT_RUN_STORAGE_METRICS
#endif
#ifdef DEBUG_TEXT_RUN_STORAGE_METRICS
uint32_t gTextRunStorageHighWaterMark = 0;
uint32_t gTextRunStorage = 0;
uint32_t gFontCount = 0;
uint32_t gGlyphExtentsCount = 0;
uint32_t gGlyphExtentsWidthsTotalSize = 0;
uint32_t gGlyphExtentsSetupEagerSimple = 0;
uint32_t gGlyphExtentsSetupEagerTight = 0;
uint32_t gGlyphExtentsSetupLazyTight = 0;
uint32_t gGlyphExtentsSetupFallBackToTight = 0;
#endif
#define LOG_FONTINIT(args) \
MOZ_LOG(gfxPlatform::GetLog(eGfxLog_fontinit), LogLevel::Debug, args)
#define LOG_FONTINIT_ENABLED() \
MOZ_LOG_TEST(gfxPlatform::GetLog(eGfxLog_fontinit), LogLevel::Debug)
/*
* gfxFontCache - global cache of gfxFont instances.
* Expires unused fonts after a short interval;
* notifies fonts to age their cached shaped-word records;
* observes memory-pressure notification and tells fonts to clear their
* shaped-word caches to free up memory.
*/
MOZ_DEFINE_MALLOC_SIZE_OF(FontCacheMallocSizeOf)
NS_IMPL_ISUPPORTS(gfxFontCache::MemoryReporter, nsIMemoryReporter)
/*virtual*/
gfxTextRunFactory::~gfxTextRunFactory() {
// Should not be dropped by stylo
MOZ_ASSERT(!Servo_IsWorkerThread());
}
NS_IMETHODIMP
gfxFontCache::MemoryReporter::CollectReports(
nsIHandleReportCallback* aHandleReport, nsISupports* aData,
bool aAnonymize) {
FontCacheSizes sizes;
gfxFontCache::GetCache()->AddSizeOfIncludingThis(&FontCacheMallocSizeOf,
&sizes);
MOZ_COLLECT_REPORT(
"explicit/gfx/font-cache", KIND_HEAP, UNITS_BYTES,
sizes.mFontInstances,
"Memory used for active font instances.");
MOZ_COLLECT_REPORT(
"explicit/gfx/font-shaped-words", KIND_HEAP, UNITS_BYTES,
sizes.mShapedWords,
"Memory used to cache shaped glyph data.");
return NS_OK;
}
NS_IMPL_ISUPPORTS(gfxFontCache::Observer, nsIObserver)
NS_IMETHODIMP
gfxFontCache::Observer::Observe(nsISupports* aSubject,
const char* aTopic,
const char16_t* someData) {
if (!nsCRT::strcmp(aTopic,
"memory-pressure")) {
gfxFontCache* fontCache = gfxFontCache::GetCache();
if (fontCache) {
fontCache->FlushShapedWordCaches();
}
}
else {
MOZ_ASSERT_UNREACHABLE(
"unexpected notification topic");
}
return NS_OK;
}
nsresult gfxFontCache::Init() {
NS_ASSERTION(!gGlobalCache,
"Where did this come from?");
gGlobalCache =
new gfxFontCache(GetMainThreadSerialEventTarget());
if (!gGlobalCache) {
return NS_ERROR_OUT_OF_MEMORY;
}
RegisterStrongMemoryReporter(
new MemoryReporter());
return NS_OK;
}
void gfxFontCache::Shutdown() {
delete gGlobalCache;
gGlobalCache = nullptr;
#ifdef DEBUG_TEXT_RUN_STORAGE_METRICS
printf(
"Textrun storage high water mark=%d\n", gTextRunStorageHighWaterMark);
printf(
"Total number of fonts=%d\n", gFontCount);
printf(
"Total glyph extents allocated=%d (size %d)\n", gGlyphExtentsCount,
int(gGlyphExtentsCount *
sizeof(gfxGlyphExtents)));
printf(
"Total glyph extents width-storage size allocated=%d\n",
gGlyphExtentsWidthsTotalSize);
printf(
"Number of simple glyph extents eagerly requested=%d\n",
gGlyphExtentsSetupEagerSimple);
printf(
"Number of tight glyph extents eagerly requested=%d\n",
gGlyphExtentsSetupEagerTight);
printf(
"Number of tight glyph extents lazily requested=%d\n",
gGlyphExtentsSetupLazyTight);
printf(
"Number of simple glyph extent setups that fell back to tight=%d\n",
gGlyphExtentsSetupFallBackToTight);
#endif
}
gfxFontCache::gfxFontCache(nsIEventTarget* aEventTarget)
: ExpirationTrackerImpl<gfxFont, 3, Lock, AutoLock>(
FONT_TIMEOUT_SECONDS * 1000,
"gfxFontCache", aEventTarget) {
nsCOMPtr<nsIObserverService> obs = GetObserverService();
if (obs) {
obs->AddObserver(
new Observer,
"memory-pressure",
false);
}
nsIEventTarget* target = nullptr;
if (XRE_IsContentProcess() && NS_IsMainThread()) {
target = aEventTarget;
}
// Create the timer used to expire shaped-word records from each font's
// cache after a short period of non-use. We have a single timer in
// gfxFontCache that loops over all fonts known to the cache, to avoid
// the overhead of individual timers in each font instance.
// The timer will be started any time shaped word records are cached
// (and pauses itself when all caches become empty).
mWordCacheExpirationTimer = NS_NewTimer(target);
}
gfxFontCache::~gfxFontCache() {
// Ensure the user font cache releases its references to font entries,
// so they aren't kept alive after the font instances and font-list
// have been shut down.
gfxUserFontSet::UserFontCache::Shutdown();
if (mWordCacheExpirationTimer) {
mWordCacheExpirationTimer->Cancel();
mWordCacheExpirationTimer = nullptr;
}
// Expire everything manually so we don't leak them.
Flush();
}
bool gfxFontCache::HashEntry::KeyEquals(
const KeyTypePointer aKey)
const {
const gfxCharacterMap* fontUnicodeRangeMap = mFont->GetUnicodeRangeMap();
return aKey->mFontEntry == mFont->GetFontEntry() &&
aKey->mStyle->Equals(*mFont->GetStyle()) &&
((!aKey->mUnicodeRangeMap && !fontUnicodeRangeMap) ||
(aKey->mUnicodeRangeMap && fontUnicodeRangeMap &&
aKey->mUnicodeRangeMap->Equals(fontUnicodeRangeMap)));
}
already_AddRefed<gfxFont> gfxFontCache::Lookup(
const gfxFontEntry* aFontEntry,
const gfxFontStyle* aStyle,
const gfxCharacterMap* aUnicodeRangeMap) {
MutexAutoLock lock(mMutex);
Key key(aFontEntry, aStyle, aUnicodeRangeMap);
HashEntry* entry = mFonts.GetEntry(key);
glean::fontlist::font_cache_hit
.EnumGet(
static_cast<glean::fontlist::FontCacheHitLabel>(entry != nullptr))
.Add();
if (!entry) {
return nullptr;
}
RefPtr<gfxFont> font = entry->mFont;
if (font->GetExpirationState()->IsTracked()) {
RemoveObjectLocked(font, lock);
}
return font.forget();
}
already_AddRefed<gfxFont> gfxFontCache::MaybeInsert(gfxFont* aFont) {
MOZ_ASSERT(aFont);
MutexAutoLock lock(mMutex);
Key key(aFont->GetFontEntry(), aFont->GetStyle(),
aFont->GetUnicodeRangeMap());
HashEntry* entry = mFonts.PutEntry(key);
if (!entry) {
return do_AddRef(aFont);
}
// If it is null, then we are inserting a new entry. Otherwise we are
// attempting to replace an existing font, probably due to a thread race, in
// which case stick with the original font.
if (!entry->mFont) {
entry->mFont = aFont;
// Assert that we can find the entry we just put in (this fails if the key
// has a NaN float value in it, e.g. 'sizeAdjust').
MOZ_ASSERT(entry == mFonts.GetEntry(key));
}
else {
MOZ_ASSERT(entry->mFont != aFont);
aFont->Destroy();
if (entry->mFont->GetExpirationState()->IsTracked()) {
RemoveObjectLocked(entry->mFont, lock);
}
}
return do_AddRef(entry->mFont);
}
bool gfxFontCache::MaybeDestroy(gfxFont* aFont) {
MOZ_ASSERT(aFont);
MutexAutoLock lock(mMutex);
// If the font has a non-zero refcount, then we must have lost the race with
// gfxFontCache::Lookup and the same font was reacquired.
if (aFont->GetRefCount() > 0) {
return false;
}
Key key(aFont->GetFontEntry(), aFont->GetStyle(),
aFont->GetUnicodeRangeMap());
HashEntry* entry = mFonts.GetEntry(key);
if (!entry || entry->mFont != aFont) {
MOZ_ASSERT(!aFont->GetExpirationState()->IsTracked());
return true;
}
// If the font is being tracked, we must have then also lost another race with
// gfxFontCache::MaybeDestroy which re-added it to the tracker.
if (aFont->GetExpirationState()->IsTracked()) {
return false;
}
// Typically this won't fail, but it may during startup/shutdown if the timer
// service is not available.
nsresult rv = AddObjectLocked(aFont, lock);
if (NS_SUCCEEDED(rv)) {
return false;
}
mFonts.RemoveEntry(entry);
return true;
}
void gfxFontCache::NotifyExpiredLocked(gfxFont* aFont,
const AutoLock& aLock) {
MOZ_ASSERT(aFont->GetRefCount() == 0);
RemoveObjectLocked(aFont, aLock);
mTrackerDiscard.AppendElement(aFont);
Key key(aFont->GetFontEntry(), aFont->GetStyle(),
aFont->GetUnicodeRangeMap());
HashEntry* entry = mFonts.GetEntry(key);
if (!entry || entry->mFont != aFont) {
MOZ_ASSERT_UNREACHABLE(
"Invalid font?");
return;
}
mFonts.RemoveEntry(entry);
}
void gfxFontCache::NotifyHandlerEnd() {
nsTArray<gfxFont*> discard;
{
MutexAutoLock lock(mMutex);
discard = std::move(mTrackerDiscard);
}
DestroyDiscard(discard);
}
void gfxFontCache::DestroyDiscard(nsTArray<gfxFont*>& aDiscard) {
for (
auto& font : aDiscard) {
NS_ASSERTION(font->GetRefCount() == 0,
"Destroying with refs outside cache!");
font->ClearCachedWords();
font->Destroy();
}
aDiscard.Clear();
}
void gfxFontCache::Flush() {
nsTArray<gfxFont*> discard;
{
MutexAutoLock lock(mMutex);
discard.SetCapacity(mFonts.Count());
for (
auto iter = mFonts.Iter(); !iter.Done(); iter.Next()) {
HashEntry* entry =
static_cast<HashEntry*>(iter.Get());
if (!entry || !entry->mFont) {
MOZ_ASSERT_UNREACHABLE(
"Invalid font?");
continue;
}
if (entry->mFont->GetRefCount() == 0) {
// If we are not tracked, then we must have won the race with
// gfxFont::MaybeDestroy and it is waiting on the mutex. To avoid a
// double free, we let gfxFont::MaybeDestroy handle the freeing when it
// acquires the mutex and discovers there is no matching entry in the
// hashtable.
if (entry->mFont->GetExpirationState()->IsTracked()) {
RemoveObjectLocked(entry->mFont, lock);
discard.AppendElement(entry->mFont);
}
}
else {
MOZ_ASSERT(!entry->mFont->GetExpirationState()->IsTracked());
}
}
MOZ_ASSERT(IsEmptyLocked(lock),
"Cache tracker still has fonts after flush!");
mFonts.Clear();
}
DestroyDiscard(discard);
}
/*static*/
void gfxFontCache::WordCacheExpirationTimerCallback(nsITimer* aTimer,
void* aCache) {
gfxFontCache* cache =
static_cast<gfxFontCache*>(aCache);
cache->AgeCachedWords();
}
void gfxFontCache::AgeCachedWords() {
bool allEmpty =
true;
{
MutexAutoLock lock(mMutex);
for (
const auto& entry : mFonts) {
allEmpty = entry.mFont->AgeCachedWords() && allEmpty;
}
}
if (allEmpty) {
PauseWordCacheExpirationTimer();
}
}
void gfxFontCache::FlushShapedWordCaches() {
{
MutexAutoLock lock(mMutex);
for (
const auto& entry : mFonts) {
entry.mFont->ClearCachedWords();
}
}
PauseWordCacheExpirationTimer();
}
void gfxFontCache::NotifyGlyphsChanged() {
MutexAutoLock lock(mMutex);
for (
const auto& entry : mFonts) {
entry.mFont->NotifyGlyphsChanged();
}
}
void gfxFontCache::AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf,
FontCacheSizes* aSizes)
const {
// TODO: add the overhead of the expiration tracker (generation arrays)
MutexAutoLock lock(*
const_cast<Mutex*>(&mMutex));
aSizes->mFontInstances += mFonts.ShallowSizeOfExcludingThis(aMallocSizeOf);
for (
const auto& entry : mFonts) {
entry.mFont->AddSizeOfExcludingThis(aMallocSizeOf, aSizes);
}
}
void gfxFontCache::AddSizeOfIncludingThis(MallocSizeOf aMallocSizeOf,
FontCacheSizes* aSizes)
const {
aSizes->mFontInstances += aMallocSizeOf(
this);
AddSizeOfExcludingThis(aMallocSizeOf, aSizes);
}
#define MAX_SSXX_VALUE 99
#define MAX_CVXX_VALUE 99
static void LookupAlternateValues(
const gfxFontFeatureValueSet& aFeatureLookup,
const nsACString& aFamily,
const StyleVariantAlternates& aAlternates,
nsTArray<gfxFontFeature>& aFontFeatures) {
using Tag = StyleVariantAlternates::Tag;
// historical-forms gets handled in nsFont::AddFontFeaturesToStyle.
if (aAlternates.IsHistoricalForms()) {
return;
}
gfxFontFeature feature;
if (aAlternates.IsCharacterVariant()) {
for (
auto& ident : aAlternates.AsCharacterVariant().AsSpan()) {
Span<
const uint32_t> values = aFeatureLookup.GetFontFeatureValuesFor(
aFamily, NS_FONT_VARIANT_ALTERNATES_CHARACTER_VARIANT,
ident.AsAtom());
// nothing defined, skip
if (values.IsEmpty()) {
continue;
}
NS_ASSERTION(values.Length() <= 2,
"too many values allowed for character-variant");
// character-variant(12 3) ==> 'cv12' = 3
uint32_t nn = values[0];
// ignore values greater than 99
if (nn == 0 || nn > MAX_CVXX_VALUE) {
continue;
}
feature.mValue = values.Length() > 1 ? values[1] : 1;
feature.mTag = HB_TAG(
'c',
'v', (
'0' + nn / 10), (
'0' + nn % 10));
aFontFeatures.AppendElement(feature);
}
return;
}
if (aAlternates.IsStyleset()) {
for (
auto& ident : aAlternates.AsStyleset().AsSpan()) {
Span<
const uint32_t> values = aFeatureLookup.GetFontFeatureValuesFor(
aFamily, NS_FONT_VARIANT_ALTERNATES_STYLESET, ident.AsAtom());
// styleset(1 2 7) ==> 'ss01' = 1, 'ss02' = 1, 'ss07' = 1
feature.mValue = 1;
for (uint32_t nn : values) {
if (nn == 0 || nn > MAX_SSXX_VALUE) {
continue;
}
feature.mTag = HB_TAG(
's',
's', (
'0' + nn / 10), (
'0' + nn % 10));
aFontFeatures.AppendElement(feature);
}
}
return;
}
uint32_t constant = 0;
nsAtom* name = nullptr;
switch (aAlternates.tag) {
case Tag::Swash:
constant = NS_FONT_VARIANT_ALTERNATES_SWASH;
name = aAlternates.AsSwash().AsAtom();
break;
case Tag::Stylistic:
constant = NS_FONT_VARIANT_ALTERNATES_STYLISTIC;
name = aAlternates.AsStylistic().AsAtom();
break;
case Tag::Ornaments:
constant = NS_FONT_VARIANT_ALTERNATES_ORNAMENTS;
name = aAlternates.AsOrnaments().AsAtom();
break;
case Tag::Annotation:
constant = NS_FONT_VARIANT_ALTERNATES_ANNOTATION;
name = aAlternates.AsAnnotation().AsAtom();
break;
default:
MOZ_ASSERT_UNREACHABLE(
"Unknown font-variant-alternates value!");
return;
}
Span<
const uint32_t> values =
aFeatureLookup.GetFontFeatureValuesFor(aFamily, constant, name);
if (values.IsEmpty()) {
return;
}
MOZ_ASSERT(values.Length() == 1,
"too many values for font-specific font-variant-alternates");
feature.mValue = values[0];
switch (aAlternates.tag) {
case Tag::Swash:
// swsh, cswh
feature.mTag = HB_TAG(
's',
'w',
's',
'h');
aFontFeatures.AppendElement(feature);
feature.mTag = HB_TAG(
'c',
's',
'w',
'h');
break;
case Tag::Stylistic:
// salt
feature.mTag = HB_TAG(
's',
'a',
'l',
't');
break;
case Tag::Ornaments:
// ornm
feature.mTag = HB_TAG(
'o',
'r',
'n',
'm');
break;
case Tag::Annotation:
// nalt
feature.mTag = HB_TAG(
'n',
'a',
'l',
't');
break;
default:
MOZ_ASSERT_UNREACHABLE(
"how?");
return;
}
aFontFeatures.AppendElement(feature);
}
/* static */
void gfxFontShaper::MergeFontFeatures(
const gfxFontStyle* aStyle,
const nsTArray<gfxFontFeature>& aFontFeatures,
bool aDisableLigatures,
const nsACString& aFamilyName,
bool aAddSmallCaps,
void (*aHandleFeature)(uint32_t, uint32_t,
void*),
void* aHandleFeatureData) {
const nsTArray<gfxFontFeature>& styleRuleFeatures = aStyle->featureSettings;
// Bail immediately if nothing to do, which is the common case.
if (styleRuleFeatures.IsEmpty() && aFontFeatures.IsEmpty() &&
!aDisableLigatures &&
aStyle->variantCaps == NS_FONT_VARIANT_CAPS_NORMAL &&
aStyle->variantSubSuper == NS_FONT_VARIANT_POSITION_NORMAL &&
aStyle->variantAlternates.IsEmpty()) {
return;
}
AutoTArray<gfxFontFeature, 32> mergedFeatures;
struct FeatureTagCmp {
bool Equals(
const gfxFontFeature& a,
const gfxFontFeature& b)
const {
return a.mTag == b.mTag;
}
bool LessThan(
const gfxFontFeature& a,
const gfxFontFeature& b)
const {
return a.mTag < b.mTag;
}
} cmp;
auto addOrReplace = [&](
const gfxFontFeature& aFeature) {
auto index = mergedFeatures.BinaryIndexOf(aFeature, cmp);
if (index == nsTArray<gfxFontFeature>::NoIndex) {
mergedFeatures.InsertElementSorted(aFeature, cmp);
}
else {
mergedFeatures[index].mValue = aFeature.mValue;
}
};
// add feature values from font
for (
const gfxFontFeature& feature : aFontFeatures) {
addOrReplace(feature);
}
// 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:
addOrReplace(gfxFontFeature{HB_TAG(
'c',
'2',
's',
'c'), 1});
// fall through to the small-caps case
[[fallthrough]];
case NS_FONT_VARIANT_CAPS_SMALLCAPS:
addOrReplace(gfxFontFeature{HB_TAG(
's',
'm',
'c',
'p'), 1});
break;
case NS_FONT_VARIANT_CAPS_ALLPETITE:
addOrReplace(gfxFontFeature{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:
addOrReplace(gfxFontFeature{aAddSmallCaps ? HB_TAG(
's',
'm',
'c',
'p')
: HB_TAG(
'p',
'c',
'a',
'p'),
1});
break;
case NS_FONT_VARIANT_CAPS_TITLING:
addOrReplace(gfxFontFeature{HB_TAG(
't',
'i',
't',
'l'), 1});
break;
case NS_FONT_VARIANT_CAPS_UNICASE:
addOrReplace(gfxFontFeature{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:
addOrReplace(gfxFontFeature{HB_TAG(
's',
'u',
'p',
's'), 1});
break;
case NS_FONT_VARIANT_POSITION_SUB:
addOrReplace(gfxFontFeature{HB_TAG(
's',
'u',
'b',
's'), 1});
break;
default:
MOZ_ASSERT_UNREACHABLE(
"Unexpected variantSubSuper");
break;
}
// add font-specific feature values from style rules
if (aStyle->featureValueLookup && !aStyle->variantAlternates.IsEmpty()) {
AutoTArray<gfxFontFeature, 4> featureList;
// insert list of alternate feature settings
for (
auto& alternate : aStyle->variantAlternates.AsSpan()) {
LookupAlternateValues(*aStyle->featureValueLookup, aFamilyName, alternate,
featureList);
}
for (
const gfxFontFeature& feature : featureList) {
addOrReplace(gfxFontFeature{feature.mTag, feature.mValue});
}
}
auto disableOptionalLigatures = [&]() ->
void {
addOrReplace(gfxFontFeature{HB_TAG(
'l',
'i',
'g',
'a'), 0});
addOrReplace(gfxFontFeature{HB_TAG(
'c',
'l',
'i',
'g'), 0});
addOrReplace(gfxFontFeature{HB_TAG(
'd',
'l',
'i',
'g'), 0});
addOrReplace(gfxFontFeature{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) {
addOrReplace(gfxFontFeature{feature.mTag, feature.mValue});
}
else if (aDisableLigatures) {
// Handle ligature-disabling setting at the boundary between high-
// and low-level features.
disableOptionalLigatures();
}
}
}
for (
const auto& f : mergedFeatures) {
aHandleFeature(f.mTag, f.mValue, aHandleFeatureData);
}
}
void gfxShapedText::SetupClusterBoundaries(uint32_t aOffset,
const char16_t* aString,
uint32_t aLength) {
if (aLength == 0) {
return;
}
CompressedGlyph*
const glyphs = GetCharacterGlyphs() + aOffset;
CompressedGlyph extendCluster = CompressedGlyph::MakeComplex(
false,
true);
// GraphemeClusterBreakIteratorUtf16 won't be able to tell us if the string
// _begins_ with a cluster-extender, so we handle that here
uint32_t ch = aString[0];
if (aLength > 1 && NS_IS_SURROGATE_PAIR(ch, aString[1])) {
ch = SURROGATE_TO_UCS4(ch, aString[1]);
}
if (IsClusterExtender(ch)) {
glyphs[0] = extendCluster;
}
intl::GraphemeClusterBreakIteratorUtf16 iter(
Span<
const char16_t>(aString, aLength));
uint32_t pos = 0;
const char16_t kIdeographicSpace = 0x3000;
// Special case for Bengali: although Virama normally clusters with the
// preceding letter, we *also* want to cluster it with a following Ya
// so that when the Virama+Ya form ya-phala, this is not separated from the
// preceding letter by any letter-spacing or justification.
const char16_t kBengaliVirama = 0x09CD;
const char16_t kBengaliYa = 0x09AF;
bool prevWasHyphen =
false;
while (pos < aLength) {
const char16_t ch = aString[pos];
if (prevWasHyphen) {
if (nsContentUtils::IsAlphanumeric(ch)) {
glyphs[pos].SetCanBreakBefore(
CompressedGlyph::FLAG_BREAK_TYPE_EMERGENCY_WRAP);
}
prevWasHyphen =
false;
}
if (ch == char16_t(
' ') || ch == kIdeographicSpace) {
glyphs[pos].SetIsSpace();
}
else if (nsContentUtils::IsHyphen(ch) && pos &&
nsContentUtils::IsAlphanumeric(aString[pos - 1])) {
prevWasHyphen =
true;
}
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;
uint32_t pos = 0;
bool prevWasHyphen =
false;
while (pos < aLength) {
uint8_t ch = aString[pos];
if (prevWasHyphen) {
if (nsContentUtils::IsAlphanumeric(ch)) {
glyphs->SetCanBreakBefore(
CompressedGlyph::FLAG_BREAK_TYPE_EMERGENCY_WRAP);
}
prevWasHyphen =
false;
}
if (ch == uint8_t(
' ')) {
glyphs->SetIsSpace();
}
else if (ch == uint8_t(
'-') && pos &&
nsContentUtils::IsAlphanumeric(aString[pos - 1])) {
prevWasHyphen =
true;
}
++pos;
++glyphs;
}
}
gfxShapedText::DetailedGlyph* gfxShapedText::AllocateDetailedGlyphs(
uint32_t aIndex, uint32_t aCount) {
NS_ASSERTION(aIndex < GetLength(),
"Index out of range");
if (!mDetailedGlyphs) {
mDetailedGlyphs = MakeUnique<DetailedGlyphStore>();
}
return mDetailedGlyphs->Allocate(aIndex, aCount);
}
void gfxShapedText::SetDetailedGlyphs(uint32_t aIndex, uint32_t aGlyphCount,
const DetailedGlyph* aGlyphs) {
CompressedGlyph& g = GetCharacterGlyphs()[aIndex];
MOZ_ASSERT(aIndex > 0 || g.IsLigatureGroupStart(),
"First character can't be a ligature continuation!");
if (aGlyphCount > 0) {
DetailedGlyph* details = AllocateDetailedGlyphs(aIndex, aGlyphCount);
memcpy(details, aGlyphs,
sizeof(DetailedGlyph) * aGlyphCount);
}
g.SetGlyphCount(aGlyphCount);
}
#define ZWNJ 0x200C
#define ZWJ 0x200D
static inline bool IsIgnorable(uint32_t aChar) {
return (IsDefaultIgnorable(aChar)) || aChar == ZWNJ || aChar == ZWJ;
}
void gfxShapedText::SetMissingGlyph(uint32_t aIndex, uint32_t aChar,
gfxFont* aFont) {
CompressedGlyph& g = GetCharacterGlyphs()[aIndex];
uint8_t category = GetGeneralCategory(aChar);
if (category >= HB_UNICODE_GENERAL_CATEGORY_SPACING_MARK &&
category <= HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK) {
g.SetComplex(
false,
true);
}
// Leaving advance as zero will prevent drawing the hexbox for ignorables.
int32_t advance = 0;
if (!IsIgnorable(aChar)) {
gfxFloat width =
std::max(aFont->GetMetrics(nsFontMetrics::eHorizontal).aveCharWidth,
gfxFloat(gfxFontMissingGlyphs::GetDesiredMinWidth(
aChar, mAppUnitsPerDevUnit)));
advance = int32_t(width * mAppUnitsPerDevUnit);
}
DetailedGlyph detail = {aChar, advance, gfx::Point()};
SetDetailedGlyphs(aIndex, 1, &detail);
g.SetMissing();
}
bool gfxShapedText::FilterIfIgnorable(uint32_t aIndex, uint32_t aCh) {
if (IsIgnorable(aCh)) {
// There are a few default-ignorables of Letter category (currently,
// just the Hangul filler characters) that we'd better not discard
// if they're followed by additional characters in the same cluster.
// Some fonts use them to carry the width of a whole cluster of
// combining jamos; see bug 1238243.
auto* charGlyphs = GetCharacterGlyphs();
if (GetGenCategory(aCh) == nsUGenCategory::kLetter &&
aIndex + 1 < GetLength() && !charGlyphs[aIndex + 1].IsClusterStart()) {
return false;
}
// A compressedGlyph that is set to MISSING but has no DetailedGlyphs list
// will be zero-width/invisible, which is what we want here.
CompressedGlyph& g = charGlyphs[aIndex];
g.SetComplex(g.IsClusterStart(), g.IsLigatureGroupStart()).SetMissing();
return true;
}
return false;
}
void gfxShapedText::ApplyTrackingToClusters(gfxFloat aTrackingAdjustment,
uint32_t aOffset,
uint32_t aLength) {
int32_t appUnitAdjustment =
NS_round(aTrackingAdjustment * gfxFloat(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 = std::max(0, advance + appUnitAdjustment);
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;
}
auto& advance = IsRightToLeft() ? details[0].mAdvance
: details[detailedLength - 1].mAdvance;
if (advance > 0) {
advance = std::max(0, advance + appUnitAdjustment);
}
}
}
}
}
size_t gfxShapedWord::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf)
const {
size_t total = aMallocSizeOf(
this);
if (mDetailedGlyphs) {
total += mDetailedGlyphs->SizeOfIncludingThis(aMallocSizeOf);
}
return total;
}
float gfxFont::AngleForSyntheticOblique()
const {
// First check conditions that mean no synthetic slant should be used:
if (mStyle.style == FontSlantStyle::NORMAL) {
return 0.0f;
// Requested style is 'normal'.
}
if (!mStyle.allowSyntheticStyle) {
return 0.0f;
// Synthetic obliquing is disabled.
}
if (!mFontEntry->MayUseSyntheticSlant()) {
return 0.0f;
// The resource supports "real" slant, so don't synthesize.
}
// If style calls for italic, and face doesn't support it, use default
// oblique angle as a simulation.
if (mStyle.style.IsItalic()) {
return mFontEntry->SupportsItalic()
? 0.0f
: FontSlantStyle::DEFAULT_OBLIQUE_DEGREES;
}
// OK, we're going to use synthetic oblique: return the requested angle.
return mStyle.style.ObliqueAngle();
}
float gfxFont::SkewForSyntheticOblique()
const {
// Precomputed value of tan(kDefaultAngle), the default italic/oblique slant;
// avoids calling tan() at runtime except for custom oblique values.
static const float kTanDefaultAngle =
tan(FontSlantStyle::DEFAULT_OBLIQUE_DEGREES * (M_PI / 180.0));
float angle = AngleForSyntheticOblique();
if (angle == 0.0f) {
return 0.0f;
}
else if (angle == FontSlantStyle::DEFAULT_OBLIQUE_DEGREES) {
return kTanDefaultAngle;
}
else {
return tan(angle * (M_PI / 180.0));
}
}
void gfxFont::RunMetrics::CombineWith(
const RunMetrics& aOther,
bool aOtherIsOnLeft) {
mAscent = std::max(mAscent, aOther.mAscent);
mDescent = std::max(mDescent, aOther.mDescent);
if (aOtherIsOnLeft) {
mBoundingBox = (mBoundingBox + gfxPoint(aOther.mAdvanceWidth, 0))
.
Union(aOther.mBoundingBox);
}
else {
mBoundingBox =
mBoundingBox.
Union(aOther.mBoundingBox + gfxPoint(mAdvanceWidth, 0));
}
mAdvanceWidth += aOther.mAdvanceWidth;
}
gfxFont::gfxFont(
const RefPtr<UnscaledFont>& aUnscaledFont,
gfxFontEntry* aFontEntry,
const gfxFontStyle* aFontStyle,
AntialiasOption anAAOption)
: mFontEntry(aFontEntry),
mLock(
"gfxFont lock"),
mUnscaledFont(aUnscaledFont),
mStyle(*aFontStyle),
mAdjustedSize(-1.0),
// negative to indicate "not yet initialized"
mFUnitsConvFactor(-1.0f),
// negative to indicate "not yet initialized"
mAntialiasOption(anAAOption),
mIsValid(
true),
mApplySyntheticBold(
false),
mKerningEnabled(
false),
mMathInitialized(
false) {
#ifdef DEBUG_TEXT_RUN_STORAGE_METRICS
++gFontCount;
#endif
if (MOZ_UNLIKELY(StaticPrefs::gfx_text_disable_aa_AtStartup())) {
mAntialiasOption = kAntialiasNone;
}
// Turn off AA for Ahem for testing purposes when requested.
if (MOZ_UNLIKELY(StaticPrefs::gfx_font_rendering_ahem_antialias_none() &&
mFontEntry->FamilyName().EqualsLiteral(
"Ahem"))) {
mAntialiasOption = kAntialiasNone;
}
mKerningSet = HasFeatureSet(HB_TAG(
'k',
'e',
'r',
'n'), mKerningEnabled);
// Ensure the gfxFontEntry's unitsPerEm and extents fields are initialized,
// so that GetFontExtents can use them without risk of races.
Unused << mFontEntry->UnitsPerEm();
}
gfxFont::~gfxFont() {
mFontEntry->NotifyFontDestroyed(
this);
// Delete objects owned through atomic pointers. (Some of these may be null,
// but that's OK.)
delete mVerticalMetrics.exchange(nullptr);
delete mHarfBuzzShaper.exchange(nullptr);
delete mGraphiteShaper.exchange(nullptr);
delete mMathTable.exchange(nullptr);
delete mNonAAFont.exchange(nullptr);
if (
auto* scaledFont = mAzureScaledFont.exchange(nullptr)) {
scaledFont->Release();
}
if (mGlyphChangeObservers) {
for (
const auto& key : *mGlyphChangeObservers) {
key->ForgetFont();
}
}
}
// Work out whether cairo will snap inter-glyph spacing to pixels.
//
// Layout does not align text to pixel boundaries, so, with font drawing
// backends that snap glyph positions to pixels, it is important that
// inter-glyph spacing within words is always an integer number of pixels.
// This ensures that the drawing backend snaps all of the word's glyphs in the
// same direction and so inter-glyph spacing remains the same.
//
gfxFont::RoundingFlags gfxFont::GetRoundOffsetsToPixels(
DrawTarget* aDrawTarget) {
// Could do something fancy here for ScaleFactors of
// AxisAlignedTransforms, but we leave things simple.
// Not much point rounding if a matrix will mess things up anyway.
// Also check if the font already knows hint metrics is off...
if (aDrawTarget->GetTransform().HasNonTranslation() || !ShouldHintMetrics()) {
return RoundingFlags(0);
}
cairo_t* cr =
static_cast<cairo_t*>(
aDrawTarget->GetNativeSurface(NativeSurfaceType::CAIRO_CONTEXT));
if (cr) {
cairo_surface_t* target = cairo_get_target(cr);
// Check whether the cairo surface's font options hint metrics.
cairo_font_options_t* fontOptions = cairo_font_options_create();
cairo_surface_get_font_options(target, fontOptions);
cairo_hint_metrics_t hintMetrics =
cairo_font_options_get_hint_metrics(fontOptions);
cairo_font_options_destroy(fontOptions);
switch (hintMetrics) {
case CAIRO_HINT_METRICS_OFF:
return RoundingFlags(0);
case CAIRO_HINT_METRICS_ON:
return RoundingFlags::kRoundX | RoundingFlags::kRoundY;
default:
break;
}
}
if (ShouldRoundXOffset(cr)) {
return RoundingFlags::kRoundX | RoundingFlags::kRoundY;
}
else {
return RoundingFlags::kRoundY;
}
}
gfxHarfBuzzShaper* gfxFont::GetHarfBuzzShaper() {
if (!mHarfBuzzShaper) {
auto* shaper =
new gfxHarfBuzzShaper(
this);
shaper->Initialize();
if (!mHarfBuzzShaper.compareExchange(nullptr, shaper)) {
delete shaper;
}
}
gfxHarfBuzzShaper* shaper = mHarfBuzzShaper;
return shaper->IsInitialized() ? shaper : nullptr;
}
gfxFloat gfxFont::GetGlyphAdvance(uint16_t aGID,
bool aVertical) {
if (!aVertical && ProvidesGlyphWidths()) {
return GetGlyphWidth(aGID) / 65536.0;
}
if (mFUnitsConvFactor < 0.0f) {
// Metrics haven't been initialized; lock while we do that.
AutoWriteLock lock(mLock);
if (mFUnitsConvFactor < 0.0f) {
GetMetrics(nsFontMetrics::eHorizontal);
}
}
NS_ASSERTION(mFUnitsConvFactor >= 0.0f,
"missing font unit conversion factor");
if (gfxHarfBuzzShaper* shaper = GetHarfBuzzShaper()) {
if (aVertical) {
// Note that GetGlyphVAdvance may return -1 to indicate it was unable
// to retrieve vertical metrics; in that case we fall back to the
// aveCharWidth value as a default advance.
int32_t advance = shaper->GetGlyphVAdvance(aGID);
if (advance < 0) {
return GetMetrics(nsFontMetrics::eVertical).aveCharWidth;
}
return advance / 65536.0;
}
return shaper->GetGlyphHAdvance(aGID) / 65536.0;
}
return 0.0;
}
gfxFloat gfxFont::GetCharAdvance(uint32_t aUnicode,
bool aVertical) {
uint32_t gid = 0;
if (ProvidesGetGlyph()) {
gid = GetGlyph(aUnicode, 0);
}
else {
if (gfxHarfBuzzShaper* shaper = GetHarfBuzzShaper()) {
gid = shaper->GetNominalGlyph(aUnicode);
}
}
if (!gid) {
return -1.0;
}
return GetGlyphAdvance(gid, aVertical);
}
static void CollectLookupsByFeature(hb_face_t* aFace, hb_tag_t aTableTag,
uint32_t aFeatureIndex,
hb_set_t* aLookups) {
uint32_t lookups[32];
uint32_t i, len, offset;
offset = 0;
do {
len = std::size(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 == std::size(lookups));
}
static void CollectLookupsByLanguage(
hb_face_t* aFace, hb_tag_t aTableTag,
const nsTHashSet<uint32_t>& aSpecificFeatures, hb_set_t* aOtherLookups,
hb_set_t* aSpecificFeatureLookups, uint32_t aScriptIndex,
uint32_t aLangIndex) {
uint32_t reqFeatureIndex;
if (hb_ot_layout_language_get_required_feature_index(
aFace, aTableTag, aScriptIndex, aLangIndex, &reqFeatureIndex)) {
CollectLookupsByFeature(aFace, aTableTag, reqFeatureIndex, aOtherLookups);
}
uint32_t featureIndexes[32];
uint32_t i, len, offset;
offset = 0;
do {
len = std::size(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 == std::size(featureIndexes));
}
static bool HasLookupRuleWithGlyphByScript(
hb_face_t* aFace, hb_tag_t aTableTag, hb_tag_t aScriptTag,
uint32_t aScriptIndex, uint16_t aGlyph,
const nsTHashSet<uint32_t>& aDefaultFeatures,
bool& aHasDefaultFeatureWithGlyph) {
uint32_t numLangs, lang;
hb_set_t* defaultFeatureLookups = hb_set_create();
hb_set_t* nonDefaultFeatureLookups = hb_set_create();
// default lang
CollectLookupsByLanguage(aFace, aTableTag, aDefaultFeatures,
nonDefaultFeatureLookups, defaultFeatureLookups,
aScriptIndex, HB_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX);
// iterate over langs
numLangs = hb_ot_layout_script_get_language_tags(
aFace, aTableTag, aScriptIndex, 0, nullptr, nullptr);
for (lang = 0; lang < numLangs; lang++) {
CollectLookupsByLanguage(aFace, aTableTag, aDefaultFeatures,
nonDefaultFeatureLookups, defaultFeatureLookups,
aScriptIndex, lang);
}
// look for the glyph among default feature lookups
aHasDefaultFeatureWithGlyph =
false;
hb_set_t* glyphs = hb_set_create();
hb_codepoint_t index = -1;
while (hb_set_next(defaultFeatureLookups, &index)) {
hb_ot_layout_lookup_collect_glyphs(aFace, aTableTag, index, glyphs, glyphs,
glyphs, nullptr);
if (hb_set_has(glyphs, aGlyph)) {
aHasDefaultFeatureWithGlyph =
true;
break;
}
}
// look for the glyph among non-default feature lookups
// if no default feature lookups contained spaces
bool hasNonDefaultFeatureWithGlyph =
false;
if (!aHasDefaultFeatureWithGlyph) {
hb_set_clear(glyphs);
index = -1;
while (hb_set_next(nonDefaultFeatureLookups, &index)) {
hb_ot_layout_lookup_collect_glyphs(aFace, aTableTag, index, glyphs,
glyphs, glyphs, nullptr);
if (hb_set_has(glyphs, aGlyph)) {
hasNonDefaultFeatureWithGlyph =
true;
break;
}
}
}
hb_set_destroy(glyphs);
hb_set_destroy(defaultFeatureLookups);
hb_set_destroy(nonDefaultFeatureLookups);
return aHasDefaultFeatureWithGlyph || hasNonDefaultFeatureWithGlyph;
}
static void HasLookupRuleWithGlyph(hb_face_t* aFace, hb_tag_t aTableTag,
bool& aHasGlyph, hb_tag_t aSpecificFeature,
bool& aHasGlyphSpecific, uint16_t aGlyph) {
// iterate over the scripts in the font
uint32_t numScripts, numLangs, script, lang;
hb_set_t* otherLookups = hb_set_create();
hb_set_t* specificFeatureLookups = hb_set_create();
nsTHashSet<uint32_t> specificFeature(1);
specificFeature.Insert(aSpecificFeature);
numScripts =
hb_ot_layout_table_get_script_tags(aFace, aTableTag, 0, nullptr, nullptr);
for (script = 0; script < numScripts; script++) {
// default lang
CollectLookupsByLanguage(aFace, aTableTag, specificFeature, otherLookups,
specificFeatureLookups, script,
HB_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX);
// iterate over langs
numLangs = hb_ot_layout_script_get_language_tags(
aFace, HB_OT_TAG_GPOS, script, 0, nullptr, nullptr);
for (lang = 0; lang < numLangs; lang++) {
CollectLookupsByLanguage(aFace, aTableTag, specificFeature, otherLookups,
specificFeatureLookups, script, lang);
}
}
// look for the glyph among non-specific feature lookups
hb_set_t* glyphs = hb_set_create();
hb_codepoint_t index = -1;
while (hb_set_next(otherLookups, &index)) {
hb_ot_layout_lookup_collect_glyphs(aFace, aTableTag, index, glyphs, glyphs,
glyphs, nullptr);
if (hb_set_has(glyphs, aGlyph)) {
aHasGlyph =
true;
break;
}
}
// look for the glyph among specific feature lookups
hb_set_clear(glyphs);
index = -1;
while (hb_set_next(specificFeatureLookups, &index)) {
hb_ot_layout_lookup_collect_glyphs(aFace, aTableTag, index, glyphs, glyphs,
glyphs, nullptr);
if (hb_set_has(glyphs, aGlyph)) {
aHasGlyphSpecific =
true;
break;
}
}
hb_set_destroy(glyphs);
hb_set_destroy(specificFeatureLookups);
hb_set_destroy(otherLookups);
}
Atomic<nsTHashMap<nsUint32HashKey, intl::Script>*> gfxFont::sScriptTagToCode;
Atomic<nsTHashSet<uint32_t>*> gfxFont::sDefaultFeatures;
static inline bool HasSubstitution(uint32_t* aBitVector, intl::Script aScript) {
return (aBitVector[
static_cast<uint32_t>(aScript) >> 5] &
(1 << (
static_cast<uint32_t>(aScript) & 0x1f))) != 0;
}
// union of all default substitution features across scripts
static const hb_tag_t defaultFeatures[] = {
HB_TAG(
'a',
'b',
'v',
'f'), HB_TAG(
'a',
'b',
'v',
's'),
HB_TAG(
'a',
'k',
'h',
'n'), HB_TAG(
'b',
'l',
'w',
'f'),
HB_TAG(
'b',
'l',
'w',
's'), HB_TAG(
'c',
'a',
'l',
't'),
HB_TAG(
'c',
'c',
'm',
'p'), HB_TAG(
'c',
'f',
'a',
'r'),
HB_TAG(
'c',
'j',
'c',
't'), HB_TAG(
'c',
'l',
'i',
'g'),
HB_TAG(
'f',
'i',
'n',
'2'), HB_TAG(
'f',
'i',
'n',
'3'),
HB_TAG(
'f',
'i',
'n',
'a'), HB_TAG(
'h',
'a',
'l',
'f'),
HB_TAG(
'h',
'a',
'l',
'n'), HB_TAG(
'i',
'n',
'i',
't'),
HB_TAG(
'i',
's',
'o',
'l'), HB_TAG(
'l',
'i',
'g',
'a'),
HB_TAG(
'l',
'j',
'm',
'o'), HB_TAG(
'l',
'o',
'c',
'l'),
HB_TAG(
'l',
't',
'r',
'a'), HB_TAG(
'l',
't',
'r',
'm'),
HB_TAG(
'm',
'e',
'd',
'2'), HB_TAG(
'm',
'e',
'd',
'i'),
HB_TAG(
'm',
's',
'e',
't'), HB_TAG(
'n',
'u',
'k',
't'),
HB_TAG(
'p',
'r',
'e',
'f'), HB_TAG(
'p',
'r',
'e',
's'),
HB_TAG(
'p',
's',
't',
'f'), HB_TAG(
'p',
's',
't',
's'),
HB_TAG(
'r',
'c',
'l',
't'), HB_TAG(
'r',
'l',
'i',
'g'),
HB_TAG(
'r',
'k',
'r',
'f'), HB_TAG(
'r',
'p',
'h',
'f'),
HB_TAG(
'r',
't',
'l',
'a'), HB_TAG(
'r',
't',
'l',
'm'),
HB_TAG(
't',
'j',
'm',
'o'), HB_TAG(
'v',
'a',
't',
'u'),
HB_TAG(
'v',
'e',
'r',
't'), HB_TAG(
'v',
'j',
'm',
'o')};
void gfxFont::CheckForFeaturesInvolvingSpace()
const {
gfxFontEntry::SpaceFeatures flags = gfxFontEntry::SpaceFeatures::None;
auto setFlags =
MakeScopeExit([&]() { mFontEntry->mHasSpaceFeatures = flags; });
bool log = LOG_FONTINIT_ENABLED();
TimeStamp start;
if (MOZ_UNLIKELY(log)) {
start = TimeStamp::Now();
}
uint32_t spaceGlyph = GetSpaceGlyph();
if (!spaceGlyph) {
return;
}
auto face(GetFontEntry()->GetHBFace());
// GSUB lookups - examine per script
if (hb_ot_layout_has_substitution(face)) {
// Get the script ==> code hashtable, creating it on first use.
nsTHashMap<nsUint32HashKey, Script>* tagToCode = sScriptTagToCode;
if (!tagToCode) {
tagToCode =
new nsTHashMap<nsUint32HashKey, Script>(
size_t(Script::NUM_SCRIPT_CODES));
tagToCode->InsertOrUpdate(HB_TAG(
'D',
'F',
'L',
'T'), Script::COMMON);
// Ensure that we don't try to look at script codes beyond what the
// current version of ICU (at runtime -- in case of system ICU)
// knows about.
Script scriptCount = Script(
std::min<
int>(intl::UnicodeProperties::GetMaxNumberOfScripts() + 1,
int(Script::NUM_SCRIPT_CODES)));
for (Script s = Script::ARABIC; s < scriptCount;
s = Script(
static_cast<
int>(s) + 1)) {
hb_script_t script = hb_script_t(GetScriptTagForCode(s));
unsigned int scriptCount = 4;
hb_tag_t scriptTags[4];
hb_ot_tags_from_script_and_language(script, HB_LANGUAGE_INVALID,
&scriptCount, scriptTags, nullptr,
nullptr);
for (
unsigned int i = 0; i < scriptCount; i++) {
tagToCode->InsertOrUpdate(scriptTags[i], s);
}
}
if (!sScriptTagToCode.compareExchange(nullptr, tagToCode)) {
// We lost a race! Discard our new table and use the winner.
delete tagToCode;
tagToCode = sScriptTagToCode;
}
}
// Set up the default-features hashset on first use.
if (!sDefaultFeatures) {
uint32_t numDefaultFeatures = std::size(defaultFeatures);
auto* set =
new nsTHashSet<uint32_t>(numDefaultFeatures);
for (uint32_t i = 0; i < numDefaultFeatures; i++) {
set->Insert(defaultFeatures[i]);
}
if (!sDefaultFeatures.compareExchange(nullptr, set)) {
delete set;
}
}
// iterate over the scripts in the font
hb_tag_t scriptTags[8];
uint32_t len, offset = 0;
do {
len = std::size(scriptTags);
hb_ot_layout_table_get_script_tags(face, HB_OT_TAG_GSUB, offset, &len,
scriptTags);
for (uint32_t i = 0; i < len; i++) {
bool isDefaultFeature =
false;
Script s;
if (!HasLookupRuleWithGlyphByScript(
face, HB_OT_TAG_GSUB, scriptTags[i], offset + i, spaceGlyph,
*sDefaultFeatures, isDefaultFeature) ||
!tagToCode->Get(scriptTags[i], &s)) {
continue;
}
flags = flags | gfxFontEntry::SpaceFeatures::HasFeatures;
uint32_t index =
static_cast<uint32_t>(s) >> 5;
uint32_t bit =
static_cast<uint32_t>(s) & 0x1f;
if (isDefaultFeature) {
mFontEntry->mDefaultSubSpaceFeatures[index] |= (1 << bit);
}
else {
mFontEntry->mNonDefaultSubSpaceFeatures[index] |= (1 << bit);
}
}
offset += len;
}
while (len == std::size(scriptTags));
}
// spaces in default features of default script?
// ==> can't use word cache, skip GPOS analysis
bool canUseWordCache =
true;
if (HasSubstitution(mFontEntry->mDefaultSubSpaceFeatures, Script::COMMON)) {
canUseWordCache =
false;
}
// GPOS lookups - distinguish kerning from non-kerning features
if (canUseWordCache && hb_ot_layout_has_positioning(face)) {
bool hasKerning =
false, hasNonKerning =
false;
HasLookupRuleWithGlyph(face, HB_OT_TAG_GPOS, hasNonKerning,
HB_TAG(
'k',
'e',
'r',
'n'), hasKerning, spaceGlyph);
if (hasKerning) {
flags |= gfxFontEntry::SpaceFeatures::HasFeatures |
gfxFontEntry::SpaceFeatures::Kerning;
}
if (hasNonKerning) {
flags |= gfxFontEntry::SpaceFeatures::HasFeatures |
gfxFontEntry::SpaceFeatures::NonKerning;
}
}
if (MOZ_UNLIKELY(log)) {
TimeDuration elapsed = TimeStamp::Now() - start;
LOG_FONTINIT((
"(fontinit-spacelookups) font: %s - "
"subst default: %8.8x %8.8x %8.8x %8.8x "
"subst non-default: %8.8x %8.8x %8.8x %8.8x "
"kerning: %s non-kerning: %s time: %6.3f\n",
mFontEntry->Name().get(), mFontEntry->mDefaultSubSpaceFeatures[3],
mFontEntry->mDefaultSubSpaceFeatures[2],
mFontEntry->mDefaultSubSpaceFeatures[1],
mFontEntry->mDefaultSubSpaceFeatures[0],
mFontEntry->mNonDefaultSubSpaceFeatures[3],
mFontEntry->mNonDefaultSubSpaceFeatures[2],
mFontEntry->mNonDefaultSubSpaceFeatures[1],
mFontEntry->mNonDefaultSubSpaceFeatures[0],
(mFontEntry->mHasSpaceFeatures & gfxFontEntry::SpaceFeatures::Kerning
?
"true"
:
"false"),
(mFontEntry->mHasSpaceFeatures & gfxFontEntry::SpaceFeatures::NonKerning
?
"true"
:
"false"),
elapsed.ToMilliseconds()));
}
}
bool gfxFont::HasSubstitutionRulesWithSpaceLookups(Script aRunScript)
const {
NS_ASSERTION(GetFontEntry()->mHasSpaceFeatures !=
gfxFontEntry::SpaceFeatures::Uninitialized,
"need to initialize space lookup flags");
NS_ASSERTION(aRunScript < Script::NUM_SCRIPT_CODES,
"weird script code");
if (aRunScript == Script::INVALID || aRunScript >= Script::NUM_SCRIPT_CODES) {
return false;
}
// default features have space lookups ==> true
if (HasSubstitution(mFontEntry->mDefaultSubSpaceFeatures, Script::COMMON) ||
HasSubstitution(mFontEntry->mDefaultSubSpaceFeatures, aRunScript)) {
return true;
}
// non-default features have space lookups and some type of
// font feature, in font or style is specified ==> true
if ((HasSubstitution(mFontEntry->mNonDefaultSubSpaceFeatures,
Script::COMMON) ||
HasSubstitution(mFontEntry->mNonDefaultSubSpaceFeatures, aRunScript)) &&
(!mStyle.featureSettings.IsEmpty() ||
!mFontEntry->mFeatureSettings.IsEmpty())) {
return true;
}
return false;
}
tainted_boolean_hint gfxFont::SpaceMayParticipateInShaping(
Script aRunScript)
const {
// avoid checking fonts known not to include default space-dependent features
if (MOZ_UNLIKELY(mFontEntry->mSkipDefaultFeatureSpaceCheck)) {
if (!mKerningSet && mStyle.featureSettings.IsEmpty() &&
mFontEntry->mFeatureSettings.IsEmpty()) {
return false;
}
}
if (FontCanSupportGraphite()) {
if (gfxPlatform::GetPlatform()->UseGraphiteShaping()) {
return mFontEntry->HasGraphiteSpaceContextuals();
}
}
// We record the presence of space-dependent features in the font entry
// so that subsequent instantiations for the same font face won't
// require us to re-check the tables; however, the actual check is done
// by gfxFont because not all font entry subclasses know how to create
// a harfbuzz face for introspection.
gfxFontEntry::SpaceFeatures flags = mFontEntry->mHasSpaceFeatures;
if (flags == gfxFontEntry::SpaceFeatures::Uninitialized) {
CheckForFeaturesInvolvingSpace();
flags = mFontEntry->mHasSpaceFeatures;
}
if (!(flags & gfxFontEntry::SpaceFeatures::HasFeatures)) {
return false;
}
// if font has substitution rules or non-kerning positioning rules
// that involve spaces, bypass
if (HasSubstitutionRulesWithSpaceLookups(aRunScript) ||
(flags & gfxFontEntry::SpaceFeatures::NonKerning)) {
return true;
}
// if kerning explicitly enabled/disabled via font-feature-settings or
// font-kerning and kerning rules use spaces, only bypass when enabled
if (mKerningSet && (flags & gfxFontEntry::SpaceFeatures::Kerning)) {
return mKerningEnabled;
}
return false;
}
bool gfxFont::SupportsFeature(Script aScript, uint32_t aFeatureTag) {
if (mGraphiteShaper && gfxPlatform::GetPlatform()->UseGraphiteShaping()) {
return GetFontEntry()->SupportsGraphiteFeature(aFeatureTag);
}
return GetFontEntry()->SupportsOpenTypeFeature(aScript, aFeatureTag);
}
bool gfxFont::SupportsVariantCaps(Script aScript, uint32_t aVariantCaps,
bool& aFallbackToSmallCaps,
bool& aSyntheticLowerToSmallCaps,
bool& aSyntheticUpperToSmallCaps) {
bool ok =
true;
// cases without fallback are fine
aFallbackToSmallCaps =
false;
aSyntheticLowerToSmallCaps =
false;
aSyntheticUpperToSmallCaps =
false;
switch (aVariantCaps) {
case NS_FONT_VARIANT_CAPS_SMALLCAPS:
ok = SupportsFeature(aScript, HB_TAG(
's',
'm',
'c',
'p'));
if (!ok) {
aSyntheticLowerToSmallCaps =
true;
}
break;
case NS_FONT_VARIANT_CAPS_ALLSMALL:
ok = SupportsFeature(aScript, HB_TAG(
's',
'm',
'c',
'p')) &&
SupportsFeature(aScript, HB_TAG(
'c',
'2',
's',
'c'));
if (!ok) {
aSyntheticLowerToSmallCaps =
true;
aSyntheticUpperToSmallCaps =
true;
}
break;
case NS_FONT_VARIANT_CAPS_PETITECAPS:
ok = SupportsFeature(aScript, HB_TAG(
'p',
'c',
'a',
'p'));
if (!ok) {
ok = SupportsFeature(aScript, HB_TAG(
's',
'm',
'c',
'p'));
aFallbackToSmallCaps = ok;
}
if (!ok) {
aSyntheticLowerToSmallCaps =
true;
}
break;
case NS_FONT_VARIANT_CAPS_ALLPETITE:
ok = SupportsFeature(aScript, HB_TAG(
'p',
'c',
'a',
'p')) &&
SupportsFeature(aScript, HB_TAG(
'c',
'2',
'p',
'c'));
if (!ok) {
ok = SupportsFeature(aScript, HB_TAG(
's',
'm',
'c',
'p')) &&
SupportsFeature(aScript, HB_TAG(
'c',
'2',
's',
'c'));
aFallbackToSmallCaps = ok;
}
if (!ok) {
aSyntheticLowerToSmallCaps =
true;
aSyntheticUpperToSmallCaps =
true;
}
break;
default:
break;
}
NS_ASSERTION(
!(ok && (aSyntheticLowerToSmallCaps || aSyntheticUpperToSmallCaps)),
"shouldn't use synthetic features if we found real ones");
NS_ASSERTION(!(!ok && aFallbackToSmallCaps),
"if we found a usable fallback, that counts as ok");
return ok;
}
bool gfxFont::SupportsSubSuperscript(uint32_t aSubSuperscript,
const uint8_t* aString, uint32_t aLength,
Script aRunScript) {
NS_ConvertASCIItoUTF16 unicodeString(
reinterpret_cast<
const char*>(aString),
aLength);
return SupportsSubSuperscript(aSubSuperscript, unicodeString.get(), aLength,
aRunScript);
}
bool gfxFont::SupportsSubSuperscript(uint32_t aSubSuperscript,
const char16_t* aString, uint32_t aLength,
Script aRunScript) {
NS_ASSERTION(aSubSuperscript == NS_FONT_VARIANT_POSITION_SUPER ||
aSubSuperscript == NS_FONT_VARIANT_POSITION_SUB,
"unknown value of font-variant-position");
uint32_t feature = aSubSuperscript == NS_FONT_VARIANT_POSITION_SUPER
? HB_TAG(
's',
'u',
'p',
's')
: HB_TAG(
's',
'u',
'b',
's');
if (!SupportsFeature(aRunScript, feature)) {
return false;
}
// xxx - for graphite, don't really know how to sniff lookups so bail
if (mGraphiteShaper && gfxPlatform::GetPlatform()->UseGraphiteShaping()) {
return true;
}
gfxHarfBuzzShaper* shaper = GetHarfBuzzShaper();
if (!shaper) {
return false;
}
// get the hbset containing input glyphs for the feature
const hb_set_t* inputGlyphs =
mFontEntry->InputsForOpenTypeFeature(aRunScript, feature);
// create an hbset containing default glyphs for the script run
hb_set_t* defaultGlyphsInRun = hb_set_create();
// for each character, get the glyph id
for (uint32_t i = 0; i < aLength; i++) {
uint32_t ch = aString[i];
if (i + 1 < aLength && NS_IS_SURROGATE_PAIR(ch, aString[i + 1])) {
i++;
ch = SURROGATE_TO_UCS4(ch, aString[i]);
}
hb_codepoint_t gid = shaper->GetNominalGlyph(ch);
hb_set_add(defaultGlyphsInRun, gid);
}
// intersect with input glyphs, if size is not the same ==> fallback
uint32_t origSize = hb_set_get_population(defaultGlyphsInRun);
hb_set_intersect(defaultGlyphsInRun, inputGlyphs);
uint32_t intersectionSize = hb_set_get_population(defaultGlyphsInRun);
hb_set_destroy(defaultGlyphsInRun);
return origSize == intersectionSize;
}
bool gfxFont::FeatureWillHandleChar(Script aRunScript, uint32_t aFeature,
uint32_t aUnicode) {
if (!SupportsFeature(aRunScript, aFeature)) {
return false;
}
// xxx - for graphite, don't really know how to sniff lookups so bail
if (mGraphiteShaper && gfxPlatform::GetPlatform()->UseGraphiteShaping()) {
return true;
}
if (gfxHarfBuzzShaper* shaper = GetHarfBuzzShaper()) {
// get the hbset containing input glyphs for the feature
const hb_set_t* inputGlyphs =
mFontEntry->InputsForOpenTypeFeature(aRunScript, aFeature);
hb_codepoint_t gid = shaper->GetNominalGlyph(aUnicode);
return hb_set_has(inputGlyphs, gid);
}
return false;
}
bool gfxFont::HasFeatureSet(uint32_t aFeature,
bool& aFeatureOn) {
aFeatureOn =
false;
if (mStyle.featureSettings.IsEmpty() &&
GetFontEntry()->mFeatureSettings.IsEmpty()) {
return false;
}
// add feature values from font
bool featureSet =
false;
uint32_t i, count;
nsTArray<gfxFontFeature>& fontFeatures = GetFontEntry()->mFeatureSettings;
count = fontFeatures.Length();
for (i = 0; i < count; i++) {
const gfxFontFeature& feature = fontFeatures.ElementAt(i);
if (feature.mTag == aFeature) {
featureSet =
true;
aFeatureOn = (feature.mValue != 0);
}
}
// add feature values from style rules
nsTArray<gfxFontFeature>& styleFeatures = mStyle.featureSettings;
count = styleFeatures.Length();
for (i = 0; i < count; i++) {
const gfxFontFeature& feature = styleFeatures.ElementAt(i);
if (feature.mTag == aFeature) {
featureSet =
true;
aFeatureOn = (feature.mValue != 0);
}
}
return featureSet;
}
already_AddRefed<mozilla::gfx::ScaledFont> gfxFont::GetScaledFont(
mozilla::gfx::DrawTarget* aDrawTarget) {
mozilla::gfx::PaletteCache dummy;
TextRunDrawParams params(dummy);
return GetScaledFont(params);
}
void gfxFont::InitializeScaledFont(
const RefPtr<mozilla::gfx::ScaledFont>& aScaledFont) {
if (!aScaledFont) {
return;
}
float angle = AngleForSyntheticOblique();
if (angle != 0.0f) {
aScaledFont->SetSyntheticObliqueAngle(angle);
}
}
/**
* A helper function in case we need to do any rounding or other
* processing here.
*/
#define ToDeviceUnits(aAppUnits, aDevUnitsPerAppUnit) \
(
double(aAppUnits) *
double(aDevUnitsPerAppUnit))
static AntialiasMode Get2DAAMode(gfxFont::AntialiasOption aAAOption) {
switch (aAAOption) {
case gfxFont::kAntialiasSubpixel:
return AntialiasMode::SUBPIXEL;
case gfxFont::kAntialiasGrayscale:
return AntialiasMode::GRAY;
case gfxFont::kAntialiasNone:
return AntialiasMode::NONE;
default:
return AntialiasMode::
DEFAULT;
}
}
class GlyphBufferAzure {
#define AUTO_BUFFER_SIZE (2048 /
sizeof(Glyph))
typedef mozilla::image::imgDrawingParams imgDrawingParams;
public:
GlyphBufferAzure(
const TextRunDrawParams& aRunParams,
const FontDrawParams& aFontParams)
: mRunParams(aRunParams),
mFontParams(aFontParams),
mBuffer(*mAutoBuffer.addr()),
mBufSize(AUTO_BUFFER_SIZE),
mCapacity(0),
mNumGlyphs(0) {}
~GlyphBufferAzure() {
if (mNumGlyphs > 0) {
FlushGlyphs();
}
if (mBuffer != *mAutoBuffer.addr()) {
free(mBuffer);
}
}
// Ensure the buffer has enough space for aGlyphCount glyphs to be added,
// considering the supplied strike multipler aStrikeCount.
// This MUST be called before OutputGlyph is used to actually store glyph
// records in the buffer. It may be called repeated to add further capacity
// in case we don't know up-front exactly what will be needed.
void AddCapacity(uint32_t aGlyphCount, uint32_t aStrikeCount) {
// Calculate the new capacity and ensure it will fit within the maximum
// allowed capacity.
static const uint64_t kMaxCapacity = 64 * 1024;
mCapacity = uint32_t(std::min(
kMaxCapacity,
uint64_t(mCapacity) + uint64_t(aGlyphCount) * uint64_t(aStrikeCount)));
// See if the required capacity fits within the already-allocated space
if (mCapacity <= mBufSize) {
return;
}
// We need to grow the buffer: determine a new size, allocate, and
// copy the existing data over if we didn't use realloc (which would
// do it automatically).
mBufSize = std::max(mCapacity, mBufSize * 2);
if (mBuffer == *mAutoBuffer.addr()) {
// switching from autobuffer to malloc, so we need to copy
mBuffer =
reinterpret_cast<Glyph*>(moz_xmalloc(mBufSize *
sizeof(Glyph)));
std::memcpy(mBuffer, *mAutoBuffer.addr(), mNumGlyphs *
sizeof(Glyph));
}
else {
mBuffer =
reinterpret_cast<Glyph*>(
moz_xrealloc(mBuffer, mBufSize *
sizeof(Glyph)));
}
}
void OutputGlyph(uint32_t aGlyphID,
const gfx::Point& aPt) {
// If the buffer is full, flush to make room for the new glyph.
if (mNumGlyphs >= mCapacity) {
// Check that AddCapacity has been used appropriately!
MOZ_ASSERT(mCapacity > 0 && mNumGlyphs == mCapacity);
Flush();
}
Glyph* glyph = mBuffer + mNumGlyphs++;
glyph->mIndex = aGlyphID;
glyph->mPosition = aPt;
}
void Flush() {
if (mNumGlyphs > 0) {
FlushGlyphs();
mNumGlyphs = 0;
}
}
const TextRunDrawParams& mRunParams;
const FontDrawParams& mFontParams;
private:
static DrawMode GetStrokeMode(DrawMode aMode) {
return aMode & (DrawMode::GLYPH_STROKE | DrawMode::GLYPH_STROKE_UNDERNEATH);
}
// Render the buffered glyphs to the draw target.
void FlushGlyphs() {
gfx::GlyphBuffer buf;
buf.mGlyphs = mBuffer;
buf.mNumGlyphs = mNumGlyphs;
const gfxContext::AzureState& state = mRunParams.context->CurrentState();
// Draw stroke first if the UNDERNEATH flag is set in drawMode.
if (mRunParams.strokeOpts &&
GetStrokeMode(mRunParams.drawMode) ==
(DrawMode::GLYPH_STROKE | DrawMode::GLYPH_STROKE_UNDERNEATH)) {
DrawStroke(state, buf);
}
if (mRunParams.drawMode & DrawMode::GLYPH_FILL) {
if (state.pattern || mFontParams.contextPaint) {
Pattern* pat;
RefPtr<gfxPattern> fillPattern;
if (mFontParams.contextPaint) {
imgDrawingParams imgParams;
fillPattern = mFontParams.contextPaint->GetFillPattern(
mRunParams.context->GetDrawTarget(),
mRunParams.context->CurrentMatrixDouble(), imgParams);
}
if (!fillPattern) {
if (state.pattern) {
--> --------------------
--> maximum size reached
--> --------------------