Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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/Telemetry.h"
#include "gfxMathTable.h"
#include "gfxSVGGlyphs.h"
#include "gfx2DGlue.h"
#include "TextDrawTarget.h"
#include "ThebesRLBox.h"
#include "GreekCasing.h"
#include "cairo.h"
#ifdef XP_WIN
# include "cairo-win32.h"
# include "gfxWindowsPlatform.h"
#endif
#include "harfbuzz/hb.h"
#include "harfbuzz/hb-ot.h"
#include <algorithm>
#include <limits>
#include <cmath>
using namespace mozilla;
using namespace mozilla::gfx;
using namespace mozilla::unicode;
using mozilla::services::GetObserverService;
gfxFontCache* gfxFontCache::gGlobalCache = nullptr;
#ifdef DEBUG_roc
# define DEBUG_TEXT_RUN_STORAGE_METRICS
#endif
#ifdef DEBUG_TEXT_RUN_STORAGE_METRICS
uint32_t gTextRunStorageHighWaterMark = 0;
uint32_t gTextRunStorage = 0;
uint32_t gFontCount = 0;
uint32_t gGlyphExtentsCount = 0;
uint32_t gGlyphExtentsWidthsTotalSize = 0;
uint32_t gGlyphExtentsSetupEagerSimple = 0;
uint32_t gGlyphExtentsSetupEagerTight = 0;
uint32_t gGlyphExtentsSetupLazyTight = 0;
uint32_t gGlyphExtentsSetupFallBackToTight = 0;
#endif
#define LOG_FONTINIT(args) \
MOZ_LOG(gfxPlatform::GetLog(eGfxLog_fontinit), LogLevel::Debug, args)
#define LOG_FONTINIT_ENABLED() \
MOZ_LOG_TEST(gfxPlatform::GetLog(eGfxLog_fontinit), LogLevel::Debug)
/*
* gfxFontCache - global cache of gfxFont instances.
* Expires unused fonts after a short interval;
* notifies fonts to age their cached shaped-word records;
* observes memory-pressure notification and tells fonts to clear their
* shaped-word caches to free up memory.
*/
MOZ_DEFINE_MALLOC_SIZE_OF(FontCacheMallocSizeOf)
NS_IMPL_ISUPPORTS(gfxFontCache::MemoryReporter, nsIMemoryReporter)
/*virtual*/
gfxTextRunFactory::~gfxTextRunFactory() {
// Should not be dropped by stylo
MOZ_ASSERT(!Servo_IsWorkerThread());
}
NS_IMETHODIMP
gfxFontCache::MemoryReporter::CollectReports(
nsIHandleReportCallback* aHandleReport, nsISupports* aData,
bool aAnonymize) {
FontCacheSizes sizes;
gfxFontCache::GetCache()->AddSizeOfIncludingThis(&FontCacheMallocSizeOf,
&sizes);
MOZ_COLLECT_REPORT("explicit/gfx/font-cache", KIND_HEAP, UNITS_BYTES,
sizes.mFontInstances,
"Memory used for active font instances.");
MOZ_COLLECT_REPORT("explicit/gfx/font-shaped-words", KIND_HEAP, UNITS_BYTES,
sizes.mShapedWords,
"Memory used to cache shaped glyph data.");
return NS_OK;
}
NS_IMPL_ISUPPORTS(gfxFontCache::Observer, nsIObserver)
NS_IMETHODIMP
gfxFontCache::Observer::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* someData) {
if (!nsCRT::strcmp(aTopic, "memory-pressure")) {
gfxFontCache* fontCache = gfxFontCache::GetCache();
if (fontCache) {
fontCache->FlushShapedWordCaches();
}
} else {
MOZ_ASSERT_UNREACHABLE("unexpected notification topic");
}
return NS_OK;
}
nsresult gfxFontCache::Init() {
NS_ASSERTION(!gGlobalCache, "Where did this come from?");
gGlobalCache = new gfxFontCache(GetMainThreadSerialEventTarget());
if (!gGlobalCache) {
return NS_ERROR_OUT_OF_MEMORY;
}
RegisterStrongMemoryReporter(new MemoryReporter());
return NS_OK;
}
void gfxFontCache::Shutdown() {
delete gGlobalCache;
gGlobalCache = nullptr;
#ifdef DEBUG_TEXT_RUN_STORAGE_METRICS
printf("Textrun storage high water mark=%d\n", gTextRunStorageHighWaterMark);
printf("Total number of fonts=%d\n", gFontCount);
printf("Total glyph extents allocated=%d (size %d)\n", gGlyphExtentsCount,
int(gGlyphExtentsCount * sizeof(gfxGlyphExtents)));
printf("Total glyph extents width-storage size allocated=%d\n",
gGlyphExtentsWidthsTotalSize);
printf("Number of simple glyph extents eagerly requested=%d\n",
gGlyphExtentsSetupEagerSimple);
printf("Number of tight glyph extents eagerly requested=%d\n",
gGlyphExtentsSetupEagerTight);
printf("Number of tight glyph extents lazily requested=%d\n",
gGlyphExtentsSetupLazyTight);
printf("Number of simple glyph extent setups that fell back to tight=%d\n",
gGlyphExtentsSetupFallBackToTight);
#endif
}
gfxFontCache::gfxFontCache(nsIEventTarget* aEventTarget)
: ExpirationTrackerImpl<gfxFont, 3, Lock, AutoLock>(
FONT_TIMEOUT_SECONDS * 1000, "gfxFontCache", aEventTarget) {
nsCOMPtr<nsIObserverService> obs = GetObserverService();
if (obs) {
obs->AddObserver(new Observer, "memory-pressure", false);
}
nsIEventTarget* target = nullptr;
if (XRE_IsContentProcess() && NS_IsMainThread()) {
target = aEventTarget;
}
// Create the timer used to expire shaped-word records from each font's
// cache after a short period of non-use. We have a single timer in
// gfxFontCache that loops over all fonts known to the cache, to avoid
// the overhead of individual timers in each font instance.
// The timer will be started any time shaped word records are cached
// (and pauses itself when all caches become empty).
mWordCacheExpirationTimer = NS_NewTimer(target);
}
gfxFontCache::~gfxFontCache() {
// Ensure the user font cache releases its references to font entries,
// so they aren't kept alive after the font instances and font-list
// have been shut down.
gfxUserFontSet::UserFontCache::Shutdown();
if (mWordCacheExpirationTimer) {
mWordCacheExpirationTimer->Cancel();
mWordCacheExpirationTimer = nullptr;
}
// Expire everything manually so we don't leak them.
Flush();
}
bool gfxFontCache::HashEntry::KeyEquals(const KeyTypePointer aKey) const {
const gfxCharacterMap* fontUnicodeRangeMap = mFont->GetUnicodeRangeMap();
return aKey->mFontEntry == mFont->GetFontEntry() &&
aKey->mStyle->Equals(*mFont->GetStyle()) &&
((!aKey->mUnicodeRangeMap && !fontUnicodeRangeMap) ||
(aKey->mUnicodeRangeMap && fontUnicodeRangeMap &&
aKey->mUnicodeRangeMap->Equals(fontUnicodeRangeMap)));
}
already_AddRefed<gfxFont> gfxFontCache::Lookup(
const gfxFontEntry* aFontEntry, const gfxFontStyle* aStyle,
const gfxCharacterMap* aUnicodeRangeMap) {
MutexAutoLock lock(mMutex);
Key key(aFontEntry, aStyle, aUnicodeRangeMap);
HashEntry* entry = mFonts.GetEntry(key);
Telemetry::Accumulate(Telemetry::FONT_CACHE_HIT, entry != nullptr);
if (!entry) {
return nullptr;
}
RefPtr<gfxFont> font = entry->mFont;
if (font->GetExpirationState()->IsTracked()) {
RemoveObjectLocked(font, lock);
}
return font.forget();
}
already_AddRefed<gfxFont> gfxFontCache::MaybeInsert(gfxFont* aFont) {
MOZ_ASSERT(aFont);
MutexAutoLock lock(mMutex);
Key key(aFont->GetFontEntry(), aFont->GetStyle(),
aFont->GetUnicodeRangeMap());
HashEntry* entry = mFonts.PutEntry(key);
if (!entry) {
return do_AddRef(aFont);
}
// If it is null, then we are inserting a new entry. Otherwise we are
// attempting to replace an existing font, probably due to a thread race, in
// which case stick with the original font.
if (!entry->mFont) {
entry->mFont = aFont;
// Assert that we can find the entry we just put in (this fails if the key
// has a NaN float value in it, e.g. 'sizeAdjust').
MOZ_ASSERT(entry == mFonts.GetEntry(key));
} else {
MOZ_ASSERT(entry->mFont != aFont);
aFont->Destroy();
if (entry->mFont->GetExpirationState()->IsTracked()) {
RemoveObjectLocked(entry->mFont, lock);
}
}
return do_AddRef(entry->mFont);
}
bool gfxFontCache::MaybeDestroy(gfxFont* aFont) {
MOZ_ASSERT(aFont);
MutexAutoLock lock(mMutex);
// If the font has a non-zero refcount, then we must have lost the race with
// gfxFontCache::Lookup and the same font was reacquired.
if (aFont->GetRefCount() > 0) {
return false;
}
Key key(aFont->GetFontEntry(), aFont->GetStyle(),
aFont->GetUnicodeRangeMap());
HashEntry* entry = mFonts.GetEntry(key);
if (!entry || entry->mFont != aFont) {
MOZ_ASSERT(!aFont->GetExpirationState()->IsTracked());
return true;
}
// If the font is being tracked, we must have then also lost another race with
// gfxFontCache::MaybeDestroy which re-added it to the tracker.
if (aFont->GetExpirationState()->IsTracked()) {
return false;
}
// Typically this won't fail, but it may during startup/shutdown if the timer
// service is not available.
nsresult rv = AddObjectLocked(aFont, lock);
if (NS_SUCCEEDED(rv)) {
return false;
}
mFonts.RemoveEntry(entry);
return true;
}
void gfxFontCache::NotifyExpiredLocked(gfxFont* aFont, const AutoLock& aLock) {
MOZ_ASSERT(aFont->GetRefCount() == 0);
RemoveObjectLocked(aFont, aLock);
mTrackerDiscard.AppendElement(aFont);
Key key(aFont->GetFontEntry(), aFont->GetStyle(),
aFont->GetUnicodeRangeMap());
HashEntry* entry = mFonts.GetEntry(key);
if (!entry || entry->mFont != aFont) {
MOZ_ASSERT_UNREACHABLE("Invalid font?");
return;
}
mFonts.RemoveEntry(entry);
}
void gfxFontCache::NotifyHandlerEnd() {
nsTArray<gfxFont*> discard;
{
MutexAutoLock lock(mMutex);
discard = std::move(mTrackerDiscard);
}
DestroyDiscard(discard);
}
void gfxFontCache::DestroyDiscard(nsTArray<gfxFont*>& aDiscard) {
for (auto& font : aDiscard) {
NS_ASSERTION(font->GetRefCount() == 0,
"Destroying with refs outside cache!");
font->ClearCachedWords();
font->Destroy();
}
aDiscard.Clear();
}
void gfxFontCache::Flush() {
nsTArray<gfxFont*> discard;
{
MutexAutoLock lock(mMutex);
discard.SetCapacity(mFonts.Count());
for (auto iter = mFonts.Iter(); !iter.Done(); iter.Next()) {
HashEntry* entry = static_cast<HashEntry*>(iter.Get());
if (!entry || !entry->mFont) {
MOZ_ASSERT_UNREACHABLE("Invalid font?");
continue;
}
if (entry->mFont->GetRefCount() == 0) {
// If we are not tracked, then we must have won the race with
// gfxFont::MaybeDestroy and it is waiting on the mutex. To avoid a
// double free, we let gfxFont::MaybeDestroy handle the freeing when it
// acquires the mutex and discovers there is no matching entry in the
// hashtable.
if (entry->mFont->GetExpirationState()->IsTracked()) {
RemoveObjectLocked(entry->mFont, lock);
discard.AppendElement(entry->mFont);
}
} else {
MOZ_ASSERT(!entry->mFont->GetExpirationState()->IsTracked());
}
}
MOZ_ASSERT(IsEmptyLocked(lock),
"Cache tracker still has fonts after flush!");
mFonts.Clear();
}
DestroyDiscard(discard);
}
/*static*/
void gfxFontCache::WordCacheExpirationTimerCallback(nsITimer* aTimer,
void* aCache) {
gfxFontCache* cache = static_cast<gfxFontCache*>(aCache);
cache->AgeCachedWords();
}
void gfxFontCache::AgeCachedWords() {
bool allEmpty = true;
{
MutexAutoLock lock(mMutex);
for (const auto& entry : mFonts) {
allEmpty = entry.mFont->AgeCachedWords() && allEmpty;
}
}
if (allEmpty) {
PauseWordCacheExpirationTimer();
}
}
void gfxFontCache::FlushShapedWordCaches() {
{
MutexAutoLock lock(mMutex);
for (const auto& entry : mFonts) {
entry.mFont->ClearCachedWords();
}
}
PauseWordCacheExpirationTimer();
}
void gfxFontCache::NotifyGlyphsChanged() {
MutexAutoLock lock(mMutex);
for (const auto& entry : mFonts) {
entry.mFont->NotifyGlyphsChanged();
}
}
void gfxFontCache::AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf,
FontCacheSizes* aSizes) const {
// TODO: add the overhead of the expiration tracker (generation arrays)
MutexAutoLock lock(*const_cast<Mutex*>(&mMutex));
aSizes->mFontInstances += mFonts.ShallowSizeOfExcludingThis(aMallocSizeOf);
for (const auto& entry : mFonts) {
entry.mFont->AddSizeOfExcludingThis(aMallocSizeOf, aSizes);
}
}
void gfxFontCache::AddSizeOfIncludingThis(MallocSizeOf aMallocSizeOf,
FontCacheSizes* aSizes) const {
aSizes->mFontInstances += aMallocSizeOf(this);
AddSizeOfExcludingThis(aMallocSizeOf, aSizes);
}
#define MAX_SSXX_VALUE 99
#define MAX_CVXX_VALUE 99
static void LookupAlternateValues(const gfxFontFeatureValueSet& aFeatureLookup,
const nsACString& aFamily,
const StyleVariantAlternates& aAlternates,
nsTArray<gfxFontFeature>& aFontFeatures) {
using Tag = StyleVariantAlternates::Tag;
// historical-forms gets handled in nsFont::AddFontFeaturesToStyle.
if (aAlternates.IsHistoricalForms()) {
return;
}
gfxFontFeature feature;
if (aAlternates.IsCharacterVariant()) {
for (auto& ident : aAlternates.AsCharacterVariant().AsSpan()) {
Span<const uint32_t> values = aFeatureLookup.GetFontFeatureValuesFor(
aFamily, NS_FONT_VARIANT_ALTERNATES_CHARACTER_VARIANT,
ident.AsAtom());
// nothing defined, skip
if (values.IsEmpty()) {
continue;
}
NS_ASSERTION(values.Length() <= 2,
"too many values allowed for character-variant");
// character-variant(12 3) ==> 'cv12' = 3
uint32_t nn = values[0];
// ignore values greater than 99
if (nn == 0 || nn > MAX_CVXX_VALUE) {
continue;
}
feature.mValue = values.Length() > 1 ? values[1] : 1;
feature.mTag = HB_TAG('c', 'v', ('0' + nn / 10), ('0' + nn % 10));
aFontFeatures.AppendElement(feature);
}
return;
}
if (aAlternates.IsStyleset()) {
for (auto& ident : aAlternates.AsStyleset().AsSpan()) {
Span<const uint32_t> values = aFeatureLookup.GetFontFeatureValuesFor(
aFamily, NS_FONT_VARIANT_ALTERNATES_STYLESET, ident.AsAtom());
// styleset(1 2 7) ==> 'ss01' = 1, 'ss02' = 1, 'ss07' = 1
feature.mValue = 1;
for (uint32_t nn : values) {
if (nn == 0 || nn > MAX_SSXX_VALUE) {
continue;
}
feature.mTag = HB_TAG('s', 's', ('0' + nn / 10), ('0' + nn % 10));
aFontFeatures.AppendElement(feature);
}
}
return;
}
uint32_t constant = 0;
nsAtom* name = nullptr;
switch (aAlternates.tag) {
case Tag::Swash:
constant = NS_FONT_VARIANT_ALTERNATES_SWASH;
name = aAlternates.AsSwash().AsAtom();
break;
case Tag::Stylistic:
constant = NS_FONT_VARIANT_ALTERNATES_STYLISTIC;
name = aAlternates.AsStylistic().AsAtom();
break;
case Tag::Ornaments:
constant = NS_FONT_VARIANT_ALTERNATES_ORNAMENTS;
name = aAlternates.AsOrnaments().AsAtom();
break;
case Tag::Annotation:
constant = NS_FONT_VARIANT_ALTERNATES_ANNOTATION;
name = aAlternates.AsAnnotation().AsAtom();
break;
default:
MOZ_ASSERT_UNREACHABLE("Unknown font-variant-alternates value!");
return;
}
Span<const uint32_t> values =
aFeatureLookup.GetFontFeatureValuesFor(aFamily, constant, name);
if (values.IsEmpty()) {
return;
}
MOZ_ASSERT(values.Length() == 1,
"too many values for font-specific font-variant-alternates");
feature.mValue = values[0];
switch (aAlternates.tag) {
case Tag::Swash: // swsh, cswh
feature.mTag = HB_TAG('s', 'w', 's', 'h');
aFontFeatures.AppendElement(feature);
feature.mTag = HB_TAG('c', 's', 'w', 'h');
break;
case Tag::Stylistic: // salt
feature.mTag = HB_TAG('s', 'a', 'l', 't');
break;
case Tag::Ornaments: // ornm
feature.mTag = HB_TAG('o', 'r', 'n', 'm');
break;
case Tag::Annotation: // nalt
feature.mTag = HB_TAG('n', 'a', 'l', 't');
break;
default:
MOZ_ASSERT_UNREACHABLE("how?");
return;
}
aFontFeatures.AppendElement(feature);
}
/* static */
void gfxFontShaper::MergeFontFeatures(
const gfxFontStyle* aStyle, const nsTArray<gfxFontFeature>& aFontFeatures,
bool aDisableLigatures, const nsACString& aFamilyName, bool aAddSmallCaps,
void (*aHandleFeature)(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 = ArrayLength(lookups);
hb_ot_layout_feature_get_lookups(aFace, aTableTag, aFeatureIndex, offset,
&len, lookups);
for (i = 0; i < len; i++) {
hb_set_add(aLookups, lookups[i]);
}
offset += len;
} while (len == ArrayLength(lookups));
}
static void CollectLookupsByLanguage(
hb_face_t* aFace, hb_tag_t aTableTag,
const nsTHashSet<uint32_t>& aSpecificFeatures, hb_set_t* aOtherLookups,
hb_set_t* aSpecificFeatureLookups, uint32_t aScriptIndex,
uint32_t aLangIndex) {
uint32_t reqFeatureIndex;
if (hb_ot_layout_language_get_required_feature_index(
aFace, aTableTag, aScriptIndex, aLangIndex, &reqFeatureIndex)) {
CollectLookupsByFeature(aFace, aTableTag, reqFeatureIndex, aOtherLookups);
}
uint32_t featureIndexes[32];
uint32_t i, len, offset;
offset = 0;
do {
len = ArrayLength(featureIndexes);
hb_ot_layout_language_get_feature_indexes(aFace, aTableTag, aScriptIndex,
aLangIndex, offset, &len,
featureIndexes);
for (i = 0; i < len; i++) {
uint32_t featureIndex = featureIndexes[i];
// get the feature tag
hb_tag_t featureTag;
uint32_t tagLen = 1;
hb_ot_layout_language_get_feature_tags(aFace, aTableTag, aScriptIndex,
aLangIndex, offset + i, &tagLen,
&featureTag);
// collect lookups
hb_set_t* lookups = aSpecificFeatures.Contains(featureTag)
? aSpecificFeatureLookups
: aOtherLookups;
CollectLookupsByFeature(aFace, aTableTag, featureIndex, lookups);
}
offset += len;
} while (len == ArrayLength(featureIndexes));
}
static bool HasLookupRuleWithGlyphByScript(
hb_face_t* aFace, hb_tag_t aTableTag, hb_tag_t aScriptTag,
uint32_t aScriptIndex, uint16_t aGlyph,
const nsTHashSet<uint32_t>& aDefaultFeatures,
bool& aHasDefaultFeatureWithGlyph) {
uint32_t numLangs, lang;
hb_set_t* defaultFeatureLookups = hb_set_create();
hb_set_t* nonDefaultFeatureLookups = hb_set_create();
// default lang
CollectLookupsByLanguage(aFace, aTableTag, aDefaultFeatures,
nonDefaultFeatureLookups, defaultFeatureLookups,
aScriptIndex, HB_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX);
// iterate over langs
numLangs = hb_ot_layout_script_get_language_tags(
aFace, aTableTag, aScriptIndex, 0, nullptr, nullptr);
for (lang = 0; lang < numLangs; lang++) {
CollectLookupsByLanguage(aFace, aTableTag, aDefaultFeatures,
nonDefaultFeatureLookups, defaultFeatureLookups,
aScriptIndex, lang);
}
// look for the glyph among default feature lookups
aHasDefaultFeatureWithGlyph = false;
hb_set_t* glyphs = hb_set_create();
hb_codepoint_t index = -1;
while (hb_set_next(defaultFeatureLookups, &index)) {
hb_ot_layout_lookup_collect_glyphs(aFace, aTableTag, index, glyphs, glyphs,
glyphs, nullptr);
if (hb_set_has(glyphs, aGlyph)) {
aHasDefaultFeatureWithGlyph = true;
break;
}
}
// look for the glyph among non-default feature lookups
// if no default feature lookups contained spaces
bool hasNonDefaultFeatureWithGlyph = false;
if (!aHasDefaultFeatureWithGlyph) {
hb_set_clear(glyphs);
index = -1;
while (hb_set_next(nonDefaultFeatureLookups, &index)) {
hb_ot_layout_lookup_collect_glyphs(aFace, aTableTag, index, glyphs,
glyphs, glyphs, nullptr);
if (hb_set_has(glyphs, aGlyph)) {
hasNonDefaultFeatureWithGlyph = true;
break;
}
}
}
hb_set_destroy(glyphs);
hb_set_destroy(defaultFeatureLookups);
hb_set_destroy(nonDefaultFeatureLookups);
return aHasDefaultFeatureWithGlyph || hasNonDefaultFeatureWithGlyph;
}
static void HasLookupRuleWithGlyph(hb_face_t* aFace, hb_tag_t aTableTag,
bool& aHasGlyph, hb_tag_t aSpecificFeature,
bool& aHasGlyphSpecific, uint16_t aGlyph) {
// iterate over the scripts in the font
uint32_t numScripts, numLangs, script, lang;
hb_set_t* otherLookups = hb_set_create();
hb_set_t* specificFeatureLookups = hb_set_create();
nsTHashSet<uint32_t> specificFeature(1);
specificFeature.Insert(aSpecificFeature);
numScripts =
hb_ot_layout_table_get_script_tags(aFace, aTableTag, 0, nullptr, nullptr);
for (script = 0; script < numScripts; script++) {
// default lang
CollectLookupsByLanguage(aFace, aTableTag, specificFeature, otherLookups,
specificFeatureLookups, script,
HB_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX);
// iterate over langs
numLangs = hb_ot_layout_script_get_language_tags(
aFace, HB_OT_TAG_GPOS, script, 0, nullptr, nullptr);
for (lang = 0; lang < numLangs; lang++) {
CollectLookupsByLanguage(aFace, aTableTag, specificFeature, otherLookups,
specificFeatureLookups, script, lang);
}
}
// look for the glyph among non-specific feature lookups
hb_set_t* glyphs = hb_set_create();
hb_codepoint_t index = -1;
while (hb_set_next(otherLookups, &index)) {
hb_ot_layout_lookup_collect_glyphs(aFace, aTableTag, index, glyphs, glyphs,
glyphs, nullptr);
if (hb_set_has(glyphs, aGlyph)) {
aHasGlyph = true;
break;
}
}
// look for the glyph among specific feature lookups
hb_set_clear(glyphs);
index = -1;
while (hb_set_next(specificFeatureLookups, &index)) {
hb_ot_layout_lookup_collect_glyphs(aFace, aTableTag, index, glyphs, glyphs,
glyphs, nullptr);
if (hb_set_has(glyphs, aGlyph)) {
aHasGlyphSpecific = true;
break;
}
}
hb_set_destroy(glyphs);
hb_set_destroy(specificFeatureLookups);
hb_set_destroy(otherLookups);
}
Atomic<nsTHashMap<nsUint32HashKey, intl::Script>*> gfxFont::sScriptTagToCode;
Atomic<nsTHashSet<uint32_t>*> gfxFont::sDefaultFeatures;
static inline bool HasSubstitution(uint32_t* aBitVector, intl::Script aScript) {
return (aBitVector[static_cast<uint32_t>(aScript) >> 5] &
(1 << (static_cast<uint32_t>(aScript) & 0x1f))) != 0;
}
// union of all default substitution features across scripts
static const hb_tag_t defaultFeatures[] = {
HB_TAG('a', 'b', 'v', 'f'), HB_TAG('a', 'b', 'v', 's'),
HB_TAG('a', 'k', 'h', 'n'), HB_TAG('b', 'l', 'w', 'f'),
HB_TAG('b', 'l', 'w', 's'), HB_TAG('c', 'a', 'l', 't'),
HB_TAG('c', 'c', 'm', 'p'), HB_TAG('c', 'f', 'a', 'r'),
HB_TAG('c', 'j', 'c', 't'), HB_TAG('c', 'l', 'i', 'g'),
HB_TAG('f', 'i', 'n', '2'), HB_TAG('f', 'i', 'n', '3'),
HB_TAG('f', 'i', 'n', 'a'), HB_TAG('h', 'a', 'l', 'f'),
HB_TAG('h', 'a', 'l', 'n'), HB_TAG('i', 'n', 'i', 't'),
HB_TAG('i', 's', 'o', 'l'), HB_TAG('l', 'i', 'g', 'a'),
HB_TAG('l', 'j', 'm', 'o'), HB_TAG('l', 'o', 'c', 'l'),
HB_TAG('l', 't', 'r', 'a'), HB_TAG('l', 't', 'r', 'm'),
HB_TAG('m', 'e', 'd', '2'), HB_TAG('m', 'e', 'd', 'i'),
HB_TAG('m', 's', 'e', 't'), HB_TAG('n', 'u', 'k', 't'),
HB_TAG('p', 'r', 'e', 'f'), HB_TAG('p', 'r', 'e', 's'),
HB_TAG('p', 's', 't', 'f'), HB_TAG('p', 's', 't', 's'),
HB_TAG('r', 'c', 'l', 't'), HB_TAG('r', 'l', 'i', 'g'),
HB_TAG('r', 'k', 'r', 'f'), HB_TAG('r', 'p', 'h', 'f'),
HB_TAG('r', 't', 'l', 'a'), HB_TAG('r', 't', 'l', 'm'),
HB_TAG('t', 'j', 'm', 'o'), HB_TAG('v', 'a', 't', 'u'),
HB_TAG('v', 'e', 'r', 't'), HB_TAG('v', 'j', 'm', 'o')};
void gfxFont::CheckForFeaturesInvolvingSpace() const {
gfxFontEntry::SpaceFeatures flags = gfxFontEntry::SpaceFeatures::None;
auto setFlags =
MakeScopeExit([&]() { mFontEntry->mHasSpaceFeatures = flags; });
bool log = LOG_FONTINIT_ENABLED();
TimeStamp start;
if (MOZ_UNLIKELY(log)) {
start = TimeStamp::Now();
}
uint32_t spaceGlyph = GetSpaceGlyph();
if (!spaceGlyph) {
return;
}
auto face(GetFontEntry()->GetHBFace());
// GSUB lookups - examine per script
if (hb_ot_layout_has_substitution(face)) {
// Get the script ==> code hashtable, creating it on first use.
nsTHashMap<nsUint32HashKey, Script>* tagToCode = sScriptTagToCode;
if (!tagToCode) {
tagToCode = new nsTHashMap<nsUint32HashKey, Script>(
size_t(Script::NUM_SCRIPT_CODES));
tagToCode->InsertOrUpdate(HB_TAG('D', 'F', 'L', 'T'), Script::COMMON);
// Ensure that we don't try to look at script codes beyond what the
// current version of ICU (at runtime -- in case of system ICU)
// knows about.
Script scriptCount = Script(
std::min<int>(intl::UnicodeProperties::GetMaxNumberOfScripts() + 1,
int(Script::NUM_SCRIPT_CODES)));
for (Script s = Script::ARABIC; s < scriptCount;
s = Script(static_cast<int>(s) + 1)) {
hb_script_t script = hb_script_t(GetScriptTagForCode(s));
unsigned int scriptCount = 4;
hb_tag_t scriptTags[4];
hb_ot_tags_from_script_and_language(script, HB_LANGUAGE_INVALID,
&scriptCount, scriptTags, nullptr,
nullptr);
for (unsigned int i = 0; i < scriptCount; i++) {
tagToCode->InsertOrUpdate(scriptTags[i], s);
}
}
if (!sScriptTagToCode.compareExchange(nullptr, tagToCode)) {
// We lost a race! Discard our new table and use the winner.
delete tagToCode;
tagToCode = sScriptTagToCode;
}
}
// Set up the default-features hashset on first use.
if (!sDefaultFeatures) {
uint32_t numDefaultFeatures = ArrayLength(defaultFeatures);
auto* set = new nsTHashSet<uint32_t>(numDefaultFeatures);
for (uint32_t i = 0; i < numDefaultFeatures; i++) {
set->Insert(defaultFeatures[i]);
}
if (!sDefaultFeatures.compareExchange(nullptr, set)) {
delete set;
}
}
// iterate over the scripts in the font
hb_tag_t scriptTags[8];
uint32_t len, offset = 0;
do {
len = ArrayLength(scriptTags);
hb_ot_layout_table_get_script_tags(face, HB_OT_TAG_GSUB, offset, &len,
scriptTags);
for (uint32_t i = 0; i < len; i++) {
bool isDefaultFeature = false;
Script s;
if (!HasLookupRuleWithGlyphByScript(
face, HB_OT_TAG_GSUB, scriptTags[i], offset + i, spaceGlyph,
*sDefaultFeatures, isDefaultFeature) ||
!tagToCode->Get(scriptTags[i], &s)) {
continue;
}
flags = flags | gfxFontEntry::SpaceFeatures::HasFeatures;
uint32_t index = static_cast<uint32_t>(s) >> 5;
uint32_t bit = static_cast<uint32_t>(s) & 0x1f;
if (isDefaultFeature) {
mFontEntry->mDefaultSubSpaceFeatures[index] |= (1 << bit);
} else {
mFontEntry->mNonDefaultSubSpaceFeatures[index] |= (1 << bit);
}
}
offset += len;
} while (len == ArrayLength(scriptTags));
}
// spaces in default features of default script?
// ==> can't use word cache, skip GPOS analysis
bool canUseWordCache = true;
if (HasSubstitution(mFontEntry->mDefaultSubSpaceFeatures, Script::COMMON)) {
canUseWordCache = false;
}
// GPOS lookups - distinguish kerning from non-kerning features
if (canUseWordCache && hb_ot_layout_has_positioning(face)) {
bool hasKerning = false, hasNonKerning = false;
HasLookupRuleWithGlyph(face, HB_OT_TAG_GPOS, hasNonKerning,
HB_TAG('k', 'e', 'r', 'n'), hasKerning, spaceGlyph);
if (hasKerning) {
flags |= gfxFontEntry::SpaceFeatures::HasFeatures |
gfxFontEntry::SpaceFeatures::Kerning;
}
if (hasNonKerning) {
flags |= gfxFontEntry::SpaceFeatures::HasFeatures |
gfxFontEntry::SpaceFeatures::NonKerning;
}
}
if (MOZ_UNLIKELY(log)) {
TimeDuration elapsed = TimeStamp::Now() - start;
LOG_FONTINIT((
"(fontinit-spacelookups) font: %s - "
"subst default: %8.8x %8.8x %8.8x %8.8x "
"subst non-default: %8.8x %8.8x %8.8x %8.8x "
"kerning: %s non-kerning: %s time: %6.3f\n",
mFontEntry->Name().get(), mFontEntry->mDefaultSubSpaceFeatures[3],
mFontEntry->mDefaultSubSpaceFeatures[2],
mFontEntry->mDefaultSubSpaceFeatures[1],
mFontEntry->mDefaultSubSpaceFeatures[0],
mFontEntry->mNonDefaultSubSpaceFeatures[3],
mFontEntry->mNonDefaultSubSpaceFeatures[2],
mFontEntry->mNonDefaultSubSpaceFeatures[1],
mFontEntry->mNonDefaultSubSpaceFeatures[0],
(mFontEntry->mHasSpaceFeatures & gfxFontEntry::SpaceFeatures::Kerning
? "true"
: "false"),
(mFontEntry->mHasSpaceFeatures & gfxFontEntry::SpaceFeatures::NonKerning
? "true"
: "false"),
elapsed.ToMilliseconds()));
}
}
bool gfxFont::HasSubstitutionRulesWithSpaceLookups(Script aRunScript) const {
NS_ASSERTION(GetFontEntry()->mHasSpaceFeatures !=
gfxFontEntry::SpaceFeatures::Uninitialized,
"need to initialize space lookup flags");
NS_ASSERTION(aRunScript < Script::NUM_SCRIPT_CODES, "weird script code");
if (aRunScript == Script::INVALID || aRunScript >= Script::NUM_SCRIPT_CODES) {
return false;
}
// default features have space lookups ==> true
if (HasSubstitution(mFontEntry->mDefaultSubSpaceFeatures, Script::COMMON) ||
HasSubstitution(mFontEntry->mDefaultSubSpaceFeatures, aRunScript)) {
return true;
}
// non-default features have space lookups and some type of
// font feature, in font or style is specified ==> true
if ((HasSubstitution(mFontEntry->mNonDefaultSubSpaceFeatures,
Script::COMMON) ||
HasSubstitution(mFontEntry->mNonDefaultSubSpaceFeatures, aRunScript)) &&
(!mStyle.featureSettings.IsEmpty() ||
!mFontEntry->mFeatureSettings.IsEmpty())) {
return true;
}
return false;
}
tainted_boolean_hint gfxFont::SpaceMayParticipateInShaping(
Script aRunScript) const {
// avoid checking fonts known not to include default space-dependent features
if (MOZ_UNLIKELY(mFontEntry->mSkipDefaultFeatureSpaceCheck)) {
if (!mKerningSet && mStyle.featureSettings.IsEmpty() &&
mFontEntry->mFeatureSettings.IsEmpty()) {
return false;
}
}
if (FontCanSupportGraphite()) {
if (gfxPlatform::GetPlatform()->UseGraphiteShaping()) {
return mFontEntry->HasGraphiteSpaceContextuals();
}
}
// We record the presence of space-dependent features in the font entry
// so that subsequent instantiations for the same font face won't
// require us to re-check the tables; however, the actual check is done
// by gfxFont because not all font entry subclasses know how to create
// a harfbuzz face for introspection.
gfxFontEntry::SpaceFeatures flags = mFontEntry->mHasSpaceFeatures;
if (flags == gfxFontEntry::SpaceFeatures::Uninitialized) {
CheckForFeaturesInvolvingSpace();
flags = mFontEntry->mHasSpaceFeatures;
}
if (!(flags & gfxFontEntry::SpaceFeatures::HasFeatures)) {
return false;
}
// if font has substitution rules or non-kerning positioning rules
// that involve spaces, bypass
if (HasSubstitutionRulesWithSpaceLookups(aRunScript) ||
(flags & gfxFontEntry::SpaceFeatures::NonKerning)) {
return true;
}
// if kerning explicitly enabled/disabled via font-feature-settings or
// font-kerning and kerning rules use spaces, only bypass when enabled
if (mKerningSet && (flags & gfxFontEntry::SpaceFeatures::Kerning)) {
return mKerningEnabled;
}
return false;
}
bool gfxFont::SupportsFeature(Script aScript, uint32_t aFeatureTag) {
if (mGraphiteShaper && gfxPlatform::GetPlatform()->UseGraphiteShaping()) {
return GetFontEntry()->SupportsGraphiteFeature(aFeatureTag);
}
return GetFontEntry()->SupportsOpenTypeFeature(aScript, aFeatureTag);
}
bool gfxFont::SupportsVariantCaps(Script aScript, uint32_t aVariantCaps,
bool& aFallbackToSmallCaps,
bool& aSyntheticLowerToSmallCaps,
bool& aSyntheticUpperToSmallCaps) {
bool ok = true; // cases without fallback are fine
aFallbackToSmallCaps = false;
aSyntheticLowerToSmallCaps = false;
aSyntheticUpperToSmallCaps = false;
switch (aVariantCaps) {
case NS_FONT_VARIANT_CAPS_SMALLCAPS:
ok = SupportsFeature(aScript, HB_TAG('s', 'm', 'c', 'p'));
if (!ok) {
aSyntheticLowerToSmallCaps = true;
}
break;
case NS_FONT_VARIANT_CAPS_ALLSMALL:
ok = SupportsFeature(aScript, HB_TAG('s', 'm', 'c', 'p')) &&
SupportsFeature(aScript, HB_TAG('c', '2', 's', 'c'));
if (!ok) {
aSyntheticLowerToSmallCaps = true;
aSyntheticUpperToSmallCaps = true;
}
break;
case NS_FONT_VARIANT_CAPS_PETITECAPS:
ok = SupportsFeature(aScript, HB_TAG('p', 'c', 'a', 'p'));
if (!ok) {
ok = SupportsFeature(aScript, HB_TAG('s', 'm', 'c', 'p'));
aFallbackToSmallCaps = ok;
}
if (!ok) {
aSyntheticLowerToSmallCaps = true;
}
break;
case NS_FONT_VARIANT_CAPS_ALLPETITE:
ok = SupportsFeature(aScript, HB_TAG('p', 'c', 'a', 'p')) &&
SupportsFeature(aScript, HB_TAG('c', '2', 'p', 'c'));
if (!ok) {
ok = SupportsFeature(aScript, HB_TAG('s', 'm', 'c', 'p')) &&
SupportsFeature(aScript, HB_TAG('c', '2', 's', 'c'));
aFallbackToSmallCaps = ok;
}
if (!ok) {
aSyntheticLowerToSmallCaps = true;
aSyntheticUpperToSmallCaps = true;
}
break;
default:
break;
}
NS_ASSERTION(
!(ok && (aSyntheticLowerToSmallCaps || aSyntheticUpperToSmallCaps)),
"shouldn't use synthetic features if we found real ones");
NS_ASSERTION(!(!ok && aFallbackToSmallCaps),
"if we found a usable fallback, that counts as ok");
return ok;
}
bool gfxFont::SupportsSubSuperscript(uint32_t aSubSuperscript,
const uint8_t* aString, uint32_t aLength,
Script aRunScript) {
NS_ConvertASCIItoUTF16 unicodeString(reinterpret_cast<const char*>(aString),
aLength);
return SupportsSubSuperscript(aSubSuperscript, unicodeString.get(), aLength,
aRunScript);
}
bool gfxFont::SupportsSubSuperscript(uint32_t aSubSuperscript,
const char16_t* aString, uint32_t aLength,
Script aRunScript) {
NS_ASSERTION(aSubSuperscript == NS_FONT_VARIANT_POSITION_SUPER ||
aSubSuperscript == NS_FONT_VARIANT_POSITION_SUB,
"unknown value of font-variant-position");
uint32_t feature = aSubSuperscript == NS_FONT_VARIANT_POSITION_SUPER
? HB_TAG('s', 'u', 'p', 's')
: HB_TAG('s', 'u', 'b', 's');
if (!SupportsFeature(aRunScript, feature)) {
return false;
}
// xxx - for graphite, don't really know how to sniff lookups so bail
if (mGraphiteShaper && gfxPlatform::GetPlatform()->UseGraphiteShaping()) {
return true;
}
gfxHarfBuzzShaper* shaper = GetHarfBuzzShaper();
if (!shaper) {
return false;
}
// get the hbset containing input glyphs for the feature
const hb_set_t* inputGlyphs =
mFontEntry->InputsForOpenTypeFeature(aRunScript, feature);
// create an hbset containing default glyphs for the script run
hb_set_t* defaultGlyphsInRun = hb_set_create();
// for each character, get the glyph id
for (uint32_t i = 0; i < aLength; i++) {
uint32_t ch = aString[i];
if (i + 1 < aLength && NS_IS_SURROGATE_PAIR(ch, aString[i + 1])) {
i++;
ch = SURROGATE_TO_UCS4(ch, aString[i]);
}
hb_codepoint_t gid = shaper->GetNominalGlyph(ch);
hb_set_add(defaultGlyphsInRun, gid);
}
// intersect with input glyphs, if size is not the same ==> fallback
uint32_t origSize = hb_set_get_population(defaultGlyphsInRun);
hb_set_intersect(defaultGlyphsInRun, inputGlyphs);
uint32_t intersectionSize = hb_set_get_population(defaultGlyphsInRun);
hb_set_destroy(defaultGlyphsInRun);
return origSize == intersectionSize;
}
bool gfxFont::FeatureWillHandleChar(Script aRunScript, uint32_t aFeature,
uint32_t aUnicode) {
if (!SupportsFeature(aRunScript, aFeature)) {
return false;
}
// xxx - for graphite, don't really know how to sniff lookups so bail
if (mGraphiteShaper && gfxPlatform::GetPlatform()->UseGraphiteShaping()) {
return true;
}
if (gfxHarfBuzzShaper* shaper = GetHarfBuzzShaper()) {
// get the hbset containing input glyphs for the feature
const hb_set_t* inputGlyphs =
mFontEntry->InputsForOpenTypeFeature(aRunScript, aFeature);
hb_codepoint_t gid = shaper->GetNominalGlyph(aUnicode);
return hb_set_has(inputGlyphs, gid);
}
return false;
}
bool gfxFont::HasFeatureSet(uint32_t aFeature, bool& aFeatureOn) {
aFeatureOn = false;
if (mStyle.featureSettings.IsEmpty() &&
GetFontEntry()->mFeatureSettings.IsEmpty()) {
return false;
}
// add feature values from font
bool featureSet = false;
uint32_t i, count;
nsTArray<gfxFontFeature>& fontFeatures = GetFontEntry()->mFeatureSettings;
count = fontFeatures.Length();
for (i = 0; i < count; i++) {
const gfxFontFeature& feature = fontFeatures.ElementAt(i);
if (feature.mTag == aFeature) {
featureSet = true;
aFeatureOn = (feature.mValue != 0);
}
}
// add feature values from style rules
nsTArray<gfxFontFeature>& styleFeatures = mStyle.featureSettings;
count = styleFeatures.Length();
for (i = 0; i < count; i++) {
const gfxFontFeature& feature = styleFeatures.ElementAt(i);
if (feature.mTag == aFeature) {
featureSet = true;
aFeatureOn = (feature.mValue != 0);
}
}
return featureSet;
}
already_AddRefed<mozilla::gfx::ScaledFont> gfxFont::GetScaledFont(
mozilla::gfx::DrawTarget* aDrawTarget) {
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) {
RefPtr<gfxPattern> statePattern =
mRunParams.context->CurrentState().pattern;
pat = statePattern->GetPattern(mRunParams.dt,
state.patternTransformChanged
? &state.patternTransform
: nullptr);
} else {
pat = nullptr;
}
} else {
pat = fillPattern->GetPattern(mRunParams.dt);
}
if (pat) {
mRunParams.dt->FillGlyphs(mFontParams.scaledFont, buf, *pat,
mFontParams.drawOptions);
}
} else {
mRunParams.dt->FillGlyphs(mFontParams.scaledFont, buf,
ColorPattern(state.color),
mFontParams.drawOptions);
}
}
// Draw stroke if the UNDERNEATH flag is not set.
if (mRunParams.strokeOpts &&
GetStrokeMode(mRunParams.drawMode) == DrawMode::GLYPH_STROKE) {
DrawStroke(state, buf);
}
if (mRunParams.drawMode & DrawMode::GLYPH_PATH) {
mRunParams.context->EnsurePathBuilder();
Matrix mat = mRunParams.dt->GetTransform();
mFontParams.scaledFont->CopyGlyphsToBuilder(
buf, mRunParams.context->mPathBuilder, &mat);
}
}
void DrawStroke(const gfxContext::AzureState& aState,
gfx::GlyphBuffer& aBuffer) {
if (mRunParams.textStrokePattern) {
Pattern* pat = mRunParams.textStrokePattern->GetPattern(
mRunParams.dt,
aState.patternTransformChanged ? &aState.patternTransform : nullptr);
if (pat) {
FlushStroke(aBuffer, *pat);
}
} else {
FlushStroke(aBuffer,
ColorPattern(ToDeviceColor(mRunParams.textStrokeColor)));
}
}
void FlushStroke(gfx::GlyphBuffer& aBuf, const Pattern& aPattern) {
mRunParams.dt->StrokeGlyphs(mFontParams.scaledFont, aBuf, aPattern,
*mRunParams.strokeOpts,
mFontParams.drawOptions);
}
// We use an "inline" buffer automatically allocated (on the stack) as part
// of the GlyphBufferAzure object to hold the glyphs in most cases, falling
// back to a separately-allocated heap buffer if the count of buffered
// glyphs gets too big.
//
// This is basically a rudimentary AutoTArray; so why not use AutoTArray
// itself?
//
// If we used an AutoTArray, we'd want to avoid using SetLength or
// AppendElements to allocate the space we actually need, because those
// methods would default-construct the new elements.
//
// Could we use SetCapacity to reserve the necessary buffer space without
// default-constructing all the Glyph records? No, because of a failure
// that could occur when we need to grow the buffer, which happens when we
// encounter a DetailedGlyph in the textrun that refers to a sequence of
// several real glyphs. At that point, we need to add some extra capacity
// to the buffer we initially allocated based on the length of the textrun
// range we're rendering.
//
// This buffer growth would work fine as long as it still fits within the
// array's inline buffer (we just use a bit more of it), or if the buffer
// was already heap-allocated (in which case AutoTArray will use realloc(),
// preserving its contents). But a problem will arise when the initial
// capacity we allocated (based on the length of the run) fits within the
// array's inline buffer, but subsequently we need to extend the buffer
// beyond the inline buffer size, so we reallocate to the heap. Because we
// haven't "officially" filled the array with SetLength or AppendElements,
// its mLength is still zero; as far as it's concerned the buffer is just
// uninitialized space, and when it switches to use a malloc'd buffer it
// won't copy the existing contents.
// Allocate space for a buffer of Glyph records, without initializing them.
AlignedStorage2<Glyph[AUTO_BUFFER_SIZE]> mAutoBuffer;
// Pointer to the buffer we're currently using -- initially mAutoBuffer,
// but may be changed to a malloc'd buffer, in which case that buffer must
// be free'd on destruction.
Glyph* mBuffer;
uint32_t mBufSize; // size of allocated buffer; capacity can grow to
// this before reallocation is needed
uint32_t mCapacity; // amount of buffer size reserved
uint32_t mNumGlyphs; // number of glyphs actually present in the buffer
#undef AUTO_BUFFER_SIZE
};
// Bug 674909. When synthetic bolding text by drawing twice, need to
// render using a pixel offset in device pixels, otherwise text
// doesn't appear bolded, it appears as if a bad text shadow exists
// when a non-identity transform exists. Use an offset factor so that
// the second draw occurs at a constant offset in device pixels.
gfx::Float gfxFont::CalcXScale(DrawTarget* aDrawTarget) {
// determine magnitude of a 1px x offset in device space
Size t = aDrawTarget->GetTransform().TransformSize(Size(1.0, 0.0));
if (t.width == 1.0 && t.height == 0.0) {
// short-circuit the most common case to avoid sqrt() and division
return 1.0;
}
gfx::Float m = sqrtf(t.width * t.width + t.height * t.height);
NS_ASSERTION(m != 0.0, "degenerate transform while synthetic bolding");
if (m == 0.0) {
return 0.0; // effectively disables offset
}
// scale factor so that offsets are 1px in device pixels
return 1.0 / m;
}
// Draw a run of CharacterGlyph records from the given offset in aShapedText.
// Returns true if glyph paths were actually emitted.
template <gfxFont::FontComplexityT FC, gfxFont::SpacingT S>
bool gfxFont::DrawGlyphs(const gfxShapedText* aShapedText,
uint32_t aOffset, // offset in the textrun
uint32_t aCount, // length of run to draw
gfx::Point* aPt,
const gfx::Matrix* aOffsetMatrix, // may be null
GlyphBufferAzure& aBuffer) {
float& inlineCoord =
aBuffer.mFontParams.isVerticalFont ? aPt->y.value : aPt->x.value;
const gfxShapedText::CompressedGlyph* glyphData =
&aShapedText->GetCharacterGlyphs()[aOffset];
if (S == SpacingT::HasSpacing) {
float space = aBuffer.mRunParams.spacing[0].mBefore *
aBuffer.mFontParams.advanceDirection;
inlineCoord += space;
}
// Allocate buffer space for the run, assuming all simple glyphs.
uint32_t capacityMult = 1 + aBuffer.mFontParams.extraStrikes;
aBuffer.AddCapacity(aCount, capacityMult);
bool emittedGlyphs = false;
for (uint32_t i = 0; i < aCount; ++i, ++glyphData) {
if (glyphData->IsSimpleGlyph()) {
float advance =
glyphData->GetSimpleAdvance() * aBuffer.mFontParams.advanceDirection;
if (aBuffer.mRunParams.isRTL) {
inlineCoord += advance;
}
DrawOneGlyph<FC>(glyphData->GetSimpleGlyph(), *aPt, aBuffer,
&emittedGlyphs);
if (!aBuffer.mRunParams.isRTL) {
inlineCoord += advance;
}
} else {
uint32_t glyphCount = glyphData->GetGlyphCount();
if (glyphCount > 0) {
// Add extra buffer capacity to allow for multiple-glyph entry.
aBuffer.AddCapacity(glyphCount - 1, capacityMult);
const gfxShapedText::DetailedGlyph* details =
aShapedText->GetDetailedGlyphs(aOffset + i);
MOZ_ASSERT(details, "missing DetailedGlyph!");
for (uint32_t j = 0; j < glyphCount; ++j, ++details) {
float advance =
details->mAdvance * aBuffer.mFontParams.advanceDirection;
if (aBuffer.mRunParams.isRTL) {
inlineCoord += advance;
}
if (glyphData->IsMissing()) {
if (!DrawMissingGlyph(aBuffer.mRunParams, aBuffer.mFontParams,
details, *aPt)) {
return false;
}
} else {
gfx::Point glyphPt(
*aPt + (aOffsetMatrix
? aOffsetMatrix->TransformPoint(details->mOffset)
: details->mOffset));
DrawOneGlyph<FC>(details->mGlyphID, glyphPt, aBuffer,
&emittedGlyphs);
}
if (!aBuffer.mRunParams.isRTL) {
inlineCoord += advance;
}
}
}
}
if (S == SpacingT::HasSpacing) {
float space = aBuffer.mRunParams.spacing[i].mAfter;
if (i + 1 < aCount) {
space += aBuffer.mRunParams.spacing[i + 1].mBefore;
}
space *= aBuffer.mFontParams.advanceDirection;
inlineCoord += space;
}
}
return emittedGlyphs;
}
// Draw an individual glyph at a specific location.
// *aPt is the glyph position in appUnits; it is converted to device
// coordinates (devPt) here.
template <gfxFont::FontComplexityT FC>
void gfxFont::DrawOneGlyph(uint32_t aGlyphID, const gfx::Point& aPt,
GlyphBufferAzure& aBuffer, bool* aEmittedGlyphs) {
const TextRunDrawParams& runParams(aBuffer.mRunParams);
gfx::Point devPt(ToDeviceUnits(aPt.x, runParams.devPerApp),
ToDeviceUnits(aPt.y, runParams.devPerApp));
auto* textDrawer = runParams.textDrawer;
if (textDrawer) {
// If the glyph is entirely outside the clip rect, we don't need to draw it
// at all. (We check the font extents here rather than the individual glyph
// bounds because that's cheaper to look up, and provides a conservative
// "worst case" for where this glyph might want to draw.)
LayoutDeviceRect extents =
LayoutDeviceRect::FromUnknownRect(aBuffer.mFontParams.fontExtents);
extents.MoveBy(LayoutDevicePoint::FromUnknownPoint(devPt));
if (!extents.Intersects(runParams.clipRect)) {
return;
}
}
if (FC == FontComplexityT::ComplexFont) {
const FontDrawParams& fontParams(aBuffer.mFontParams);
gfxContextMatrixAutoSaveRestore matrixRestore;
if (fontParams.obliqueSkew != 0.0f && fontParams.isVerticalFont &&
!textDrawer) {
// We have to flush each glyph individually when doing
// synthetic-oblique for vertical-upright text, because
// the skew transform needs to be applied to a separate
// origin for each glyph, not once for the whole run.
aBuffer.Flush();
matrixRestore.SetContext(runParams.context);
gfx::Point skewPt(
devPt.x + GetMetrics(nsFontMetrics::eVertical).emHeight / 2, devPt.y);
gfx::Matrix mat =
runParams.context->CurrentMatrix()
.PreTranslate(skewPt)
.PreMultiply(gfx::Matrix(1, fontParams.obliqueSkew, 0, 1, 0, 0))
.PreTranslate(-skewPt);
runParams.context->SetMatrix(mat);
}
if (fontParams.haveSVGGlyphs) {
if (!runParams.paintSVGGlyphs) {
return;
}
NS_WARNING_ASSERTION(
runParams.drawMode != DrawMode::GLYPH_PATH,
"Rendering SVG glyph despite request for glyph path");
if (RenderSVGGlyph(runParams.context, textDrawer, devPt, aGlyphID,
fontParams.contextPaint, runParams.callbacks,
*aEmittedGlyphs)) {
return;
}
}
if (fontParams.haveColorGlyphs && !UseNativeColrFontSupport() &&
RenderColorGlyph(runParams.dt, runParams.context, textDrawer,
fontParams, devPt, aGlyphID)) {
return;
}
aBuffer.OutputGlyph(aGlyphID, devPt);
// Synthetic bolding (if required) by multi-striking.
for (int32_t i = 0; i < fontParams.extraStrikes; ++i) {
if (fontParams.isVerticalFont) {
devPt.y += fontParams.synBoldOnePixelOffset;
} else {
devPt.x += fontParams.synBoldOnePixelOffset;
}
aBuffer.OutputGlyph(aGlyphID, devPt);
}
if (fontParams.obliqueSkew != 0.0f && fontParams.isVerticalFont &&
!textDrawer) {
aBuffer.Flush();
}
} else {
aBuffer.OutputGlyph(aGlyphID, devPt);
}
*aEmittedGlyphs = true;
}
bool gfxFont::DrawMissingGlyph(const TextRunDrawParams& aRunParams,
const FontDrawParams& aFontParams,
const gfxShapedText::DetailedGlyph* aDetails,
const gfx::Point& aPt) {
// Default-ignorable chars will have zero advance width;
// we don't have to draw the hexbox for them.
float advance = aDetails->mAdvance;
if (aRunParams.drawMode != DrawMode::GLYPH_PATH && advance > 0) {
auto* textDrawer = aRunParams.textDrawer;
const Matrix* matPtr = nullptr;
Matrix mat;
if (textDrawer) {
// Generate an orientation matrix for the current writing mode
wr::FontInstanceFlags flags = textDrawer->GetWRGlyphFlags();
if (flags & wr::FontInstanceFlags::TRANSPOSE) {
std::swap(mat._11, mat._12);
std::swap(mat._21, mat._22);
}
mat.PostScale(flags & wr::FontInstanceFlags::FLIP_X ? -1.0f : 1.0f,
flags & wr::FontInstanceFlags::FLIP_Y ? -1.0f : 1.0f);
matPtr = &mat;
}
Point pt(Float(ToDeviceUnits(aPt.x, aRunParams.devPerApp)),
Float(ToDeviceUnits(aPt.y, aRunParams.devPerApp)));
Float advanceDevUnits = Float(ToDeviceUnits(advance, aRunParams.devPerApp));
Float height = GetMetrics(nsFontMetrics::eHorizontal).maxAscent;
// Horizontally center if drawing vertically upright with no sideways
// transform.
Rect glyphRect =
aFontParams.isVerticalFont && !mat.HasNonAxisAlignedTransform()
? Rect(pt.x - height / 2, pt.y, height, advanceDevUnits)
: Rect(pt.x, pt.y - height, advanceDevUnits, height);
// If there's a fake-italic skew in effect as part
// of the drawTarget's transform, we need to undo
// this before drawing the hexbox. (Bug 983985)
gfxContextMatrixAutoSaveRestore matrixRestore;
if (aFontParams.obliqueSkew != 0.0f && !aFontParams.isVerticalFont &&
!textDrawer) {
matrixRestore.SetContext(aRunParams.context);
gfx::Matrix mat =
aRunParams.context->CurrentMatrix()
.PreTranslate(pt)
.PreMultiply(gfx::Matrix(1, 0, aFontParams.obliqueSkew, 1, 0, 0))
.PreTranslate(-pt);
aRunParams.context->SetMatrix(mat);
}
gfxFontMissingGlyphs::DrawMissingGlyph(
aDetails->mGlyphID, glyphRect, *aRunParams.dt,
PatternFromState(aRunParams.context), matPtr);
}
return true;
}
// This method is mostly parallel to DrawGlyphs.
void gfxFont::DrawEmphasisMarks(const gfxTextRun* aShapedText, gfx::Point* aPt,
uint32_t aOffset, uint32_t aCount,
const EmphasisMarkDrawParams& aParams) {
float& inlineCoord = aParams.isVertical ? aPt->y.value : aPt->x.value;
gfxTextRun::Range markRange(aParams.mark);
gfxTextRun::DrawParams params(aParams.context, aParams.paletteCache);
float clusterStart = -std::numeric_limits<float>::infinity();
bool shouldDrawEmphasisMark = false;
for (uint32_t i = 0, idx = aOffset; i < aCount; ++i, ++idx) {
if (aParams.spacing) {
inlineCoord += aParams.direction * aParams.spacing[i].mBefore;
}
if (aShapedText->IsClusterStart(idx) ||
clusterStart == -std::numeric_limits<float>::infinity()) {
clusterStart = inlineCoord;
}
if (aShapedText->CharMayHaveEmphasisMark(idx)) {
shouldDrawEmphasisMark = true;
}
inlineCoord += aParams.direction * aShapedText->GetAdvanceForGlyph(idx);
if (shouldDrawEmphasisMark &&
(i + 1 == aCount || aShapedText->IsClusterStart(idx + 1))) {
float clusterAdvance = inlineCoord - clusterStart;
// Move the coord backward to get the needed start point.
float delta = (clusterAdvance + aParams.advance) / 2;
inlineCoord -= delta;
aParams.mark->Draw(markRange, *aPt, params);
inlineCoord += delta;
shouldDrawEmphasisMark = false;
}
if (aParams.spacing) {
inlineCoord += aParams.direction * aParams.spacing[i].mAfter;
}
}
}
void gfxFont::Draw(const gfxTextRun* aTextRun, uint32_t aStart, uint32_t aEnd,
gfx::Point* aPt, TextRunDrawParams& aRunParams,
gfx::ShapedTextFlags aOrientation) {
NS_ASSERTION(aRunParams.drawMode == DrawMode::GLYPH_PATH ||
!(int(aRunParams.drawMode) & int(DrawMode::GLYPH_PATH)),
"GLYPH_PATH cannot be used with GLYPH_FILL, GLYPH_STROKE or "
"GLYPH_STROKE_UNDERNEATH");
if (aStart >= aEnd) {
return;
}
FontDrawParams fontParams;
if (aRunParams.drawOpts) {
fontParams.drawOptions = *aRunParams.drawOpts;
}
fontParams.scaledFont = GetScaledFont(aRunParams);
if (!fontParams.scaledFont) {
return;
}
auto* textDrawer = aRunParams.textDrawer;
fontParams.obliqueSkew = SkewForSyntheticOblique();
fontParams.haveSVGGlyphs = GetFontEntry()->TryGetSVGData(this);
fontParams.haveColorGlyphs = GetFontEntry()->TryGetColorGlyphs();
fontParams.hasTextShadow = aRunParams.hasTextShadow;
fontParams.contextPaint = aRunParams.runContextPaint;
if (fontParams.haveColorGlyphs && !UseNativeColrFontSupport()) {
DeviceColor ctxColor;
fontParams.currentColor = aRunParams.context->GetDeviceColor(ctxColor)
? sRGBColor::FromABGR(ctxColor.ToABGR())
: sRGBColor::OpaqueBlack();
fontParams.palette = aRunParams.paletteCache.GetPaletteFor(
GetFontEntry(), aRunParams.fontPalette);
}
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.
if (aTextRun->UseCenterBaseline()) {
const Metrics& metrics = GetMetrics(nsFontMetrics::eHorizontal);
float baseAdj = (metrics.emAscent - metrics.emDescent) / 2;
baseline += baseAdj * aTextRun->GetAppUnitsPerDevUnit() * baselineDir;
}
} else if (textDrawer &&
aOrientation == ShapedTextFlags::TEXT_ORIENT_VERTICAL_UPRIGHT) {
glyphFlagsRestore.Save(textDrawer);
textDrawer->SetWRGlyphFlags(textDrawer->GetWRGlyphFlags() |
wr::FontInstanceFlags::VERTICAL);
}
if (fontParams.obliqueSkew != 0.0f && !fontParams.isVerticalFont &&
!textDrawer) {
// Adjust matrix for synthetic-oblique, except if we're doing vertical-
// upright text, in which case this will be handled for each glyph
// individually in DrawOneGlyph.
if (!matrixRestore.HasMatrix()) {
matrixRestore.SetContext(aRunParams.context);
}
gfx::Point p(aPt->x * aRunParams.devPerApp, aPt->y * aRunParams.devPerApp);
gfx::Matrix mat =
aRunParams.context->CurrentMatrix()
.PreTranslate(p)
.PreMultiply(gfx::Matrix(1, 0, -fontParams.obliqueSkew, 1, 0, 0))
.PreTranslate(-p);
aRunParams.context->SetMatrix(mat);
}
RefPtr<SVGContextPaint> contextPaint;
if (fontParams.haveSVGGlyphs && !fontParams.contextPaint) {
// If no pattern is specified for fill, use the current pattern
NS_ASSERTION((int(aRunParams.drawMode) & int(DrawMode::GLYPH_STROKE)) == 0,
"no pattern supplied for stroking text");
RefPtr<gfxPattern> fillPattern = aRunParams.context->GetPattern();
contextPaint = new SimpleTextContextPaint(
fillPattern, nullptr, aRunParams.context->CurrentMatrixDouble());
fontParams.contextPaint = contextPaint.get();
}
// Synthetic-bold strikes are each offset one device pixel in run direction
// (these values are only needed if ApplySyntheticBold() is true).
// If drawing via webrender, it will do multistrike internally so we don't
// need to handle it here.
bool doMultistrikeBold = ApplySyntheticBold() && !textDrawer;
if (doMultistrikeBold) {
// For screen display, we want to try and repeat strikes with an offset of
// one device pixel, accounting for zoom or other transforms that may be
// in effect, so compute x-axis scale factor from the drawtarget.
// However, when generating PDF output the drawtarget's transform does not
// really bear any relation to "device pixels", and may result in an
// excessively large offset relative to the font size (bug 1823888), so
// we limit it based on the used font size to avoid this.
// The constant 48.0 reflects the threshold where the calculation in
// gfxFont::GetSyntheticBoldOffset() switches to a simple origin-based
// slope, though the exact value is somewhat arbitrary; it's selected to
// allow a visible amount of boldness while preventing the offset from
// becoming "large" in relation to the glyphs.
Float xscale =
std::min<Float>(GetAdjustedSize() / 48.0,
CalcXScale(aRunParams.context->GetDrawTarget()));
fontParams.synBoldOnePixelOffset = aRunParams.direction * xscale;
if (xscale != 0.0) {
static const int32_t kMaxExtraStrikes = 128;
gfxFloat extraStrikes = GetSyntheticBoldOffset() / xscale;
if (extraStrikes > kMaxExtraStrikes) {
// if too many strikes are required, limit them and increase the step
// size to compensate
fontParams.extraStrikes = kMaxExtraStrikes;
fontParams.synBoldOnePixelOffset = aRunParams.direction *
GetSyntheticBoldOffset() /
fontParams.extraStrikes;
} else {
// use as many strikes as needed for the increased advance
fontParams.extraStrikes = NS_lroundf(std::max(1.0, extraStrikes));
}
} else {
// Degenerate transform?!
fontParams.extraStrikes = 0;
}
} else {
fontParams.synBoldOnePixelOffset = 0;
fontParams.extraStrikes = 0;
}
// Figure out the maximum extents for the font, accounting for synthetic
// oblique and bold.
if (mFUnitsConvFactor > 0.0) {
fontParams.fontExtents = GetFontEntry()->GetFontExtents(mFUnitsConvFactor);
} else {
// Was it not an sfnt? Maybe on Linux... use arbitrary huge extents, so we
// don't inadvertently clip stuff. A bit less efficient than true extents,
// but this should be extremely rare.
auto size = GetAdjustedSize();
fontParams.fontExtents = Rect(-2 * size, -2 * size, 5 * size, 5 * size);
}
if (fontParams.obliqueSkew != 0.0f) {
gfx::Point p(fontParams.fontExtents.x, fontParams.fontExtents.y);
gfx::Matrix skew(1, 0, fontParams.obliqueSkew, 1, 0, 0);
fontParams.fontExtents = skew.TransformBounds(fontParams.fontExtents);
}
if (fontParams.extraStrikes) {
if (fontParams.isVerticalFont) {
fontParams.fontExtents.height +=
float(fontParams.extraStrikes) * fontParams.synBoldOnePixelOffset;
} else {
fontParams.fontExtents.width +=
float(fontParams.extraStrikes) * fontParams.synBoldOnePixelOffset;
}
}
bool oldSubpixelAA = aRunParams.dt->GetPermitSubpixelAA();
if (!AllowSubpixelAA()) {
aRunParams.dt->SetPermitSubpixelAA(false);
}
Matrix mat;
Matrix oldMat = aRunParams.dt->GetTransform();
fontParams.drawOptions.mAntialiasMode = Get2DAAMode(mAntialiasOption);
if (mStyle.baselineOffset != 0.0) {
baseline +=
mStyle.baselineOffset * aTextRun->GetAppUnitsPerDevUnit() * baselineDir;
}
bool emittedGlyphs;
{
// Select appropriate version of the templated DrawGlyphs method
// to output glyphs to the buffer, depending on complexity needed
// for the type of font, and whether added inter-glyph spacing
// is specified.
GlyphBufferAzure buffer(aRunParams, fontParams);
if (fontParams.haveSVGGlyphs || fontParams.haveColorGlyphs ||
fontParams.extraStrikes ||
(fontParams.obliqueSkew != 0.0f && fontParams.isVerticalFont &&
!textDrawer)) {
if (aRunParams.spacing) {
emittedGlyphs =
DrawGlyphs<FontComplexityT::ComplexFont, SpacingT::HasSpacing>(
aTextRun, aStart, aEnd - aStart, aPt, offsetMatrix, buffer);
} else {
emittedGlyphs =
DrawGlyphs<FontComplexityT::ComplexFont, SpacingT::NoSpacing>(
aTextRun, aStart, aEnd - aStart, aPt, offsetMatrix, buffer);
}
} else {
if (aRunParams.spacing) {
emittedGlyphs =
DrawGlyphs<FontComplexityT::SimpleFont, SpacingT::HasSpacing>(
aTextRun, aStart, aEnd - aStart, aPt, offsetMatrix, buffer);
} else {
emittedGlyphs =
DrawGlyphs<FontComplexityT::SimpleFont, SpacingT::NoSpacing>(
aTextRun, aStart, aEnd - aStart, aPt, offsetMatrix, buffer);
}
}
}
baseline = origBaseline;
if (aRunParams.callbacks && emittedGlyphs) {
aRunParams.callbacks->NotifyGlyphPathEmitted();
}
aRunParams.dt->SetTransform(oldMat);
aRunParams.dt->SetPermitSubpixelAA(oldSubpixelAA);
if (sidewaysDir != 0.0f && !textDrawer) {
// Adjust updated aPt to account for the transform we were using.
// The advance happened horizontally in local-space, but the transformed
// sideways advance is actually vertical, with sign depending on the
// direction of rotation.
float advance = aPt->x - origPt.x;
*aPt = gfx::Point(origPt.x, origPt.y + advance * sidewaysDir);
}
}
bool gfxFont::RenderSVGGlyph(gfxContext* aContext,
layout::TextDrawTarget* aTextDrawer,
gfx::Point aPoint, uint32_t aGlyphId,
SVGContextPaint* aContextPaint) const {
if (!GetFontEntry()->HasSVGGlyph(aGlyphId)) {
return false;
}
if (aTextDrawer) {
// WebRender doesn't support SVG Glyphs.
// (pretend to succeed, output doesn't matter, we will emit a blob)
aTextDrawer->FoundUnsupportedFeature();
return true;
}
const gfxFloat devUnitsPerSVGUnit =
GetAdjustedSize() / GetFontEntry()->UnitsPerEm();
gfxContextMatrixAutoSaveRestore matrixRestore(aContext);
aContext->SetMatrix(aContext->CurrentMatrix()
.PreTranslate(aPoint.x, aPoint.y)
.PreScale(devUnitsPerSVGUnit, devUnitsPerSVGUnit));
aContextPaint->InitStrokeGeometry(aContext, devUnitsPerSVGUnit);
GetFontEntry()->RenderSVGGlyph(aContext, aGlyphId, aContextPaint);
aContext->NewPath();
return true;
}
bool gfxFont::RenderSVGGlyph(gfxContext* aContext,
layout::TextDrawTarget* aTextDrawer,
gfx::Point aPoint, uint32_t aGlyphId,
SVGContextPaint* aContextPaint,
gfxTextRunDrawCallbacks* aCallbacks,
bool& aEmittedGlyphs) const {
if (aCallbacks && aEmittedGlyphs) {
aCallbacks->NotifyGlyphPathEmitted();
aEmittedGlyphs = false;
}
return RenderSVGGlyph(aContext, aTextDrawer, aPoint, aGlyphId, aContextPaint);
}
bool gfxFont::RenderColorGlyph(DrawTarget* aDrawTarget, gfxContext* aContext,
layout::TextDrawTarget* aTextDrawer,
const FontDrawParams& aFontParams,
const Point& aPoint, uint32_t aGlyphId) {
if (aTextDrawer && aFontParams.hasTextShadow) {
aTextDrawer->FoundUnsupportedFeature();
return true;
}
auto* colr = GetFontEntry()->GetCOLR();
if (const auto* paintGraph = COLRFonts::GetGlyphPaintGraph(colr, aGlyphId)) {
const auto* hbShaper = GetHarfBuzzShaper();
if (hbShaper && hbShaper->IsInitialized()) {
if (aTextDrawer) {
aTextDrawer->FoundUnsupportedFeature();
return true;
}
// For reasonable font sizes, use a cache of rasterized glyphs.
if (GetAdjustedSize() <= 256.0) {
AutoWriteLock lock(mLock);
if (!mColorGlyphCache) {
mColorGlyphCache = MakeUnique<ColorGlyphCache>();
}
// Tell the cache what colors we're using; if they have changed, it will
// discard any currently-cached entries.
mColorGlyphCache->SetColors(aFontParams.currentColor,
aFontParams.palette);
bool ok = false;
auto cached = mColorGlyphCache->mCache.lookupForAdd(aGlyphId);
Rect bounds = COLRFonts::GetColorGlyphBounds(
colr, hbShaper->GetHBFont(), aGlyphId, aDrawTarget,
aFontParams.scaledFont, mFUnitsConvFactor);
bounds.RoundOut();
if (cached) {
ok = true;
} else {
// Create a temporary DrawTarget, render the glyph, and save a
// snapshot of the rendering in the cache.
IntSize size(int(bounds.width), int(bounds.height));
SurfaceFormat format = SurfaceFormat::B8G8R8A8;
RefPtr target =
Factory::CreateDrawTarget(BackendType::SKIA, size, format);
if (target) {
ok = COLRFonts::PaintGlyphGraph(
GetFontEntry()->GetCOLR(), hbShaper->GetHBFont(), paintGraph,
target, nullptr, aFontParams.scaledFont,
aFontParams.drawOptions, -bounds.TopLeft(),
aFontParams.currentColor, aFontParams.palette->Colors(),
aGlyphId, mFUnitsConvFactor);
if (ok) {
RefPtr snapshot = target->Snapshot();
ok = mColorGlyphCache->mCache.add(cached, aGlyphId, snapshot);
}
}
}
if (ok) {
// Paint the snapshot from cached->value(), and return.
aDrawTarget->DrawSurface(
cached->value(), Rect(aPoint + bounds.TopLeft(), bounds.Size()),
Rect(Point(), bounds.Size()));
return true;
}
}
// If we failed to cache the glyph, or it was too large to even try,
// just paint directly to the target.
return COLRFonts::PaintGlyphGraph(
colr, hbShaper->GetHBFont(), paintGraph, aDrawTarget, aTextDrawer,
aFontParams.scaledFont, aFontParams.drawOptions, aPoint,
aFontParams.currentColor, aFontParams.palette->Colors(), aGlyphId,
mFUnitsConvFactor);
}
}
if (const auto* layers =
COLRFonts::GetGlyphLayers(GetFontEntry()->GetCOLR(), aGlyphId)) {
auto face(GetFontEntry()->GetHBFace());
bool ok = COLRFonts::PaintGlyphLayers(
colr, face, layers, aDrawTarget, aTextDrawer, aFontParams.scaledFont,
aFontParams.drawOptions, aPoint, aFontParams.currentColor,
aFontParams.palette->Colors());
return ok;
}
return false;
}
void gfxFont::ColorGlyphCache::SetColors(sRGBColor aCurrentColor,
FontPalette* aPalette) {
if (aCurrentColor != mCurrentColor || aPalette != mPalette) {
mCache.clear();
mCurrentColor = aCurrentColor;
mPalette = aPalette;
}
}
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;
}
// Emoji special-case: flag sequences NOT based on Regional Indicator pairs
// use the BLACK FLAG character plus a series of plane-14 TAG LETTERs, e.g.
// England = <black-flag, tag-G, tag-B, tag-E, tag-N, tag-G, tag-cancel>
// Here, we don't check for support of the entire sequence (too much
// expensive lookahead), but we check that the font at least supports the
// first of the tag letter codes, because if it doesn't, we're at risk of
// just getting an undifferentiated black flag glyph.
if (gfxFontUtils::IsEmojiFlagAndTag(aCh, aNextCh)) {
if (!shaper->GetNominalGlyph(aNextCh)) {
return false;
}
}
// Check if there is a COLR/CPAL or SVG glyph for this ID.
if (fe->TryGetColorGlyphs() &&
(COLRFonts::GetGlyphPaintGraph(fe->GetCOLR(), gid) ||
COLRFonts::GetGlyphLayers(fe->GetCOLR(), gid))) {
return true;
}
if (fe->TryGetSVGData(this) && fe->HasSVGGlyph(gid)) {
return true;
}
return false;
}
static void UnionRange(gfxFloat aX, gfxFloat* aDestMin, gfxFloat* aDestMax) {
*aDestMin = std::min(*aDestMin, aX);
*aDestMax = std::max(*aDestMax, aX);
}
// We get precise glyph extents if the textrun creator requested them, or
// if the font is a user font --- in which case the author may be relying
// on overflowing glyphs.
static bool NeedsGlyphExtents(gfxFont* aFont, const gfxTextRun* aTextRun) {
return (aTextRun->GetFlags() &
gfx::ShapedTextFlags::TEXT_NEED_BOUNDING_BOX) ||
aFont->GetFontEntry()->IsUserFont();
}
bool gfxFont::IsSpaceGlyphInvisible(DrawTarget* aRefDrawTarget,
const gfxTextRun* aTextRun) {
gfxFontEntry::LazyFlag flag = mFontEntry->mSpaceGlyphIsInvisible;
if (flag == gfxFontEntry::LazyFlag::Uninitialized &&
GetAdjustedSize() >= 1.0) {
gfxGlyphExtents* extents =
GetOrCreateGlyphExtents(aTextRun->GetAppUnitsPerDevUnit());
gfxRect glyphExtents;
flag = extents->GetTightGlyphExtentsAppUnits(
this, aRefDrawTarget, GetSpaceGlyph(), &glyphExtents) &&
glyphExtents.IsEmpty()
? gfxFontEntry::LazyFlag::Yes
: gfxFontEntry::LazyFlag::No;
mFontEntry->mSpaceGlyphIsInvisible = flag;
}
return flag == gfxFontEntry::LazyFlag::Yes;
}
bool gfxFont::MeasureGlyphs(const gfxTextRun* aTextRun, uint32_t aStart,
uint32_t aEnd, BoundingBoxType aBoundingBoxType,
DrawTarget* aRefDrawTarget, Spacing* aSpacing,
gfxGlyphExtents* aExtents, bool aIsRTL,
bool aNeedsGlyphExtents, RunMetrics& aMetrics,
gfxFloat* aAdvanceMin, gfxFloat* aAdvanceMax) {
const gfxTextRun::CompressedGlyph* charGlyphs =
aTextRun->GetCharacterGlyphs();
double x = 0;
if (aSpacing) {
x += aSpacing[0].mBefore;
}
uint32_t spaceGlyph = GetSpaceGlyph();
bool allGlyphsInvisible = true;
AutoReadLock lock(aExtents->mLock);
for (uint32_t i = aStart; i < aEnd; ++i) {
const gfxTextRun::CompressedGlyph* glyphData = &charGlyphs[i];
if (glyphData->IsSimpleGlyph()) {
double advance = glyphData->GetSimpleAdvance();
uint32_t glyphIndex = glyphData->GetSimpleGlyph();
if (allGlyphsInvisible) {
if (glyphIndex != spaceGlyph) {
allGlyphsInvisible = false;
} else {
gfxFontEntry::LazyFlag flag = mFontEntry->mSpaceGlyphIsInvisible;
if (flag == gfxFontEntry::LazyFlag::Uninitialized &&
GetAdjustedSize() >= 1.0) {
gfxRect glyphExtents;
flag = aExtents->GetTightGlyphExtentsAppUnitsLocked(
this, aRefDrawTarget, spaceGlyph, &glyphExtents) &&
glyphExtents.IsEmpty()
? gfxFontEntry::LazyFlag::Yes
: gfxFontEntry::LazyFlag::No;
mFontEntry->mSpaceGlyphIsInvisible = flag;
}
if (flag == gfxFontEntry::LazyFlag::No) {
allGlyphsInvisible = false;
}
}
}
// Only get the real glyph horizontal extent if we were asked
// for the tight bounding box or we're in quality mode
if (aBoundingBoxType != LOOSE_INK_EXTENTS || aNeedsGlyphExtents) {
uint16_t extentsWidth =
aExtents->GetContainedGlyphWidthAppUnitsLocked(glyphIndex);
if (extentsWidth != gfxGlyphExtents::INVALID_WIDTH &&
aBoundingBoxType == LOOSE_INK_EXTENTS) {
UnionRange(x, aAdvanceMin, aAdvanceMax);
UnionRange(x + extentsWidth, aAdvanceMin, aAdvanceMax);
} else {
gfxRect glyphRect;
if (!aExtents->GetTightGlyphExtentsAppUnitsLocked(
this, aRefDrawTarget, glyphIndex, &glyphRect)) {
glyphRect = gfxRect(0, aMetrics.mBoundingBox.Y(), advance,
aMetrics.mBoundingBox.Height());
}
if (aIsRTL) {
// In effect, swap left and right sidebearings of the glyph, for
// proper accumulation of potentially-overlapping glyph rects.
glyphRect.MoveToX(advance - glyphRect.XMost());
}
glyphRect.MoveByX(x);
aMetrics.mBoundingBox = aMetrics.mBoundingBox.Union(glyphRect);
}
}
x += advance;
} else {
allGlyphsInvisible = false;
uint32_t glyphCount = glyphData->GetGlyphCount();
if (glyphCount > 0) {
const gfxTextRun::DetailedGlyph* details =
aTextRun->GetDetailedGlyphs(i);
NS_ASSERTION(details != nullptr,
"detailedGlyph record should not be missing!");
uint32_t j;
for (j = 0; j < glyphCount; ++j, ++details) {
uint32_t glyphIndex = details->mGlyphID;
double advance = details->mAdvance;
gfxRect glyphRect;
if (glyphData->IsMissing() ||
!aExtents->GetTightGlyphExtentsAppUnitsLocked(
this, aRefDrawTarget, glyphIndex, &glyphRect)) {
// We might have failed to get glyph extents due to
// OOM or something
glyphRect = gfxRect(0, -aMetrics.mAscent, advance,
aMetrics.mAscent + aMetrics.mDescent);
}
if (aIsRTL) {
// Swap left/right sidebearings of the glyph, because we're doing
// mirrored measurement.
glyphRect.MoveToX(advance - glyphRect.XMost());
// Move to current x position, mirroring any x-offset amount.
glyphRect.MoveByX(x - details->mOffset.x);
} else {
glyphRect.MoveByX(x + details->mOffset.x);
}
glyphRect.MoveByY(details->mOffset.y);
aMetrics.mBoundingBox = aMetrics.mBoundingBox.Union(glyphRect);
x += advance;
}
}
}
if (aSpacing) {
double space = aSpacing[i - aStart].mAfter;
if (i + 1 < aEnd) {
space += aSpacing[i + 1 - aStart].mBefore;
}
x += space;
}
}
aMetrics.mAdvanceWidth = x;
return allGlyphsInvisible;
}
bool gfxFont::MeasureGlyphs(const gfxTextRun* aTextRun, uint32_t aStart,
uint32_t aEnd, BoundingBoxType aBoundingBoxType,
DrawTarget* aRefDrawTarget, Spacing* aSpacing,
bool aIsRTL, RunMetrics& aMetrics) {
const gfxTextRun::CompressedGlyph* charGlyphs =
aTextRun->GetCharacterGlyphs();
double x = 0;
if (aSpacing) {
x += aSpacing[0].mBefore;
}
uint32_t spaceGlyph = GetSpaceGlyph();
bool allGlyphsInvisible = true;
for (uint32_t i = aStart; i < aEnd; ++i) {
const gfxTextRun::CompressedGlyph* glyphData = &charGlyphs[i];
if (glyphData->IsSimpleGlyph()) {
double advance = glyphData->GetSimpleAdvance();
uint32_t glyphIndex = glyphData->GetSimpleGlyph();
if (allGlyphsInvisible &&
(glyphIndex != spaceGlyph ||
!IsSpaceGlyphInvisible(aRefDrawTarget, aTextRun))) {
allGlyphsInvisible = false;
}
x += advance;
} else {
allGlyphsInvisible = false;
uint32_t glyphCount = glyphData->GetGlyphCount();
if (glyphCount > 0) {
const gfxTextRun::DetailedGlyph* details =
aTextRun->GetDetailedGlyphs(i);
NS_ASSERTION(details != nullptr,
"detailedGlyph record should not be missing!");
uint32_t j;
for (j = 0; j < glyphCount; ++j, ++details) {
double advance = details->mAdvance;
gfxRect glyphRect(0, -aMetrics.mAscent, advance,
aMetrics.mAscent + aMetrics.mDescent);
if (aIsRTL) {
// Swap left/right sidebearings of the glyph, because we're doing
// mirrored measurement.
glyphRect.MoveToX(advance - glyphRect.XMost());
// Move to current x position, mirroring any x-offset amount.
glyphRect.MoveByX(x - details->mOffset.x);
} else {
glyphRect.MoveByX(x + details->mOffset.x);
}
glyphRect.MoveByY(details->mOffset.y);
aMetrics.mBoundingBox = aMetrics.mBoundingBox.Union(glyphRect);
x += advance;
}
}
}
if (aSpacing) {
double space = aSpacing[i - aStart].mAfter;
if (i + 1 < aEnd) {
space += aSpacing[i + 1 - aStart].mBefore;
}
x += space;
}
}
aMetrics.mAdvanceWidth = x;
return allGlyphsInvisible;
}
gfxFont::RunMetrics gfxFont::Measure(const gfxTextRun* aTextRun,
uint32_t aStart, uint32_t aEnd,
BoundingBoxType aBoundingBoxType,
DrawTarget* aRefDrawTarget,
Spacing* aSpacing,
gfx::ShapedTextFlags aOrientation) {
// If aBoundingBoxType is TIGHT_HINTED_OUTLINE_EXTENTS
// and the underlying cairo font may be antialiased,
// we need to create a copy in order to avoid getting cached extents.
// This is only used by MathML layout at present.
if (aBoundingBoxType == TIGHT_HINTED_OUTLINE_EXTENTS &&
mAntialiasOption != kAntialiasNone) {
gfxFont* nonAA = mNonAAFont;
if (!nonAA) {
nonAA = CopyWithAntialiasOption(kAntialiasNone);
if (nonAA) {
if (!mNonAAFont.compareExchange(nullptr, nonAA)) {
delete nonAA;
nonAA = mNonAAFont;
}
}
}
// if font subclass doesn't implement CopyWithAntialiasOption(),
// it will return null and we'll proceed to use the existing font
if (nonAA) {
return nonAA->Measure(aTextRun, aStart, aEnd,
TIGHT_HINTED_OUTLINE_EXTENTS, aRefDrawTarget,
aSpacing, aOrientation);
}
}
const int32_t appUnitsPerDevUnit = aTextRun->GetAppUnitsPerDevUnit();
// Current position in appunits
Orientation orientation =
aOrientation == gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_UPRIGHT
? nsFontMetrics::eVertical
: nsFontMetrics::eHorizontal;
const gfxFont::Metrics& fontMetrics = GetMetrics(orientation);
gfxFloat baselineOffset = 0;
if (aTextRun->UseCenterBaseline() &&
orientation == nsFontMetrics::eHorizontal) {
// For a horizontal font being used in vertical writing mode with
// text-orientation:mixed, the overall metrics we're accumulating
// will be aimed at a center baseline. But this font's metrics were
// based on the alphabetic baseline. So we compute a baseline offset
// that will be applied to ascent/descent values and glyph rects
// to effectively shift them relative to the baseline.
// XXX Eventually we should probably use the BASE table, if present.
// But it usually isn't, so we need an ad hoc adjustment for now.
baselineOffset =
appUnitsPerDevUnit * (fontMetrics.emAscent - fontMetrics.emDescent) / 2;
}
RunMetrics metrics;
metrics.mAscent = fontMetrics.maxAscent * appUnitsPerDevUnit;
metrics.mDescent = fontMetrics.maxDescent * appUnitsPerDevUnit;
if (aStart == aEnd) {
// exit now before we look at aSpacing[0], which is undefined
metrics.mAscent -= baselineOffset;
metrics.mDescent += baselineOffset;
metrics.mBoundingBox =
gfxRect(0, -metrics.mAscent, 0, metrics.mAscent + metrics.mDescent);
return metrics;
}
gfxFloat advanceMin = 0, advanceMax = 0;
bool isRTL = aTextRun->IsRightToLeft();
bool needsGlyphExtents = NeedsGlyphExtents(this, aTextRun);
gfxGlyphExtents* extents =
((aBoundingBoxType == LOOSE_INK_EXTENTS && !needsGlyphExtents &&
!aTextRun->HasDetailedGlyphs()) ||
MOZ_UNLIKELY(GetStyle()->AdjustedSizeMustBeZero()))
? nullptr
: GetOrCreateGlyphExtents(aTextRun->GetAppUnitsPerDevUnit());
bool allGlyphsInvisible;
if (extents) {
allGlyphsInvisible = MeasureGlyphs(
aTextRun, aStart, aEnd, aBoundingBoxType, aRefDrawTarget, aSpacing,
extents, isRTL, needsGlyphExtents, metrics, &advanceMin, &advanceMax);
} else {
allGlyphsInvisible =
MeasureGlyphs(aTextRun, aStart, aEnd, aBoundingBoxType, aRefDrawTarget,
aSpacing, isRTL, metrics);
}
if (allGlyphsInvisible) {
metrics.mBoundingBox.SetEmpty();
} else if (aBoundingBoxType == LOOSE_INK_EXTENTS) {
UnionRange(metrics.mAdvanceWidth, &advanceMin, &advanceMax);
gfxRect fontBox(advanceMin, -metrics.mAscent, advanceMax - advanceMin,
metrics.mAscent + metrics.mDescent);
metrics.mBoundingBox = metrics.mBoundingBox.Union(fontBox);
}
if (isRTL) {
// Reverse the effect of having swapped each glyph's sidebearings, to get
// the correct sidebearings of the merged bounding box.
metrics.mBoundingBox.MoveToX(metrics.mAdvanceWidth -
metrics.mBoundingBox.XMost());
}
// If the font may be rendered with a fake-italic effect, we need to allow
// for the top-right of the glyphs being skewed to the right, and the
// bottom-left being skewed further left.
gfxFloat skew = SkewForSyntheticOblique();
if (skew != 0.0) {
gfxFloat extendLeftEdge, extendRightEdge;
if (orientation == nsFontMetrics::eVertical) {
// The glyph will actually be skewed vertically, but "left" and "right"
// here refer to line-left (physical top) and -right (bottom), so these
// are still the directions in which we need to extend the box.
extendLeftEdge = skew < 0.0 ? ceil(-skew * metrics.mBoundingBox.XMost())
: ceil(skew * -metrics.mBoundingBox.X());
extendRightEdge = skew < 0.0 ? ceil(-skew * -metrics.mBoundingBox.X())
: ceil(skew * metrics.mBoundingBox.XMost());
} else {
extendLeftEdge = skew < 0.0 ? ceil(-skew * -metrics.mBoundingBox.Y())
: ceil(skew * metrics.mBoundingBox.YMost());
extendRightEdge = skew < 0.0 ? ceil(-skew * metrics.mBoundingBox.YMost())
: ceil(skew * -metrics.mBoundingBox.Y());
}
metrics.mBoundingBox.SetWidth(metrics.mBoundingBox.Width() +
extendLeftEdge + extendRightEdge);
metrics.mBoundingBox.MoveByX(-extendLeftEdge);
}
if (baselineOffset != 0) {
metrics.mAscent -= baselineOffset;
metrics.mDescent += baselineOffset;
metrics.mBoundingBox.MoveByY(baselineOffset);
}
return metrics;
}
bool gfxFont::AgeCachedWords() {
mozilla::AutoWriteLock lock(mLock);
if (mWordCache) {
for (auto it = mWordCache->modIter(); !it.done(); it.next()) {
auto& entry = it.get().value();
if (!entry) {
NS_ASSERTION(entry, "cache entry has no gfxShapedWord!");
it.remove();
} else if (entry->IncrementAge() == kShapedWordCacheMaxAge) {
it.remove();
}
}
return mWordCache->empty();
}
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;
}
// In 8-bit text, there cannot be any cluster-extenders.
static uint8_t IsBoundarySpace(uint8_t aChar, uint8_t aNextChar) {
if (aChar == ' ' || aChar == 0x00A0) {
return aChar;
}
return 0;
}
#ifdef __GNUC__
# define GFX_MAYBE_UNUSED __attribute__((unused))
#else
# define GFX_MAYBE_UNUSED
#endif
template <typename T, typename Func>
bool gfxFont::ProcessShapedWordInternal(
DrawTarget* aDrawTarget, const T* aText, uint32_t aLength, uint32_t aHash,
Script aRunScript, nsAtom* aLanguage, bool aVertical,
int32_t aAppUnitsPerDevUnit, gfx::ShapedTextFlags aFlags,
RoundingFlags aRounding, gfxTextPerfMetrics* aTextPerf GFX_MAYBE_UNUSED,
Func aCallback) {
WordCacheKey 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 (auto entry = mWordCache->lookup(key)) {
entry->value()->ResetAge();
#ifndef RELEASE_OR_BETA
if (aTextPerf) {
// XXX we should make sure this is atomic
aTextPerf->current.wordCacheHit++;
}
#endif
aCallback(entry->value().get());
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.
UniquePtr<gfxShapedWord> newShapedWord(
gfxShapedWord::Create(aText, aLength, aRunScript, aLanguage,
aAppUnitsPerDevUnit, aFlags, aRounding));
if (!newShapedWord) {
NS_WARNING("failed to create gfxShapedWord - expect missing text");
return false;
}
DebugOnly<bool> ok =
ShapeText(aDrawTarget, aText, 0, aLength, aRunScript, aLanguage,
aVertical, aRounding, newShapedWord.get());
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<HashMap<WordCacheKey, UniquePtr<gfxShapedWord>,
WordCacheKey::HashPolicy>>();
} 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();
}
}
// Update key so that it references the text stored in the newShapedWord,
// which is guaranteed to live as long as the hashtable entry.
if ((key.mTextIs8Bit = newShapedWord->TextIs8Bit())) {
key.mText.mSingle = newShapedWord->Text8Bit();
} else {
key.mText.mDouble = newShapedWord->TextUnicode();
}
auto entry = mWordCache->lookupForAdd(key);
// It's unlikely, but maybe another thread got there before us...
if (entry) {
// Use the existing entry; the newShapedWord will be discarded.
entry->value()->ResetAge();
#ifndef RELEASE_OR_BETA
if (aTextPerf) {
aTextPerf->current.wordCacheHit++;
}
#endif
aCallback(entry->value().get());
return true;
}
if (!mWordCache->add(entry, key, std::move(newShapedWord))) {
NS_WARNING("failed to cache gfxShapedWord - expect missing text");
return false;
}
#ifndef RELEASE_OR_BETA
if (aTextPerf) {
aTextPerf->current.wordCacheMiss++;
}
#endif
aCallback(entry->value().get());
}
gfxFontCache::GetCache()->RunWordCacheExpirationTimer();
return true;
}
bool gfxFont::WordCacheKey::HashPolicy::match(const Key& aKey,
const Lookup& aLookup) {
if (aKey.mLength != aLookup.mLength || aKey.mFlags != aLookup.mFlags ||
aKey.mRounding != aLookup.mRounding ||
aKey.mAppUnitsPerDevUnit != aLookup.mAppUnitsPerDevUnit ||
aKey.mScript != aLookup.mScript || aKey.mLanguage != aLookup.mLanguage) {
return false;
}
if (aKey.mTextIs8Bit) {
if (aLookup.mTextIs8Bit) {
return (0 == memcmp(aKey.mText.mSingle, aLookup.mText.mSingle,
aKey.mLength * sizeof(uint8_t)));
}
// The lookup 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 = aKey.mText.mSingle;
const char16_t* s2 = aLookup.mText.mDouble;
const char16_t* s2end = s2 + aKey.mLength;
while (s2 < s2end) {
if (*s1++ != *s2++) {
return false;
}
}
return true;
}
NS_ASSERTION(!(aLookup.mFlags & gfx::ShapedTextFlags::TEXT_IS_8BIT) &&
!aLookup.mTextIs8Bit,
"didn't expect 8-bit text here");
return (0 == memcmp(aKey.mText.mDouble, aLookup.mText.mDouble,
aKey.mLength * sizeof(char16_t)));
}
bool gfxFont::ProcessSingleSpaceShapedWord(
DrawTarget* aDrawTarget, bool aVertical, int32_t aAppUnitsPerDevUnit,
gfx::ShapedTextFlags aFlags, RoundingFlags aRounding,
const std::function<void(gfxShapedWord*)>& aCallback) {
static const uint8_t space = ' ';
return ProcessShapedWordInternal(
aDrawTarget, &space, 1, gfxShapedWord::HashMix(0, ' '), Script::LATIN,
/* aLanguage = */ nullptr, aVertical, aAppUnitsPerDevUnit, aFlags,
aRounding, nullptr, aCallback);
}
bool gfxFont::ShapeText(DrawTarget* aDrawTarget, const uint8_t* aText,
uint32_t aOffset, uint32_t aLength, Script aScript,
nsAtom* aLanguage, bool aVertical,
RoundingFlags aRounding, gfxShapedText* aShapedText) {
nsDependentCSubstring ascii((const char*)aText, aLength);
nsAutoString utf16;
AppendASCIItoUTF16(ascii, utf16);
if (utf16.Length() != aLength) {
return false;
}
return ShapeText(aDrawTarget, utf16.BeginReading(), aOffset, aLength, aScript,
aLanguage, aVertical, aRounding, aShapedText);
}
bool gfxFont::ShapeText(DrawTarget* aDrawTarget, const char16_t* aText,
uint32_t aOffset, uint32_t aLength, Script aScript,
nsAtom* aLanguage, bool aVertical,
RoundingFlags aRounding, gfxShapedText* aShapedText) {
// XXX Currently, we do all vertical shaping through harfbuzz.
// Vertical graphite support may be wanted as a future enhancement.
// XXX Graphite shaping currently only supported on the main thread!
// Worker-thread shaping (offscreen canvas) will always go via harfbuzz.
if (FontCanSupportGraphite() && !aVertical && NS_IsMainThread()) {
if (gfxPlatform::GetPlatform()->UseGraphiteShaping()) {
gfxGraphiteShaper* shaper = mGraphiteShaper;
if (!shaper) {
shaper = new gfxGraphiteShaper(this);
if (mGraphiteShaper.compareExchange(nullptr, shaper)) {
Telemetry::ScalarAdd(Telemetry::ScalarID::BROWSER_USAGE_GRAPHITE, 1);
} else {
delete shaper;
shaper = mGraphiteShaper;
}
}
if (shaper->ShapeText(aDrawTarget, aText, aOffset, aLength, aScript,
aLanguage, aVertical, aRounding, aShapedText)) {
PostShapingFixup(aDrawTarget, aText, aOffset, aLength, aVertical,
aShapedText);
return true;
}
}
}
gfxHarfBuzzShaper* shaper = GetHarfBuzzShaper();
if (shaper &&
shaper->ShapeText(aDrawTarget, aText, aOffset, aLength, aScript,
aLanguage, aVertical, aRounding, aShapedText)) {
PostShapingFixup(aDrawTarget, aText, aOffset, aLength, aVertical,
aShapedText);
if (GetFontEntry()->HasTrackingTable()) {
// Convert font size from device pixels back to CSS px
// to use in selecting tracking value
gfxFloat trackSize = GetAdjustedSize() *
aShapedText->GetAppUnitsPerDevUnit() /
AppUnitsPerCSSPixel();
// Usually, a given font will be used with the same appunit scale, so we
// can cache the tracking value rather than recompute it every time.
{
AutoReadLock lock(mLock);
if (trackSize == mCachedTrackingSize) {
// 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->ApplyTrackingToClusters(mTracking, aOffset, aLength);
return true;
}
}
// We didn't have the appropriate tracking value cached yet.
AutoWriteLock lock(mLock);
if (trackSize != mCachedTrackingSize) {
mCachedTrackingSize = trackSize;
mTracking =
GetFontEntry()->TrackingForCSSPx(trackSize) * mFUnitsConvFactor;
}
aShapedText->ApplyTrackingToClusters(mTracking, aOffset, aLength);
}
return true;
}
NS_WARNING_ASSERTION(false, "shaper failed, expect scrambled/missing text");
return false;
}
void gfxFont::PostShapingFixup(DrawTarget* aDrawTarget, const char16_t* aText,
uint32_t aOffset, uint32_t aLength,
bool aVertical, gfxShapedText* aShapedText) {
if (ApplySyntheticBold()) {
const Metrics& metrics = GetMetrics(aVertical ? nsFontMetrics::eVertical
: nsFontMetrics::eHorizontal);
if (metrics.maxAdvance > metrics.aveCharWidth) {
aShapedText->ApplyTrackingToClusters(GetSyntheticBoldOffset(), aOffset,
aLength);
}
}
}
#define MAX_SHAPING_LENGTH \
32760 // slightly less than 32K, trying to avoid
// over-stressing platform shapers
#define BACKTRACK_LIMIT \
16 // backtrack this far looking for a good place
// to split into fragments for separate shaping
template <typename T>
bool gfxFont::ShapeFragmentWithoutWordCache(DrawTarget* aDrawTarget,
const T* aText, uint32_t aOffset,
uint32_t aLength, Script aScript,
nsAtom* aLanguage, bool aVertical,
RoundingFlags aRounding,
gfxTextRun* aTextRun) {
aTextRun->SetupClusterBoundaries(aOffset, aText, aLength);
bool ok = true;
while (ok && aLength > 0) {
uint32_t fragLen = aLength;
// limit the length of text we pass to shapers in a single call
if (fragLen > MAX_SHAPING_LENGTH) {
fragLen = MAX_SHAPING_LENGTH;
// in the 8-bit case, there are no multi-char clusters,
// so we don't need to do this check
if constexpr (sizeof(T) == sizeof(char16_t)) {
uint32_t i;
for (i = 0; i < BACKTRACK_LIMIT; ++i) {
if (aTextRun->IsClusterStart(aOffset + fragLen - i)) {
fragLen -= i;
break;
}
}
if (i == BACKTRACK_LIMIT) {
// if we didn't find any cluster start while backtracking,
// just check that we're not in the middle of a surrogate
// pair; back up by one code unit if we are.
if (NS_IS_SURROGATE_PAIR(aText[fragLen - 1], aText[fragLen])) {
--fragLen;
}
}
}
}
ok = ShapeText(aDrawTarget, aText, aOffset, fragLen, aScript, aLanguage,
aVertical, aRounding, aTextRun);
aText += fragLen;
aOffset += fragLen;
aLength -= fragLen;
}
return ok;
}
// Check if aCh is an unhandled control character that should be displayed
// as a hexbox rather than rendered by some random font on the system.
// We exclude \r as stray &#13;s are rather common (bug 941940).
// Note that \n and \t don't come through here, as they have specific
// meanings that have already been handled.
static bool IsInvalidControlChar(uint32_t aCh) {
return aCh != '\r' && ((aCh & 0x7f) < 0x20 || aCh == 0x7f);
}
template <typename T>
bool gfxFont::ShapeTextWithoutWordCache(DrawTarget* aDrawTarget, const T* aText,
uint32_t aOffset, uint32_t aLength,
Script aScript, nsAtom* aLanguage,
bool aVertical, RoundingFlags aRounding,
gfxTextRun* aTextRun) {
uint32_t fragStart = 0;
bool ok = true;
for (uint32_t i = 0; i <= aLength && ok; ++i) {
T ch = (i < aLength) ? aText[i] : '\n';
bool invalid = gfxFontGroup::IsInvalidChar(ch);
uint32_t length = i - fragStart;
// break into separate fragments when we hit an invalid char
if (!invalid) {
continue;
}
if (length > 0) {
ok = ShapeFragmentWithoutWordCache(
aDrawTarget, aText + fragStart, aOffset + fragStart, length, aScript,
aLanguage, aVertical, aRounding, aTextRun);
}
if (i == aLength) {
break;
}
// fragment was terminated by an invalid char: skip it,
// unless it's a control char that we want to show as a hexbox,
// but record where TAB or NEWLINE occur
if (ch == '\t') {
aTextRun->SetIsTab(aOffset + i);
} else if (ch == '\n') {
aTextRun->SetIsNewline(aOffset + i);
} else if (GetGeneralCategory(ch) == HB_UNICODE_GENERAL_CATEGORY_FORMAT) {
aTextRun->SetIsFormattingControl(aOffset + i);
} else if (IsInvalidControlChar(ch) &&
!(aTextRun->GetFlags() &
gfx::ShapedTextFlags::TEXT_HIDE_CONTROL_CHARACTERS)) {
if (GetFontEntry()->IsUserFont() && HasCharacter(ch)) {
ShapeFragmentWithoutWordCache(aDrawTarget, aText + i, aOffset + i, 1,
aScript, aLanguage, aVertical, aRounding,
aTextRun);
} else {
aTextRun->SetMissingGlyph(aOffset + i, ch, this);
}
}
fragStart = i + 1;
}
NS_WARNING_ASSERTION(ok, "failed to shape text - expect garbled text");
return ok;
}
#ifndef RELEASE_OR_BETA
# define TEXT_PERF_INCR(tp, m) (tp ? (tp)->current.m++ : 0)
#else
# define TEXT_PERF_INCR(tp, m)
#endif
inline static bool IsChar8Bit(uint8_t /*aCh*/) { return true; }
inline static bool IsChar8Bit(char16_t aCh) { return aCh < 0x100; }
inline static bool HasSpaces(const uint8_t* aString, uint32_t aLen) {
return memchr(aString, 0x20, aLen) != nullptr;
}
inline static bool HasSpaces(const char16_t* aString, uint32_t aLen) {
for (const char16_t* ch = aString; ch < aString + aLen; ch++) {
if (*ch == 0x20) {
return true;
}
}
return false;
}
template <typename T>
bool gfxFont::SplitAndInitTextRun(
DrawTarget* aDrawTarget, gfxTextRun* aTextRun,
const T* aString, // text for this font run
uint32_t aRunStart, // position in the textrun
uint32_t aRunLength, Script aRunScript, nsAtom* aLanguage,
ShapedTextFlags aOrientation) {
if (aRunLength == 0) {
return true;
}
gfxTextPerfMetrics* tp = nullptr;
RoundingFlags rounding = GetRoundOffsetsToPixels(aDrawTarget);
#ifndef RELEASE_OR_BETA
tp = aTextRun->GetFontGroup()->GetTextPerfMetrics();
if (tp) {
if (mStyle.systemFont) {
tp->current.numChromeTextRuns++;
} else {
tp->current.numContentTextRuns++;
}
tp->current.numChars += aRunLength;
if (aRunLength > tp->current.maxTextRunLen) {
tp->current.maxTextRunLen = aRunLength;
}
}
#endif
uint32_t wordCacheCharLimit =
gfxPlatform::GetPlatform()->WordCacheCharLimit();
bool vertical = aOrientation == ShapedTextFlags::TEXT_ORIENT_VERTICAL_UPRIGHT;
// If spaces can participate in shaping (e.g. within lookups for automatic
// fractions), need to shape without using the word cache which segments
// textruns on space boundaries. Word cache can be used if the textrun
// is short enough to fit in the word cache and it lacks spaces.
tainted_boolean_hint t_canParticipate =
SpaceMayParticipateInShaping(aRunScript);
bool canParticipate = t_canParticipate.unverified_safe_because(
"We need to ensure that this function operates safely independent of "
"t_canParticipate. The worst that can happen here is that the decision "
"to use the cache is incorrectly made, resulting in a bad "
"rendering/slowness. However, this would not compromise the memory "
"safety of Firefox in any way, and can thus be permitted");
if (canParticipate) {
if (aRunLength > wordCacheCharLimit || HasSpaces(aString, aRunLength)) {
TEXT_PERF_INCR(tp, wordCacheSpaceRules);
return ShapeTextWithoutWordCache(aDrawTarget, aString, aRunStart,
aRunLength, aRunScript, aLanguage,
vertical, rounding, aTextRun);
}
}
// the only flags we care about for ShapedWord construction/caching
gfx::ShapedTextFlags flags = aTextRun->GetFlags();
flags &= (gfx::ShapedTextFlags::TEXT_IS_RTL |
gfx::ShapedTextFlags::TEXT_DISABLE_OPTIONAL_LIGATURES |
gfx::ShapedTextFlags::TEXT_USE_MATH_SCRIPT |
gfx::ShapedTextFlags::TEXT_ORIENT_MASK);
if constexpr (sizeof(T) == sizeof(uint8_t)) {
flags |= gfx::ShapedTextFlags::TEXT_IS_8BIT;
}
uint32_t wordStart = 0;
uint32_t hash = 0;
bool wordIs8Bit = true;
int32_t appUnitsPerDevUnit = aTextRun->GetAppUnitsPerDevUnit();
T nextCh = aString[0];
for (uint32_t i = 0; i <= aRunLength; ++i) {
T ch = nextCh;
nextCh = (i < aRunLength - 1) ? aString[i + 1] : '\n';
T boundary = IsBoundarySpace(ch, nextCh);
bool invalid = !boundary && gfxFontGroup::IsInvalidChar(ch);
uint32_t length = i - wordStart;
// break into separate ShapedWords when we hit an invalid char,
// or a boundary space (always handled individually),
// or the first non-space after a space
if (!boundary && !invalid) {
if (!IsChar8Bit(ch)) {
wordIs8Bit = false;
}
// include this character in the hash, and move on to next
hash = gfxShapedWord::HashMix(hash, ch);
continue;
}
// We've decided to break here (i.e. we're at the end of a "word");
// shape the word and add it to the textrun.
// For words longer than the limit, we don't use the
// font's word cache but just shape directly into the textrun.
if (length > wordCacheCharLimit) {
TEXT_PERF_INCR(tp, wordCacheLong);
bool ok = ShapeFragmentWithoutWordCache(
aDrawTarget, aString + wordStart, aRunStart + wordStart, length,
aRunScript, aLanguage, vertical, rounding, aTextRun);
if (!ok) {
return false;
}
} else if (length > 0) {
gfx::ShapedTextFlags wordFlags = flags;
// in the 8-bit version of this method, TEXT_IS_8BIT was
// already set as part of |flags|, so no need for a per-word
// adjustment here
if (sizeof(T) == sizeof(char16_t)) {
if (wordIs8Bit) {
wordFlags |= gfx::ShapedTextFlags::TEXT_IS_8BIT;
}
}
bool processed = ProcessShapedWordInternal(
aDrawTarget, aString + wordStart, length, hash, aRunScript, aLanguage,
vertical, appUnitsPerDevUnit, wordFlags, rounding, tp,
[&](gfxShapedWord* aShapedWord) {
aTextRun->CopyGlyphDataFrom(aShapedWord, aRunStart + wordStart);
});
if (!processed) {
return false; // failed, presumably out of memory?
}
}
if (boundary) {
// word was terminated by a space: add that to the textrun
MOZ_ASSERT(aOrientation != ShapedTextFlags::TEXT_ORIENT_VERTICAL_MIXED,
"text-orientation:mixed should be resolved earlier");
if (boundary != ' ' || !aTextRun->SetSpaceGlyphIfSimple(
this, aRunStart + i, ch, aOrientation)) {
// Currently, the only "boundary" characters we recognize are
// space and no-break space, which are both 8-bit, so we force
// that flag (below). If we ever change IsBoundarySpace, we
// may need to revise this.
// Avoid tautological-constant-out-of-range-compare in 8-bit:
DebugOnly<char16_t> boundary16 = boundary;
NS_ASSERTION(boundary16 < 256, "unexpected boundary!");
bool processed = ProcessShapedWordInternal(
aDrawTarget, &boundary, 1, gfxShapedWord::HashMix(0, boundary),
aRunScript, aLanguage, vertical, appUnitsPerDevUnit,
flags | gfx::ShapedTextFlags::TEXT_IS_8BIT, rounding, tp,
[&](gfxShapedWord* aShapedWord) {
aTextRun->CopyGlyphDataFrom(aShapedWord, aRunStart + i);
if (boundary == ' ') {
aTextRun->GetCharacterGlyphs()[aRunStart + i].SetIsSpace();
}
});
if (!processed) {
return false;
}
}
hash = 0;
wordStart = i + 1;
wordIs8Bit = true;
continue;
}
if (i == aRunLength) {
break;
}
NS_ASSERTION(invalid, "how did we get here except via an invalid char?");
// word was terminated by an invalid char: skip it,
// unless it's a control char that we want to show as a hexbox,
// but record where TAB or NEWLINE occur
if (ch == '\t') {
aTextRun->SetIsTab(aRunStart + i);
} else if (ch == '\n') {
aTextRun->SetIsNewline(aRunStart + i);
} else if (GetGeneralCategory(ch) == HB_UNICODE_GENERAL_CATEGORY_FORMAT) {
aTextRun->SetIsFormattingControl(aRunStart + i);
} else if (IsInvalidControlChar(ch) &&
!(aTextRun->GetFlags() &
gfx::ShapedTextFlags::TEXT_HIDE_CONTROL_CHARACTERS)) {
if (GetFontEntry()->IsUserFont() && HasCharacter(ch)) {
ShapeFragmentWithoutWordCache(aDrawTarget, aString + i, aRunStart + i,
1, aRunScript, aLanguage, vertical,
rounding, aTextRun);
} else {
aTextRun->SetMissingGlyph(aRunStart + i, ch, this);
}
}
hash = 0;
wordStart = i + 1;
wordIs8Bit = true;
}
return true;
}
// Explicit instantiations of SplitAndInitTextRun, to avoid libxul link failure
template bool gfxFont::SplitAndInitTextRun(
DrawTarget* aDrawTarget, gfxTextRun* aTextRun, const uint8_t* aString,
uint32_t aRunStart, uint32_t aRunLength, Script aRunScript,
nsAtom* aLanguage, ShapedTextFlags aOrientation);
template bool gfxFont::SplitAndInitTextRun(
DrawTarget* aDrawTarget, gfxTextRun* aTextRun, const char16_t* aString,
uint32_t aRunStart, uint32_t aRunLength, Script aRunScript,
nsAtom* aLanguage, ShapedTextFlags aOrientation);
template <>
bool gfxFont::InitFakeSmallCapsRun(
nsPresContext* aPresContext, DrawTarget* aDrawTarget, gfxTextRun* aTextRun,
const char16_t* aText, uint32_t aOffset, uint32_t aLength,
FontMatchType aMatchType, gfx::ShapedTextFlags aOrientation, Script aScript,
nsAtom* aLanguage, bool aSyntheticLower, bool aSyntheticUpper) {
bool ok = true;
RefPtr<gfxFont> smallCapsFont = GetSmallCapsFont();
if (!smallCapsFont) {
NS_WARNING("failed to get reduced-size font for smallcaps!");
smallCapsFont = this;
}
bool isCJK = gfxTextRun::IsCJKScript(aScript);
enum RunCaseAction { kNoChange, kUppercaseReduce, kUppercase };
RunCaseAction runAction = kNoChange;
uint32_t runStart = 0;
for (uint32_t i = 0; i <= aLength; ++i) {
uint32_t extraCodeUnits = 0; // Will be set to 1 if we need to consume
// a trailing surrogate as well as the
// current code unit.
RunCaseAction chAction = kNoChange;
// Unless we're at the end, figure out what treatment the current
// character will need.
if (i < aLength) {
uint32_t ch = aText[i];
if (i < aLength - 1 && NS_IS_SURROGATE_PAIR(ch, aText[i + 1])) {
ch = SURROGATE_TO_UCS4(ch, aText[i + 1]);
extraCodeUnits = 1;
}
// Characters that aren't the start of a cluster are ignored here.
// They get added to whatever lowercase/non-lowercase run we're in.
if (IsClusterExtender(ch)) {
chAction = runAction;
} else {
if (ch != ToUpperCase(ch) || SpecialUpper(ch)) {
// ch is lower case
chAction = (aSyntheticLower ? kUppercaseReduce : kNoChange);
} else if (ch != ToLowerCase(ch)) {
// ch is upper case
chAction = (aSyntheticUpper ? kUppercaseReduce : kNoChange);
if (aLanguage == nsGkAtoms::el) {
// In Greek, check for characters that will be modified by
// the GreekUpperCase mapping - this catches accented
// capitals where the accent is to be removed (bug 307039).
// These are handled by using the full-size font with the
// uppercasing transform.
mozilla::GreekCasing::State state;
bool markEta, updateEta;
uint32_t ch2 =
mozilla::GreekCasing::UpperCase(ch, state, markEta, updateEta);
if ((ch != ch2 || markEta) && !aSyntheticUpper) {
chAction = kUppercase;
}
}
}
}
}
// At the end of the text or when the current character needs different
// casing treatment from the current run, finish the run-in-progress
// and prepare to accumulate a new run.
// Note that we do not look at any source data for offset [i] here,
// as that would be invalid in the case where i==length.
if ((i == aLength || runAction != chAction) && runStart < i) {
uint32_t runLength = i - runStart;
gfxFont* f = this;
switch (runAction) {
case kNoChange:
// just use the current font and the existing string
aTextRun->AddGlyphRun(f, aMatchType, aOffset + runStart, true,
aOrientation, isCJK);
if (!f->SplitAndInitTextRun(aDrawTarget, aTextRun, aText + runStart,
aOffset + runStart, runLength, aScript,
aLanguage, aOrientation)) {
ok = false;
}
break;
case kUppercaseReduce:
// use reduced-size font, then fall through to uppercase the text
f = smallCapsFont;
[[fallthrough]];
case kUppercase:
// apply uppercase transform to the string
nsDependentSubstring origString(aText + runStart, runLength);
nsAutoString convertedString;
AutoTArray<bool, 50> charsToMergeArray;
AutoTArray<bool, 50> deletedCharsArray;
StyleTextTransform globalTransform{StyleTextTransformCase::Uppercase,
{}};
// No mask needed; we're doing case conversion, not password-hiding.
const char16_t maskChar = 0;
bool mergeNeeded = nsCaseTransformTextRunFactory::TransformString(
origString, convertedString, Some(globalTransform), maskChar,
/* aCaseTransformsOnly = */ false, aLanguage, charsToMergeArray,
deletedCharsArray);
// Check whether the font supports the uppercased characters needed;
// if not, we're not going to be able to simulate small-caps.
bool failed = false;
char16_t highSurrogate = 0;
for (const char16_t* cp = convertedString.BeginReading();
cp != convertedString.EndReading(); ++cp) {
if (NS_IS_HIGH_SURROGATE(*cp)) {
highSurrogate = *cp;
continue;
}
uint32_t ch = *cp;
if (NS_IS_LOW_SURROGATE(*cp) && highSurrogate) {
ch = SURROGATE_TO_UCS4(highSurrogate, *cp);
}
highSurrogate = 0;
if (!f->HasCharacter(ch)) {
if (IsDefaultIgnorable(ch)) {
continue;
}
failed = true;
break;
}
}
// Required uppercase letter(s) missing from the font. Just use the
// original text with the original font, no fake small caps!
if (failed) {
convertedString = origString;
mergeNeeded = false;
f = this;
}
if (mergeNeeded) {
// This is the hard case: the transformation caused chars
// to be inserted or deleted, so we can't shape directly
// into the destination textrun but have to handle the
// mismatch of character positions.
gfxTextRunFactory::Parameters params = {
aDrawTarget, nullptr, nullptr,
nullptr, 0, aTextRun->GetAppUnitsPerDevUnit()};
RefPtr<gfxTextRun> tempRun(gfxTextRun::Create(
&params, convertedString.Length(), aTextRun->GetFontGroup(),
gfx::ShapedTextFlags(), nsTextFrameUtils::Flags()));
tempRun->AddGlyphRun(f, aMatchType, 0, true, aOrientation, isCJK);
if (!f->SplitAndInitTextRun(aDrawTarget, tempRun.get(),
convertedString.BeginReading(), 0,
convertedString.Length(), aScript,
aLanguage, aOrientation)) {
ok = false;
} else {
RefPtr<gfxTextRun> mergedRun(gfxTextRun::Create(
&params, runLength, aTextRun->GetFontGroup(),
gfx::ShapedTextFlags(), nsTextFrameUtils::Flags()));
MergeCharactersInTextRun(mergedRun.get(), tempRun.get(),
charsToMergeArray.Elements(),
deletedCharsArray.Elements());
gfxTextRun::Range runRange(0, runLength);
aTextRun->CopyGlyphDataFrom(mergedRun.get(), runRange,
aOffset + runStart);
}
} else {
aTextRun->AddGlyphRun(f, aMatchType, aOffset + runStart, true,
aOrientation, isCJK);
if (!f->SplitAndInitTextRun(aDrawTarget, aTextRun,
convertedString.BeginReading(),
aOffset + runStart, runLength, aScript,
aLanguage, aOrientation)) {
ok = false;
}
}
break;
}
runStart = i;
}
i += extraCodeUnits;
if (i < aLength) {
runAction = chAction;
}
}
return ok;
}
template <>
bool gfxFont::InitFakeSmallCapsRun(
nsPresContext* aPresContext, DrawTarget* aDrawTarget, gfxTextRun* aTextRun,
const uint8_t* aText, uint32_t aOffset, uint32_t aLength,
FontMatchType aMatchType, gfx::ShapedTextFlags aOrientation, Script aScript,
nsAtom* aLanguage, bool aSyntheticLower, bool aSyntheticUpper) {
NS_ConvertASCIItoUTF16 unicodeString(reinterpret_cast<const char*>(aText),
aLength);
return InitFakeSmallCapsRun(aPresContext, aDrawTarget, aTextRun,
static_cast<const char16_t*>(unicodeString.get()),
aOffset, aLength, aMatchType, aOrientation,
aScript, aLanguage, aSyntheticLower,
aSyntheticUpper);
}
already_AddRefed<gfxFont> gfxFont::GetSmallCapsFont() const {
gfxFontStyle style(*GetStyle());
style.size *= SMALL_CAPS_SCALE_FACTOR;
style.variantCaps = NS_FONT_VARIANT_CAPS_NORMAL;
gfxFontEntry* fe = GetFontEntry();
return fe->FindOrMakeFont(&style, mUnicodeRangeMap);
}
already_AddRefed<gfxFont> gfxFont::GetSubSuperscriptFont(
int32_t aAppUnitsPerDevPixel) const {
gfxFontStyle style(*GetStyle());
style.AdjustForSubSuperscript(aAppUnitsPerDevPixel);
gfxFontEntry* fe = GetFontEntry();
return fe->FindOrMakeFont(&style, mUnicodeRangeMap);
}
gfxGlyphExtents* gfxFont::GetOrCreateGlyphExtents(int32_t aAppUnitsPerDevUnit) {
uint32_t readCount;
{
AutoReadLock lock(mLock);
readCount = mGlyphExtentsArray.Length();
for (uint32_t i = 0; i < readCount; ++i) {
if (mGlyphExtentsArray[i]->GetAppUnitsPerDevUnit() == aAppUnitsPerDevUnit)
return mGlyphExtentsArray[i].get();
}
}
AutoWriteLock lock(mLock);
// Re-check in case of race.
uint32_t count = mGlyphExtentsArray.Length();
for (uint32_t i = readCount; i < count; ++i) {
if (mGlyphExtentsArray[i]->GetAppUnitsPerDevUnit() == aAppUnitsPerDevUnit)
return mGlyphExtentsArray[i].get();
}
gfxGlyphExtents* glyphExtents = new gfxGlyphExtents(aAppUnitsPerDevUnit);
if (glyphExtents) {
mGlyphExtentsArray.AppendElement(glyphExtents);
// Initialize the extents of a space glyph, assuming that spaces don't
// render anything!
glyphExtents->SetContainedGlyphWidthAppUnits(GetSpaceGlyph(), 0);
}
return glyphExtents;
}
void gfxFont::SetupGlyphExtents(DrawTarget* aDrawTarget, uint32_t aGlyphID,
bool aNeedTight, gfxGlyphExtents* aExtents) {
gfxRect svgBounds;
if (mFontEntry->TryGetSVGData(this) && mFontEntry->HasSVGGlyph(aGlyphID) &&
mFontEntry->GetSVGGlyphExtents(aDrawTarget, aGlyphID, GetAdjustedSize(),
&svgBounds)) {
gfxFloat d2a = aExtents->GetAppUnitsPerDevUnit();
aExtents->SetTightGlyphExtents(
aGlyphID, gfxRect(svgBounds.X() * d2a, svgBounds.Y() * d2a,
svgBounds.Width() * d2a, svgBounds.Height() * d2a));
return;
}
if (mFontEntry->TryGetColorGlyphs() && mFontEntry->mCOLR &&
COLRFonts::GetColrTableVersion(mFontEntry->mCOLR) == 1) {
auto* shaper = GetHarfBuzzShaper();
if (shaper && shaper->IsInitialized()) {
RefPtr scaledFont = GetScaledFont(aDrawTarget);
Rect r = COLRFonts::GetColorGlyphBounds(
mFontEntry->mCOLR, shaper->GetHBFont(), aGlyphID, aDrawTarget,
scaledFont, mFUnitsConvFactor);
if (!r.IsEmpty()) {
gfxFloat d2a = aExtents->GetAppUnitsPerDevUnit();
aExtents->SetTightGlyphExtents(
aGlyphID, gfxRect(r.X() * d2a, r.Y() * d2a, r.Width() * d2a,
r.Height() * d2a));
return;
}
}
}
gfxRect bounds;
GetGlyphBounds(aGlyphID, &bounds, mAntialiasOption == kAntialiasNone);
const Metrics& fontMetrics = GetMetrics(nsFontMetrics::eHorizontal);
int32_t appUnitsPerDevUnit = aExtents->GetAppUnitsPerDevUnit();
if (!aNeedTight && bounds.x >= 0.0 && bounds.y >= -fontMetrics.maxAscent &&
bounds.height + bounds.y <= fontMetrics.maxDescent) {
uint32_t appUnitsWidth =
uint32_t(ceil((bounds.x + bounds.width) * appUnitsPerDevUnit));
if (appUnitsWidth < gfxGlyphExtents::INVALID_WIDTH) {
aExtents->SetContainedGlyphWidthAppUnits(aGlyphID,
uint16_t(appUnitsWidth));
return;
}
}
#ifdef DEBUG_TEXT_RUN_STORAGE_METRICS
if (!aNeedTight) {
++gGlyphExtentsSetupFallBackToTight;
}
#endif
gfxFloat d2a = appUnitsPerDevUnit;
aExtents->SetTightGlyphExtents(
aGlyphID, gfxRect(bounds.x * d2a, bounds.y * d2a, bounds.width * d2a,
bounds.height * d2a));
}
// Try to initialize font metrics by reading sfnt tables directly;
// set mIsValid=TRUE and return TRUE on success.
// Return FALSE if the gfxFontEntry subclass does not
// implement GetFontTable(), or for non-sfnt fonts where tables are
// not available.
// If this returns TRUE without setting the mIsValid flag, then we -did-
// apparently find an sfnt, but it was too broken to be used.
bool gfxFont::InitMetricsFromSfntTables(Metrics& aMetrics) {
mIsValid = false; // font is NOT valid in case of early return
const uint32_t kHheaTableTag = TRUETYPE_TAG('h', 'h', 'e', 'a');
const uint32_t kOS_2TableTag = TRUETYPE_TAG('O', 'S', '/', '2');
uint32_t len;
if (mFUnitsConvFactor < 0.0) {
// If the conversion factor from FUnits is not yet set,
// get the unitsPerEm from the 'head' table via the font entry
uint16_t unitsPerEm = GetFontEntry()->UnitsPerEm();
if (unitsPerEm == gfxFontEntry::kInvalidUPEM) {
return false;
}
mFUnitsConvFactor = GetAdjustedSize() / unitsPerEm;
}
// 'hhea' table is required for the advanceWidthMax field
gfxFontEntry::AutoTable hheaTable(mFontEntry, kHheaTableTag);
if (!hheaTable) {
return false; // no 'hhea' table -> not an sfnt
}
const MetricsHeader* hhea =
reinterpret_cast<const MetricsHeader*>(hb_blob_get_data(hheaTable, &len));
if (len < sizeof(MetricsHeader)) {
return false;
}
#define SET_UNSIGNED(field, src) \
aMetrics.field = uint16_t(src) * mFUnitsConvFactor
#define SET_SIGNED(field, src) aMetrics.field = int16_t(src) * mFUnitsConvFactor
SET_UNSIGNED(maxAdvance, hhea->advanceWidthMax);
// 'OS/2' table is optional, if not found we'll estimate xHeight
// and aveCharWidth by measuring glyphs
gfxFontEntry::AutoTable os2Table(mFontEntry, kOS_2TableTag);
if (os2Table) {
const OS2Table* os2 =
reinterpret_cast<const OS2Table*>(hb_blob_get_data(os2Table, &len));
// this should always be present in any valid OS/2 of any version
if (len >= offsetof(OS2Table, xAvgCharWidth) + sizeof(int16_t)) {
SET_SIGNED(aveCharWidth, os2->xAvgCharWidth);
}
}
#undef SET_SIGNED
#undef SET_UNSIGNED
hb_font_t* hbFont = gfxHarfBuzzShaper::CreateHBFont(this);
hb_position_t position;
auto FixedToFloat = [](hb_position_t f) -> gfxFloat { return f / 65536.0; };
if (hb_ot_metrics_get_position(hbFont, HB_OT_METRICS_TAG_HORIZONTAL_ASCENDER,
&position)) {
aMetrics.maxAscent = FixedToFloat(position);
}
if (hb_ot_metrics_get_position(hbFont, HB_OT_METRICS_TAG_HORIZONTAL_DESCENDER,
&position)) {
aMetrics.maxDescent = -FixedToFloat(position);
}
if (hb_ot_metrics_get_position(hbFont, HB_OT_METRICS_TAG_HORIZONTAL_LINE_GAP,
&position)) {
aMetrics.externalLeading = FixedToFloat(position);
}
if (hb_ot_metrics_get_position(hbFont, HB_OT_METRICS_TAG_UNDERLINE_OFFSET,
&position)) {
aMetrics.underlineOffset = FixedToFloat(position);
}
if (hb_ot_metrics_get_position(hbFont, HB_OT_METRICS_TAG_UNDERLINE_SIZE,
&position)) {
aMetrics.underlineSize = FixedToFloat(position);
}
if (hb_ot_metrics_get_position(hbFont, HB_OT_METRICS_TAG_STRIKEOUT_OFFSET,
&position)) {
aMetrics.strikeoutOffset = FixedToFloat(position);
}
if (hb_ot_metrics_get_position(hbFont, HB_OT_METRICS_TAG_STRIKEOUT_SIZE,
&position)) {
aMetrics.strikeoutSize = FixedToFloat(position);
}
// Although sxHeight and sCapHeight are signed fields, we consider
// zero/negative values to be erroneous and just ignore them.
if (hb_ot_metrics_get_position(hbFont, HB_OT_METRICS_TAG_X_HEIGHT,
&position) &&
position > 0) {
aMetrics.xHeight = FixedToFloat(position);
}
if (hb_ot_metrics_get_position(hbFont, HB_OT_METRICS_TAG_CAP_HEIGHT,
&position) &&
position > 0) {
aMetrics.capHeight = FixedToFloat(position);
}
hb_font_destroy(hbFont);
mIsValid = true;
return true;
}
static double RoundToNearestMultiple(double aValue, double aFraction) {
return floor(aValue / aFraction + 0.5) * aFraction;
}
void gfxFont::CalculateDerivedMetrics(Metrics& aMetrics) {
aMetrics.maxAscent =
ceil(RoundToNearestMultiple(aMetrics.maxAscent, 1 / 1024.0));
aMetrics.maxDescent =
ceil(RoundToNearestMultiple(aMetrics.maxDescent, 1 / 1024.0));
if (aMetrics.xHeight <= 0) {
// only happens if we couldn't find either font metrics
// or a char to measure;
// pick an arbitrary value that's better than zero
aMetrics.xHeight = aMetrics.maxAscent * DEFAULT_XHEIGHT_FACTOR;
}
// If we have a font that doesn't provide a capHeight value, use maxAscent
// as a reasonable fallback.
if (aMetrics.capHeight <= 0) {
aMetrics.capHeight = aMetrics.maxAscent;
}
aMetrics.maxHeight = aMetrics.maxAscent + aMetrics.maxDescent;
if (aMetrics.maxHeight - aMetrics.emHeight > 0.0) {
aMetrics.internalLeading = aMetrics.maxHeight - aMetrics.emHeight;
} else {
aMetrics.internalLeading = 0.0;
}
aMetrics.emAscent =
aMetrics.maxAscent * aMetrics.emHeight / aMetrics.maxHeight;
aMetrics.emDescent = aMetrics.emHeight - aMetrics.emAscent;
if (GetFontEntry()->IsFixedPitch()) {
// Some Quartz fonts are fixed pitch, but there's some glyph with a bigger
// advance than the average character width... this forces
// those fonts to be recognized like fixed pitch fonts by layout.
aMetrics.maxAdvance = aMetrics.aveCharWidth;
}
if (!aMetrics.strikeoutOffset) {
aMetrics.strikeoutOffset = aMetrics.xHeight * 0.5;
}
if (!aMetrics.strikeoutSize) {
aMetrics.strikeoutSize = aMetrics.underlineSize;
}
}
void gfxFont::SanitizeMetrics(gfxFont::Metrics* aMetrics,
bool aIsBadUnderlineFont) {
// Even if this font size is zero, this font is created with non-zero size.
// However, for layout and others, we should return the metrics of zero size
// font.
if (mStyle.AdjustedSizeMustBeZero()) {
memset(aMetrics, 0, sizeof(gfxFont::Metrics));
return;
}
// If the font entry has ascent/descent/lineGap-override values,
// replace the metrics from the font with the overrides.
gfxFloat adjustedSize = GetAdjustedSize();
if (mFontEntry->mAscentOverride >= 0.0) {
aMetrics->maxAscent = mFontEntry->mAscentOverride * adjustedSize;
aMetrics->maxHeight = aMetrics->maxAscent + aMetrics->maxDescent;
aMetrics->internalLeading =
std::max(0.0, aMetrics->maxHeight - aMetrics->emHeight);
}
if (mFontEntry->mDescentOverride >= 0.0) {
aMetrics->maxDescent = mFontEntry->mDescentOverride * adjustedSize;
aMetrics->maxHeight = aMetrics->maxAscent + aMetrics->maxDescent;
aMetrics->internalLeading =
std::max(0.0, aMetrics->maxHeight - aMetrics->emHeight);
}
if (mFontEntry->mLineGapOverride >= 0.0) {
aMetrics->externalLeading = mFontEntry->mLineGapOverride * adjustedSize;
}
aMetrics->underlineSize = std::max(1.0, aMetrics->underlineSize);
aMetrics->strikeoutSize = std::max(1.0, aMetrics->strikeoutSize);
aMetrics->underlineOffset = std::min(aMetrics->underlineOffset, -1.0);
if (aMetrics->maxAscent < 1.0) {
// We cannot draw strikeout line and overline in the ascent...
aMetrics->underlineSize = 0;
aMetrics->underlineOffset = 0;
aMetrics->strikeoutSize = 0;
aMetrics->strikeoutOffset = 0;
return;
}
/**
* Some CJK fonts have bad underline offset. Therefore, if this is such font,
* we need to lower the underline offset to bottom of *em* descent.
* However, if this is system font, we should not do this for the rendering
* compatibility with another application's UI on the platform.
* XXX Should not use this hack if the font size is too small?
* Such text cannot be read, this might be used for tight CSS
* rendering? (E.g., Acid2)
*/
if (!mStyle.systemFont && aIsBadUnderlineFont) {
// First, we need 2 pixels between baseline and underline at least. Because
// many CJK characters put their glyphs on the baseline, so, 1 pixel is too
// close for CJK characters.
aMetrics->underlineOffset = std::min(aMetrics->underlineOffset, -2.0);
// Next, we put the underline to bottom of below of the descent space.
if (aMetrics->internalLeading + aMetrics->externalLeading >
aMetrics->underlineSize) {
aMetrics->underlineOffset =
std::min(aMetrics->underlineOffset, -aMetrics->emDescent);
} else {
aMetrics->underlineOffset =
std::min(aMetrics->underlineOffset,
aMetrics->underlineSize - aMetrics->emDescent);
}
}
// If underline positioned is too far from the text, descent position is
// preferred so that underline will stay within the boundary.
else if (aMetrics->underlineSize - aMetrics->underlineOffset >
aMetrics->maxDescent) {
if (aMetrics->underlineSize > aMetrics->maxDescent)
aMetrics->underlineSize = std::max(aMetrics->maxDescent, 1.0);
// The max underlineOffset is 1px (the min underlineSize is 1px, and min
// maxDescent is 0px.)
aMetrics->underlineOffset = aMetrics->underlineSize - aMetrics->maxDescent;
}
// If strikeout line is overflowed from the ascent, the line should be resized
// and moved for that being in the ascent space. Note that the strikeoutOffset
// is *middle* of the strikeout line position.
gfxFloat halfOfStrikeoutSize = floor(aMetrics->strikeoutSize / 2.0 + 0.5);
if (halfOfStrikeoutSize + aMetrics->strikeoutOffset > aMetrics->maxAscent) {
if (aMetrics->strikeoutSize > aMetrics->maxAscent) {
aMetrics->strikeoutSize = std::max(aMetrics->maxAscent, 1.0);
halfOfStrikeoutSize = floor(aMetrics->strikeoutSize / 2.0 + 0.5);
}
gfxFloat ascent = floor(aMetrics->maxAscent + 0.5);
aMetrics->strikeoutOffset = std::max(halfOfStrikeoutSize, ascent / 2.0);
}
// If overline is larger than the ascent, the line should be resized.
if (aMetrics->underlineSize > aMetrics->maxAscent) {
aMetrics->underlineSize = aMetrics->maxAscent;
}
}
gfxFont::Baselines gfxFont::GetBaselines(Orientation aOrientation) {
// Approximated baselines for fonts lacking actual baseline data. These are
// fractions of the em ascent/descent from the alphabetic baseline.
const double kHangingBaselineDefault = 0.8; // fraction of ascent
const double kIdeographicBaselineDefault = -0.5; // fraction of descent
// If no BASE table is present, just return synthetic values immediately.
if (!mFontEntry->HasFontTable(TRUETYPE_TAG('B', 'A', 'S', 'E'))) {
// No baseline table; just synthesize them immediately.
const Metrics& metrics = GetMetrics(aOrientation);
return Baselines{
0.0, // alphabetic
kHangingBaselineDefault * metrics.emAscent, // hanging
kIdeographicBaselineDefault * metrics.emDescent // ideographic
};
}
// Use harfbuzz to try to read the font's baseline metrics.
Baselines result{NAN, NAN, NAN};
hb_font_t* hbFont = gfxHarfBuzzShaper::CreateHBFont(this);
hb_direction_t hbDir = aOrientation == nsFontMetrics::eHorizontal
? HB_DIRECTION_LTR
: HB_DIRECTION_TTB;
hb_position_t position;
unsigned count = 0;
auto Fix2Float = [](hb_position_t f) -> gfxFloat { return f / 65536.0; };
if (hb_ot_layout_get_baseline(hbFont, HB_OT_LAYOUT_BASELINE_TAG_ROMAN, hbDir,
HB_OT_TAG_DEFAULT_SCRIPT,
HB_OT_TAG_DEFAULT_LANGUAGE, &position)) {
result.mAlphabetic = Fix2Float(position);
count++;
}
if (hb_ot_layout_get_baseline(hbFont, HB_OT_LAYOUT_BASELINE_TAG_HANGING,
hbDir, HB_OT_TAG_DEFAULT_SCRIPT,
HB_OT_TAG_DEFAULT_LANGUAGE, &position)) {
result.mHanging = Fix2Float(position);
count++;
}
if (hb_ot_layout_get_baseline(
hbFont, HB_OT_LAYOUT_BASELINE_TAG_IDEO_EMBOX_BOTTOM_OR_LEFT, hbDir,
HB_OT_TAG_DEFAULT_SCRIPT, HB_OT_TAG_DEFAULT_LANGUAGE, &position)) {
result.mIdeographic = Fix2Float(position);
count++;
}
hb_font_destroy(hbFont);
// If we successfully read all three, we can return now.
if (count == 3) {
return result;
}
// Synthesize the baselines that we didn't find in the font.
const Metrics& metrics = GetMetrics(aOrientation);
if (std::isnan(result.mAlphabetic)) {
result.mAlphabetic = 0.0;
}
if (std::isnan(result.mHanging)) {
result.mHanging = kHangingBaselineDefault * metrics.emAscent;
}
if (std::isnan(result.mIdeographic)) {
result.mIdeographic = kIdeographicBaselineDefault * metrics.emDescent;
}
return result;
}
// Create a Metrics record to be used for vertical layout. This should never
// fail, as we've already decided this is a valid font. We do not have the
// option of marking it invalid (as can happen if we're unable to read
// horizontal metrics), because that could break a font that we're already
// using for horizontal text.
// So we will synthesize *something* usable here even if there aren't any of the
// usual font tables (which can happen in the case of a legacy bitmap or Type1
// font for which the platform-specific backend used platform APIs instead of
// sfnt tables to create the horizontal metrics).
void gfxFont::CreateVerticalMetrics() {
const uint32_t kHheaTableTag = TRUETYPE_TAG('h', 'h', 'e', 'a');
const uint32_t kVheaTableTag = TRUETYPE_TAG('v', 'h', 'e', 'a');
const uint32_t kPostTableTag = TRUETYPE_TAG('p', 'o', 's', 't');
const uint32_t kOS_2TableTag = TRUETYPE_TAG('O', 'S', '/', '2');
uint32_t len;
auto* metrics = new Metrics();
::memset(metrics, 0, sizeof(Metrics));
// Some basic defaults, in case the font lacks any real metrics tables.
// TODO: consider what rounding (if any) we should apply to these.
metrics->emHeight = GetAdjustedSize();
metrics->emAscent = metrics->emHeight / 2;
metrics->emDescent = metrics->emHeight - metrics->emAscent;
metrics->maxAscent = metrics->emAscent;
metrics->maxDescent = metrics->emDescent;
const float UNINITIALIZED_LEADING = -10000.0f;
metrics->externalLeading = UNINITIALIZED_LEADING;
if (mFUnitsConvFactor < 0.0) {
uint16_t upem = GetFontEntry()->UnitsPerEm();
if (upem != gfxFontEntry::kInvalidUPEM) {
AutoWriteLock lock(mLock);
mFUnitsConvFactor = GetAdjustedSize() / upem;
}
}
#define SET_UNSIGNED(field, src) \
metrics->field = uint16_t(src) * mFUnitsConvFactor
#define SET_SIGNED(field, src) metrics->field = int16_t(src) * mFUnitsConvFactor
gfxFontEntry::AutoTable os2Table(mFontEntry, kOS_2TableTag);
if (os2Table && mFUnitsConvFactor >= 0.0) {
const OS2Table* os2 =
reinterpret_cast<const OS2Table*>(hb_blob_get_data(os2Table, &len));
// These fields should always be present in any valid OS/2 table
if (len >= offsetof(OS2Table, sTypoLineGap) + sizeof(int16_t)) {
SET_SIGNED(strikeoutSize, os2->yStrikeoutSize);
// Use ascent+descent from the horizontal metrics as the default
// advance (aveCharWidth) in vertical mode
gfxFloat ascentDescent =
gfxFloat(mFUnitsConvFactor) *
(int16_t(os2->sTypoAscender) - int16_t(os2->sTypoDescender));
metrics->aveCharWidth = std::max(metrics->emHeight, ascentDescent);
// Use xAvgCharWidth from horizontal metrics as minimum font extent
// for vertical layout, applying half of it to ascent and half to
// descent (to work with a default centered baseline).
gfxFloat halfCharWidth =
int16_t(os2->xAvgCharWidth) * gfxFloat(mFUnitsConvFactor) / 2;
metrics->maxAscent = std::max(metrics->maxAscent, halfCharWidth);
metrics->maxDescent = std::max(metrics->maxDescent, halfCharWidth);
}
}
// If we didn't set aveCharWidth from OS/2, try to read 'hhea' metrics
// and use the line height from its ascent/descent.
if (!metrics->aveCharWidth) {
gfxFontEntry::AutoTable hheaTable(mFontEntry, kHheaTableTag);
if (hheaTable && mFUnitsConvFactor >= 0.0) {
const MetricsHeader* hhea = reinterpret_cast<const MetricsHeader*>(
hb_blob_get_data(hheaTable, &len));
if (len >= sizeof(MetricsHeader)) {
SET_SIGNED(aveCharWidth,
int16_t(hhea->ascender) - int16_t(hhea->descender));
metrics->maxAscent = metrics->aveCharWidth / 2;
metrics->maxDescent = metrics->aveCharWidth - metrics->maxAscent;
}
}
}
// Read real vertical metrics if available.
metrics->ideographicWidth = -1.0;
metrics->zeroWidth = -1.0;
gfxFontEntry::AutoTable vheaTable(mFontEntry, kVheaTableTag);
if (vheaTable && mFUnitsConvFactor >= 0.0) {
const MetricsHeader* vhea = reinterpret_cast<const MetricsHeader*>(
hb_blob_get_data(vheaTable, &len));
if (len >= sizeof(MetricsHeader)) {
SET_UNSIGNED(maxAdvance, vhea->advanceWidthMax);
// Redistribute space between ascent/descent because we want a
// centered vertical baseline by default.
gfxFloat halfExtent =
0.5 * gfxFloat(mFUnitsConvFactor) *
(int16_t(vhea->ascender) + std::abs(int16_t(vhea->descender)));
// Some bogus fonts have ascent and descent set to zero in 'vhea'.
// In that case we just ignore them and keep our synthetic values
// from above.
if (halfExtent > 0) {
metrics->maxAscent = halfExtent;
metrics->maxDescent = halfExtent;
SET_SIGNED(externalLeading, vhea->lineGap);
}
// Call gfxHarfBuzzShaper::GetGlyphVAdvance directly, as GetCharAdvance
// would potentially recurse if no v-advance is available and it attempts
// to fall back to a value from mVerticalMetrics.
if (gfxHarfBuzzShaper* shaper = GetHarfBuzzShaper()) {
uint32_t gid = ProvidesGetGlyph()
? GetGlyph(kWaterIdeograph, 0)
: shaper->GetNominalGlyph(kWaterIdeograph);
if (gid) {
int32_t advance = shaper->GetGlyphVAdvance(gid);
// Convert 16.16 fixed-point advance from the shaper to a float.
metrics->ideographicWidth =
advance < 0 ? metrics->aveCharWidth : advance / 65536.0;
}
gid = ProvidesGetGlyph() ? GetGlyph('0', 0)
: shaper->GetNominalGlyph('0');
if (gid) {
int32_t advance = shaper->GetGlyphVAdvance(gid);
metrics->zeroWidth =
advance < 0 ? metrics->aveCharWidth : advance / 65536.0;
}
}
}
}
// If we didn't set aveCharWidth above, we must be dealing with a non-sfnt
// font of some kind (Type1, bitmap, vector, ...), so fall back to using
// whatever the platform backend figured out for horizontal layout.
// And if we haven't set externalLeading yet, then copy that from the
// horizontal metrics as well, to help consistency of CSS line-height.
if (!metrics->aveCharWidth ||
metrics->externalLeading == UNINITIALIZED_LEADING) {
const Metrics& horizMetrics = GetHorizontalMetrics();
if (!metrics->aveCharWidth) {
metrics->aveCharWidth = horizMetrics.maxAscent + horizMetrics.maxDescent;
}
if (metrics->externalLeading == UNINITIALIZED_LEADING) {
metrics->externalLeading = horizMetrics.externalLeading;
}
}
// Get underline thickness from the 'post' table if available.
// We also read the underline position, although in vertical-upright mode
// this will not be appropriate to use directly (see nsTextFrame.cpp).
gfxFontEntry::AutoTable postTable(mFontEntry, kPostTableTag);
if (postTable) {
const PostTable* post =
reinterpret_cast<const PostTable*>(hb_blob_get_data(postTable, &len));
if (len >= offsetof(PostTable, underlineThickness) + sizeof(uint16_t)) {
static_assert(offsetof(PostTable, underlinePosition) <
offsetof(PostTable, underlineThickness),
"broken PostTable struct?");
SET_SIGNED(underlineOffset, post->underlinePosition);
SET_UNSIGNED(underlineSize, post->underlineThickness);
// Also use for strikeout if we didn't find that in OS/2 above.
if (!metrics->strikeoutSize) {
metrics->strikeoutSize = metrics->underlineSize;
}
}
}
#undef SET_UNSIGNED
#undef SET_SIGNED
// If we didn't read this from a vhea table, it will still be zero.
// In any case, let's make sure it is not less than the value we've
// come up with for aveCharWidth.
metrics->maxAdvance = std::max(metrics->maxAdvance, metrics->aveCharWidth);
// Thickness of underline and strikeout may have been read from tables,
// but in case they were not present, ensure a minimum of 1 pixel.
metrics->underlineSize = std::max(1.0, metrics->underlineSize);
metrics->strikeoutSize = std::max(1.0, metrics->strikeoutSize);
metrics->strikeoutOffset = -0.5 * metrics->strikeoutSize;
// Somewhat arbitrary values for now, subject to future refinement...
metrics->spaceWidth = metrics->aveCharWidth;
metrics->maxHeight = metrics->maxAscent + metrics->maxDescent;
metrics->xHeight = metrics->emHeight / 2;
metrics->capHeight = metrics->maxAscent;
if (metrics->zeroWidth < 0.0) {
metrics->zeroWidth = metrics->aveCharWidth;
}
if (!mVerticalMetrics.compareExchange(nullptr, metrics)) {
delete metrics;
}
}
gfxFloat gfxFont::SynthesizeSpaceWidth(uint32_t aCh) {
// return an appropriate width for various Unicode space characters
// that we "fake" if they're not actually present in the font;
// returns negative value if the char is not a known space.
switch (aCh) {
case 0x2000: // en quad
case 0x2002:
return GetAdjustedSize() / 2; // en space
case 0x2001: // em quad
case 0x2003:
return GetAdjustedSize(); // em space
case 0x2004:
return GetAdjustedSize() / 3; // three-per-em space
case 0x2005:
return GetAdjustedSize() / 4; // four-per-em space
case 0x2006:
return GetAdjustedSize() / 6; // six-per-em space
case 0x2007:
return GetMetrics(nsFontMetrics::eHorizontal)
.ZeroOrAveCharWidth(); // figure space
case 0x2008:
return GetMetrics(nsFontMetrics::eHorizontal)
.spaceWidth; // punctuation space
case 0x2009:
return GetAdjustedSize() / 5; // thin space
case 0x200a:
return GetAdjustedSize() / 10; // hair space
case 0x202f:
return GetAdjustedSize() / 5; // narrow no-break space
case 0x3000:
return GetAdjustedSize(); // ideographic space
default:
return -1.0;
}
}
void gfxFont::AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf,
FontCacheSizes* aSizes) const {
AutoReadLock lock(mLock);
for (uint32_t i = 0; i < mGlyphExtentsArray.Length(); ++i) {
aSizes->mFontInstances +=
mGlyphExtentsArray[i]->SizeOfIncludingThis(aMallocSizeOf);
}
if (mWordCache) {
aSizes->mShapedWords +=
mWordCache->shallowSizeOfIncludingThis(aMallocSizeOf);
for (auto it = mWordCache->iter(); !it.done(); it.next()) {
aSizes->mShapedWords +=
it.get().value()->SizeOfIncludingThis(aMallocSizeOf);
}
}
}
void gfxFont::AddSizeOfIncludingThis(MallocSizeOf aMallocSizeOf,
FontCacheSizes* aSizes) const {
aSizes->mFontInstances += aMallocSizeOf(this);
AddSizeOfExcludingThis(aMallocSizeOf, aSizes);
}
void gfxFont::AddGlyphChangeObserver(GlyphChangeObserver* aObserver) {
AutoWriteLock lock(mLock);
if (!mGlyphChangeObservers) {
mGlyphChangeObservers = MakeUnique<nsTHashSet<GlyphChangeObserver*>>();
}
mGlyphChangeObservers->Insert(aObserver);
}
void gfxFont::RemoveGlyphChangeObserver(GlyphChangeObserver* aObserver) {
AutoWriteLock lock(mLock);
NS_ASSERTION(mGlyphChangeObservers, "No observers registered");
NS_ASSERTION(mGlyphChangeObservers->Contains(aObserver),
"Observer not registered");
mGlyphChangeObservers->Remove(aObserver);
}
#define DEFAULT_PIXEL_FONT_SIZE 16.0f
gfxFontStyle::gfxFontStyle()
: size(DEFAULT_PIXEL_FONT_SIZE),
sizeAdjust(0.0f),
baselineOffset(0.0f),
languageOverride(NO_FONT_LANGUAGE_OVERRIDE),
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),
useSyntheticPosition(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,
bool aUsePositionSynthesis,
uint32_t aLanguageOverride)
: size(aSize),
baselineOffset(0.0f),
languageOverride(aLanguageOverride),
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),
useSyntheticPosition(aUsePositionSynthesis),
noFallbackVariantFeatures(true) {
MOZ_ASSERT(!std::isnan(size));
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?");
#define HANDLE_TAG(TAG) \
case FontSizeAdjust::Tag::TAG: \
sizeAdjust = aSizeAdjust.As##TAG(); \
break;
switch (aSizeAdjust.tag) {
case FontSizeAdjust::Tag::None:
sizeAdjust = 0.0f;
break;
HANDLE_TAG(ExHeight)
HANDLE_TAG(CapHeight)
HANDLE_TAG(ChWidth)
HANDLE_TAG(IcWidth)
HANDLE_TAG(IcHeight)
}
#undef HANDLE_TAG
MOZ_ASSERT(!std::isnan(sizeAdjust));
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;
}