Bug 1371386 - Take account of requirements for emoji-style or text-style presentation during font selection & fallback. r=m_kato

Differential Revision: https://phabricator.services.mozilla.com/D87304
This commit is contained in:
Jonathan Kew 2020-08-24 14:24:12 +00:00
parent 5dd6f7cbe3
commit ba87878325
29 changed files with 411 additions and 142 deletions

View File

@ -33,7 +33,8 @@ static double WSSDistance(const Face* aFace, const gfxFontStyle& aStyle) {
// weight/style/stretch priority: stretch >> style >> weight
// so we multiply the stretch and style values to make them dominate
// the result
return stretchDist * 1.0e8 + styleDist * 1.0e4 + weightDist;
return stretchDist * kStretchFactor + styleDist * kStyleFactor +
weightDist * kWeightFactor;
}
void* Pointer::ToPtr(FontList* aFontList) const {
@ -398,6 +399,21 @@ void Family::SearchAllFontsForChar(FontList* aList,
if (!charmap && !fe->HasCharacter(aMatchData->mCh)) {
continue;
}
if (aMatchData->mPresentation != eFontPresentation::Any) {
gfxFont* font = fe->FindOrMakeFont(&aMatchData->mStyle);
if (!font) {
continue;
}
bool hasColorGlyph =
font->HasColorGlyphFor(aMatchData->mCh, aMatchData->mNextCh);
if (hasColorGlyph !=
(aMatchData->mPresentation == eFontPresentation::Emoji)) {
distance += kPresentationMismatch;
if (distance >= aMatchData->mMatchDistance) {
continue;
}
}
}
aMatchData->mBestMatch = fe;
aMatchData->mMatchDistance = distance;
aMatchData->mMatchedSharedFamily = this;

View File

@ -156,14 +156,14 @@ static bool IsJapaneseLocale() {
}
void gfxAndroidPlatform::GetCommonFallbackFonts(
uint32_t aCh, uint32_t aNextCh, Script aRunScript,
uint32_t aCh, Script aRunScript, eFontPresentation aPresentation,
nsTArray<const char*>& aFontList) {
static const char kDroidSansJapanese[] = "Droid Sans Japanese";
static const char kMotoyaLMaru[] = "MotoyaLMaru";
static const char kNotoSansCJKJP[] = "Noto Sans CJK JP";
static const char kNotoColorEmoji[] = "Noto Color Emoji";
if (ShouldPreferEmojiFont(aCh, aNextCh)) {
if (aPresentation == eFontPresentation::Emoji) {
aFontList.AppendElement(kNotoColorEmoji);
}

View File

@ -38,7 +38,8 @@ class gfxAndroidPlatform final : public gfxPlatform {
void ReadSystemFontList(
nsTArray<mozilla::dom::SystemFontListEntry>* aFontList) override;
void GetCommonFallbackFonts(uint32_t aCh, uint32_t aNextCh, Script aRunScript,
void GetCommonFallbackFonts(uint32_t aCh, Script aRunScript,
eFontPresentation aPresentation,
nsTArray<const char*>& aFontList) override;
bool FontHintingEnabled() override;

View File

@ -2409,6 +2409,40 @@ bool gfxFont::RenderColorGlyph(DrawTarget* aDrawTarget, gfxContext* aContext,
return true;
}
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.
if (!mHarfBuzzShaper) {
mHarfBuzzShaper = MakeUnique<gfxHarfBuzzShaper>(this);
}
auto* shaper = static_cast<gfxHarfBuzzShaper*>(mHarfBuzzShaper.get());
if (!shaper->Initialize()) {
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;
}
// Check if there is a COLR/CPAL or SVG glyph for this ID.
if (fe->TryGetColorGlyphs() && fe->HasColorLayersForGlyph(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);

View File

@ -1864,6 +1864,8 @@ class gfxFont {
// glyphs. This does not add a reference to the returned font.
gfxFont* GetSubSuperscriptFont(int32_t aAppUnitsPerDevPixel);
bool HasColorGlyphFor(uint32_t aCh, uint32_t aNextCh);
protected:
virtual const Metrics& GetHorizontalMetrics() = 0;

View File

@ -1556,7 +1556,8 @@ static inline double WeightStyleStretchDistance(
// weight/style/stretch priority: stretch >> style >> weight
// so we multiply the stretch and style values to make them dominate
// the result
return stretchDist * 1.0e8 + styleDist * 1.0e4 + weightDist;
return stretchDist * kStretchFactor + styleDist * kStyleFactor +
weightDist * kWeightFactor;
}
void gfxFontFamily::FindAllFontsForStyle(
@ -1794,6 +1795,18 @@ void gfxFontFamily::FindFontForChar(GlobalFontMatch* aMatchData) {
fe = e;
distance = WeightStyleStretchDistance(fe, aMatchData->mStyle);
if (aMatchData->mPresentation != eFontPresentation::Any) {
RefPtr<gfxFont> font = fe->FindOrMakeFont(&aMatchData->mStyle);
if (!font) {
continue;
}
bool hasColorGlyph =
font->HasColorGlyphFor(aMatchData->mCh, aMatchData->mNextCh);
if (hasColorGlyph !=
(aMatchData->mPresentation == eFontPresentation::Emoji)) {
distance += kPresentationMismatch;
}
}
break;
}
}
@ -1802,7 +1815,8 @@ void gfxFontFamily::FindFontForChar(GlobalFontMatch* aMatchData) {
// If style/weight/stretch was not Normal, see if we can
// fall back to a next-best face (e.g. Arial Black -> Bold,
// or Arial Narrow -> Regular).
GlobalFontMatch data(aMatchData->mCh, aMatchData->mStyle);
GlobalFontMatch data(aMatchData->mCh, aMatchData->mNextCh,
aMatchData->mStyle, aMatchData->mPresentation);
SearchAllFontsForChar(&data);
if (!data.mBestMatch) {
return;
@ -1836,6 +1850,18 @@ void gfxFontFamily::SearchAllFontsForChar(GlobalFontMatch* aMatchData) {
gfxFontEntry* fe = mAvailableFonts[i];
if (fe && fe->HasCharacter(aMatchData->mCh)) {
float distance = WeightStyleStretchDistance(fe, aMatchData->mStyle);
if (aMatchData->mPresentation != eFontPresentation::Any) {
RefPtr<gfxFont> font = fe->FindOrMakeFont(&aMatchData->mStyle);
if (!font) {
continue;
}
bool hasColorGlyph =
font->HasColorGlyphFor(aMatchData->mCh, aMatchData->mNextCh);
if (hasColorGlyph !=
(aMatchData->mPresentation == eFontPresentation::Emoji)) {
distance += kPresentationMismatch;
}
}
if (distance < aMatchData->mMatchDistance ||
(distance == aMatchData->mMatchDistance &&
Compare(fe->Name(), aMatchData->mBestMatch->Name()) > 0)) {

View File

@ -12,6 +12,7 @@
#include "gfxFontFeatures.h"
#include "gfxFontUtils.h"
#include "gfxFontVariations.h"
#include "gfxPlatform.h"
#include "nsTArray.h"
#include "nsTHashtable.h"
#include "mozilla/HashFunctions.h"
@ -254,6 +255,15 @@ class gfxFontEntry {
const mozilla::gfx::DeviceColor& aDefaultColor,
nsTArray<uint16_t>& layerGlyphs,
nsTArray<mozilla::gfx::DeviceColor>& layerColors);
bool HasColorLayersForGlyph(uint32_t aGlyphId) {
MOZ_ASSERT(mCOLR);
return gfxFontUtils::HasColorLayersForGlyph(mCOLR, aGlyphId);
}
bool HasColorBitmapTable() {
return HasFontTable(TRUETYPE_TAG('C', 'B', 'D', 'T')) ||
HasFontTable(TRUETYPE_TAG('s', 'b', 'i', 'x'));
}
// Access to raw font table data (needed for Harfbuzz):
// returns a pointer to data owned by the fontEntry or the OS,
@ -752,17 +762,23 @@ inline bool gfxFontEntry::SupportsBold() {
// used when iterating over all fonts looking for a match for a given character
struct GlobalFontMatch {
GlobalFontMatch(const uint32_t aCharacter, const gfxFontStyle& aStyle)
: mStyle(aStyle), mCh(aCharacter) {}
GlobalFontMatch(uint32_t aCharacter, uint32_t aNextCh,
const gfxFontStyle& aStyle, eFontPresentation aPresentation)
: mStyle(aStyle),
mCh(aCharacter),
mNextCh(aNextCh),
mPresentation(aPresentation) {}
RefPtr<gfxFontEntry> mBestMatch; // current best match
RefPtr<gfxFontFamily> mMatchedFamily; // the family it belongs to
mozilla::fontlist::Family* mMatchedSharedFamily = nullptr;
const gfxFontStyle& mStyle; // style to match
const uint32_t mCh; // codepoint to be matched
const uint32_t mNextCh; // following codepoint (or zero)
eFontPresentation mPresentation;
uint32_t mCount = 0; // number of fonts matched
uint32_t mCmapsTested = 0; // number of cmaps tested
float mMatchDistance = INFINITY; // metric indicating closest match
double mMatchDistance = INFINITY; // metric indicating closest match
};
// Installation status (base system / langpack / user-installed) may determine

View File

@ -1722,6 +1722,16 @@ bool gfxFontUtils::GetColorGlyphLayers(
return true;
}
bool gfxFontUtils::HasColorLayersForGlyph(hb_blob_t* aCOLR, uint32_t aGlyphId) {
unsigned int blobLength;
const COLRHeader* colr =
reinterpret_cast<const COLRHeader*>(hb_blob_get_data(aCOLR, &blobLength));
MOZ_ASSERT(colr, "Cannot get COLR raw data");
MOZ_ASSERT(blobLength, "Found COLR data, but length is 0");
return LookForBaseGlyphRecord(colr, aGlyphId);
}
void gfxFontUtils::GetVariationData(
gfxFontEntry* aFontEntry, nsTArray<gfxFontVariationAxis>* aAxes,
nsTArray<gfxFontVariationInstance>* aInstances) {

View File

@ -1159,6 +1159,7 @@ class gfxFontUtils {
const mozilla::gfx::DeviceColor& aDefaultColor,
nsTArray<uint16_t>& aGlyphs,
nsTArray<mozilla::gfx::DeviceColor>& aColors);
static bool HasColorLayersForGlyph(hb_blob_t* aCOLR, uint32_t aGlyphId);
// Helper used to implement gfxFontEntry::GetVariation{Axes,Instances} for
// platforms where the native font APIs don't provide the info we want
@ -1204,6 +1205,16 @@ class gfxFontUtils {
static const mozilla::Encoding* gMSFontNameCharsets[];
};
// Factors used to weight the distances between the available and target font
// properties during font-matching. These ensure that we respect the CSS-fonts
// requirement that font-stretch >> font-style >> font-weight; and in addition,
// a mismatch between the desired and actual glyph presentation (emoji vs text)
// will take precedence over any of the style attributes.
constexpr double kPresentationMismatch = 1.0e12;
constexpr double kStretchFactor = 1.0e8;
constexpr double kStyleFactor = 1.0e4;
constexpr double kWeightFactor = 1.0e0;
// style distance ==> [0,500]
static inline double StyleDistance(const mozilla::SlantStyleRange& aRange,
mozilla::FontSlantStyle aTargetStyle) {

View File

@ -96,6 +96,10 @@ enum eGfxLog {
eGfxLog_textperf = 5
};
// Used during font matching to express a preference, if any, for whether
// to use a font that will present a color or monochrome glyph.
enum class eFontPresentation : uint8_t { Any = 0, Text = 1, Emoji = 2 };
// when searching through pref langs, max number of pref langs
const uint32_t kMaxLenPrefLangList = 32;
@ -475,8 +479,8 @@ class gfxPlatform : public mozilla::layers::MemoryPressureListener {
// returns a list of commonly used fonts for a given character
// these are *possible* matches, no cmap-checking is done at this level
virtual void GetCommonFallbackFonts(uint32_t /*aCh*/, uint32_t /*aNextCh*/,
Script /*aRunScript*/,
virtual void GetCommonFallbackFonts(uint32_t /*aCh*/, Script /*aRunScript*/,
eFontPresentation /*aPresentation*/,
nsTArray<const char*>& /*aFontList*/) {
// platform-specific override, by default do nothing
}

View File

@ -863,20 +863,19 @@ void gfxPlatformFontList::GetFontFamilyList(
}
}
gfxFontEntry* gfxPlatformFontList::SystemFindFontForChar(
gfxFont* gfxPlatformFontList::SystemFindFontForChar(
uint32_t aCh, uint32_t aNextCh, Script aRunScript,
const gfxFontStyle* aStyle, FontVisibility* aVisibility,
FontMatchingStats* aFontMatchingStats) {
eFontPresentation aPresentation, const gfxFontStyle* aStyle,
FontVisibility* aVisibility, FontMatchingStats* aFontMatchingStats) {
MOZ_ASSERT(!mCodepointsWithNoFonts.test(aCh),
"don't call for codepoints already known to be unsupported");
gfxFontEntry* fontEntry = nullptr;
// Try to short-circuit font fallback for U+FFFD, used to represent
// encoding errors: just use cached family from last time U+FFFD was seen.
// This helps speed up pages with lots of encoding errors, binary-as-text,
// etc.
if (aCh == 0xFFFD) {
gfxFontEntry* fontEntry = nullptr;
if (mReplacementCharFallbackFamily.mIsShared &&
mReplacementCharFallbackFamily.mShared) {
fontlist::Face* face =
@ -897,7 +896,7 @@ gfxFontEntry* gfxPlatformFontList::SystemFindFontForChar(
// this should never fail, as we must have found U+FFFD in order to set
// mReplacementCharFallbackFamily at all, but better play it safe
if (fontEntry && fontEntry->HasCharacter(aCh)) {
return fontEntry;
return fontEntry->FindOrMakeFont(aStyle);
}
}
@ -906,15 +905,35 @@ gfxFontEntry* gfxPlatformFontList::SystemFindFontForChar(
// search commonly available fonts
bool common = true;
FontFamily fallbackFamily;
fontEntry =
CommonFontFallback(aCh, aNextCh, aRunScript, aStyle, fallbackFamily);
gfxFont* candidate = CommonFontFallback(
aCh, aNextCh, aRunScript, aPresentation, aStyle, fallbackFamily);
gfxFont* font = nullptr;
if (candidate) {
if (aPresentation == eFontPresentation::Any) {
font = candidate;
} else {
bool hasColorGlyph = candidate->HasColorGlyphFor(aCh, aNextCh);
if (hasColorGlyph == (aPresentation == eFontPresentation::Emoji)) {
font = candidate;
}
}
}
// if didn't find a font, do system-wide fallback (except for specials)
// If we didn't find a common font, or it was not the preferred type (color
// or monochrome), do system-wide fallback (except for specials).
uint32_t cmapCount = 0;
if (!fontEntry) {
if (!font) {
common = false;
fontEntry = GlobalFontFallback(aCh, aRunScript, aStyle, cmapCount,
fallbackFamily, aFontMatchingStats);
font = GlobalFontFallback(aCh, aNextCh, aRunScript, aPresentation, aStyle,
cmapCount, fallbackFamily, aFontMatchingStats);
// If the font we found doesn't match the requested type, and we also found
// a candidate above, prefer that one.
if (font && aPresentation != eFontPresentation::Any && candidate) {
bool hasColorGlyph = font->HasColorGlyphFor(aCh, aNextCh);
if (hasColorGlyph != (aPresentation == eFontPresentation::Emoji)) {
font = candidate;
}
}
}
TimeDuration elapsed = TimeStamp::Now() - start;
@ -927,12 +946,12 @@ gfxFontEntry* gfxPlatformFontList::SystemFindFontForChar(
"script: %d match: [%s]"
" time: %dus cmaps: %d\n",
(common ? "common" : "global"), aCh, static_cast<int>(script),
(fontEntry ? fontEntry->Name().get() : "<none>"),
(font ? font->GetFontEntry()->Name().get() : "<none>"),
int32_t(elapsed.ToMicroseconds()), cmapCount));
}
// no match? add to set of non-matching codepoints
if (!fontEntry) {
if (!font) {
mCodepointsWithNoFonts.set(aCh);
} else {
*aVisibility = fallbackFamily.mIsShared
@ -957,18 +976,19 @@ gfxFontEntry* gfxPlatformFontList::SystemFindFontForChar(
Telemetry::Accumulate(Telemetry::SYSTEM_FONT_FALLBACK_SCRIPT,
int(aRunScript) + 1);
return fontEntry;
return font;
}
#define NUM_FALLBACK_FONTS 8
gfxFontEntry* gfxPlatformFontList::CommonFontFallback(
gfxFont* gfxPlatformFontList::CommonFontFallback(
uint32_t aCh, uint32_t aNextCh, Script aRunScript,
const gfxFontStyle* aMatchStyle, FontFamily& aMatchedFamily) {
eFontPresentation aPresentation, const gfxFontStyle* aMatchStyle,
FontFamily& aMatchedFamily) {
AutoTArray<const char*, NUM_FALLBACK_FONTS> defaultFallbacks;
gfxPlatform::GetPlatform()->GetCommonFallbackFonts(aCh, aNextCh, aRunScript,
defaultFallbacks);
GlobalFontMatch data(aCh, *aMatchStyle);
gfxPlatform::GetPlatform()->GetCommonFallbackFonts(
aCh, aRunScript, aPresentation, defaultFallbacks);
GlobalFontMatch data(aCh, aNextCh, *aMatchStyle, aPresentation);
if (SharedFontList()) {
for (const auto name : defaultFallbacks) {
fontlist::Family* family = FindSharedFamily(nsDependentCString(name));
@ -978,7 +998,7 @@ gfxFontEntry* gfxPlatformFontList::CommonFontFallback(
family->SearchAllFontsForChar(SharedFontList(), &data);
if (data.mBestMatch) {
aMatchedFamily = FontFamily(family);
return data.mBestMatch;
return data.mBestMatch->FindOrMakeFont(aMatchStyle);
}
}
} else {
@ -991,15 +1011,16 @@ gfxFontEntry* gfxPlatformFontList::CommonFontFallback(
fallback->FindFontForChar(&data);
if (data.mBestMatch) {
aMatchedFamily = FontFamily(fallback);
return data.mBestMatch;
return data.mBestMatch->FindOrMakeFont(aMatchStyle);
}
}
}
return nullptr;
}
gfxFontEntry* gfxPlatformFontList::GlobalFontFallback(
const uint32_t aCh, Script aRunScript, const gfxFontStyle* aMatchStyle,
gfxFont* gfxPlatformFontList::GlobalFontFallback(
uint32_t aCh, uint32_t aNextCh, Script aRunScript,
eFontPresentation aPresentation, const gfxFontStyle* aMatchStyle,
uint32_t& aCmapCount, FontFamily& aMatchedFamily,
FontMatchingStats* aFontMatchingStats) {
bool useCmaps = IsFontFamilyWhitelistActive() ||
@ -1012,12 +1033,30 @@ gfxFontEntry* gfxPlatformFontList::GlobalFontFallback(
if (fe) {
if (aMatchedFamily.mIsShared) {
if (IsVisibleToCSS(*aMatchedFamily.mShared)) {
return fe;
gfxFont* font = fe->FindOrMakeFont(aMatchStyle);
if (font) {
if (aPresentation == eFontPresentation::Any) {
return font;
}
bool hasColorGlyph = font->HasColorGlyphFor(aCh, aNextCh);
if (hasColorGlyph == (aPresentation == eFontPresentation::Emoji)) {
return font;
}
}
}
rejectedFallbackVisibility = aMatchedFamily.mShared->Visibility();
} else {
if (IsVisibleToCSS(*aMatchedFamily.mUnshared)) {
return fe;
gfxFont* font = fe->FindOrMakeFont(aMatchStyle);
if (font) {
if (aPresentation == eFontPresentation::Any) {
return font;
}
bool hasColorGlyph = font->HasColorGlyphFor(aCh, aNextCh);
if (hasColorGlyph == (aPresentation == eFontPresentation::Emoji)) {
return font;
}
}
}
rejectedFallbackVisibility = aMatchedFamily.mUnshared->Visibility();
}
@ -1025,7 +1064,7 @@ gfxFontEntry* gfxPlatformFontList::GlobalFontFallback(
}
// otherwise, try to find it among local fonts
GlobalFontMatch data(aCh, *aMatchStyle);
GlobalFontMatch data(aCh, aNextCh, *aMatchStyle, aPresentation);
if (SharedFontList()) {
fontlist::Family* families = SharedFontList()->Families();
if (families) {
@ -1042,7 +1081,7 @@ gfxFontEntry* gfxPlatformFontList::GlobalFontFallback(
}
if (data.mBestMatch) {
aMatchedFamily = FontFamily(data.mMatchedSharedFamily);
return data.mBestMatch;
return data.mBestMatch->FindOrMakeFont(aMatchStyle);
}
}
} else {
@ -1064,7 +1103,7 @@ gfxFontEntry* gfxPlatformFontList::GlobalFontFallback(
aCmapCount = data.mCmapsTested;
if (data.mBestMatch) {
aMatchedFamily = FontFamily(data.mMatchedFamily);
return data.mBestMatch;
return data.mBestMatch->FindOrMakeFont(aMatchStyle);
}
}

View File

@ -209,8 +209,9 @@ class gfxPlatformFontList : public gfxFontInfoLoader {
void GetFontFamilyList(nsTArray<RefPtr<gfxFontFamily>>& aFamilyArray);
gfxFontEntry* SystemFindFontForChar(uint32_t aCh, uint32_t aNextCh,
gfxFont* SystemFindFontForChar(uint32_t aCh, uint32_t aNextCh,
Script aRunScript,
eFontPresentation aPresentation,
const gfxFontStyle* aStyle,
FontVisibility* aVisibility,
FontMatchingStats* aFontMatchingStats);
@ -622,16 +623,16 @@ class gfxPlatformFontList : public gfxFontInfoLoader {
}
// returns default font for a given character, null otherwise
gfxFontEntry* CommonFontFallback(uint32_t aCh, uint32_t aNextCh,
Script aRunScript,
gfxFont* CommonFontFallback(uint32_t aCh, uint32_t aNextCh, Script aRunScript,
eFontPresentation aPresentation,
const gfxFontStyle* aMatchStyle,
FontFamily& aMatchedFamily);
// Search fonts system-wide for a given character, null if not found.
gfxFontEntry* GlobalFontFallback(const uint32_t aCh, Script aRunScript,
gfxFont* GlobalFontFallback(uint32_t aCh, uint32_t aNextCh, Script aRunScript,
eFontPresentation aPresentation,
const gfxFontStyle* aMatchStyle,
uint32_t& aCmapCount,
FontFamily& aMatchedFamily,
uint32_t& aCmapCount, FontFamily& aMatchedFamily,
FontMatchingStats* aFontMatchingStats);
// Platform-specific implementation of global font fallback, if any;

View File

@ -206,10 +206,10 @@ static const char kFontWenQuanYiMicroHei[] = "WenQuanYi Micro Hei";
static const char kFontNanumGothic[] = "NanumGothic";
static const char kFontSymbola[] = "Symbola";
void gfxPlatformGtk::GetCommonFallbackFonts(uint32_t aCh, uint32_t aNextCh,
Script aRunScript,
void gfxPlatformGtk::GetCommonFallbackFonts(uint32_t aCh, Script aRunScript,
eFontPresentation aPresentation,
nsTArray<const char*>& aFontList) {
if (ShouldPreferEmojiFont(aCh, aNextCh)) {
if (aPresentation == eFontPresentation::Emoji) {
aFontList.AppendElement(kFontTwemojiMozilla);
}

View File

@ -42,7 +42,8 @@ class gfxPlatformGtk final : public gfxPlatform {
nsresult UpdateFontList() override;
void GetCommonFallbackFonts(uint32_t aCh, uint32_t aNextCh, Script aRunScript,
void GetCommonFallbackFonts(uint32_t aCh, Script aRunScript,
eFontPresentation aPresentation,
nsTArray<const char*>& aFontList) override;
gfxPlatformFontList* CreatePlatformFontList() override;

View File

@ -171,10 +171,10 @@ static const char kFontSTIXGeneral[] = "STIXGeneral";
static const char kFontTamilMN[] = "Tamil MN";
static const char kFontZapfDingbats[] = "Zapf Dingbats";
void gfxPlatformMac::GetCommonFallbackFonts(uint32_t aCh, uint32_t aNextCh,
Script aRunScript,
void gfxPlatformMac::GetCommonFallbackFonts(uint32_t aCh, Script aRunScript,
eFontPresentation aPresentation,
nsTArray<const char*>& aFontList) {
if (ShouldPreferEmojiFont(aCh, aNextCh)) {
if (aPresentation == eFontPresentation::Emoji) {
aFontList.AppendElement(kFontAppleColorEmoji);
}

View File

@ -39,7 +39,8 @@ class gfxPlatformMac : public gfxPlatform {
bool IsFontFormatSupported(uint32_t aFormatFlags) override;
void GetCommonFallbackFonts(uint32_t aCh, uint32_t aNextCh, Script aRunScript,
void GetCommonFallbackFonts(uint32_t aCh, Script aRunScript,
eFontPresentation aPresentation,
nsTArray<const char*>& aFontList) override;
// lookup the system font for a particular system font type and set

View File

@ -2909,9 +2909,10 @@ gfxTextRun* gfxFontGroup::GetEllipsisTextRun(
return mCachedEllipsisTextRun.get();
}
gfxFont* gfxFontGroup::FindFallbackFaceForChar(gfxFontFamily* aFamily,
uint32_t aCh) {
GlobalFontMatch data(aCh, mStyle);
gfxFont* gfxFontGroup::FindFallbackFaceForChar(
gfxFontFamily* aFamily, uint32_t aCh, uint32_t aNextCh,
eFontPresentation aPresentation) {
GlobalFontMatch data(aCh, aNextCh, mStyle, aPresentation);
aFamily->SearchAllFontsForChar(&data);
gfxFontEntry* fe = data.mBestMatch;
if (!fe) {
@ -2920,11 +2921,12 @@ gfxFont* gfxFontGroup::FindFallbackFaceForChar(gfxFontFamily* aFamily,
return fe->FindOrMakeFont(&mStyle);
}
gfxFont* gfxFontGroup::FindFallbackFaceForChar(fontlist::Family* aFamily,
uint32_t aCh) {
gfxFont* gfxFontGroup::FindFallbackFaceForChar(
fontlist::Family* aFamily, uint32_t aCh, uint32_t aNextCh,
eFontPresentation aPresentation) {
fontlist::FontList* list =
gfxPlatformFontList::PlatformFontList()->SharedFontList();
GlobalFontMatch data(aCh, mStyle);
GlobalFontMatch data(aCh, aNextCh, mStyle, aPresentation);
aFamily->SearchAllFontsForChar(list, &data);
gfxFontEntry* fe = data.mBestMatch;
if (!fe) {
@ -2933,12 +2935,15 @@ gfxFont* gfxFontGroup::FindFallbackFaceForChar(fontlist::Family* aFamily,
return fe->FindOrMakeFont(&mStyle);
}
gfxFont* gfxFontGroup::FindFallbackFaceForChar(const FamilyFace& aFamily,
uint32_t aCh) {
gfxFont* gfxFontGroup::FindFallbackFaceForChar(
const FamilyFace& aFamily, uint32_t aCh, uint32_t aNextCh,
eFontPresentation aPresentation) {
if (aFamily.IsSharedFamily()) {
return FindFallbackFaceForChar(aFamily.SharedFamily(), aCh);
return FindFallbackFaceForChar(aFamily.SharedFamily(), aCh, aNextCh,
aPresentation);
}
return FindFallbackFaceForChar(aFamily.OwnedFamily(), aCh);
return FindFallbackFaceForChar(aFamily.OwnedFamily(), aCh, aNextCh,
aPresentation);
}
gfxFloat gfxFontGroup::GetUnderlineOffset() {
@ -3030,12 +3035,41 @@ gfxFont* gfxFontGroup::FindFontForChar(uint32_t aCh, uint32_t aPrevCh,
bool isJoinControl = gfxFontUtils::IsJoinControl(aCh);
bool wasJoinCauser = gfxFontUtils::IsJoinCauser(aPrevCh);
bool isVarSelector = gfxFontUtils::IsVarSelector(aCh);
bool nextIsVarSelector = gfxFontUtils::IsVarSelector(aNextCh);
// Whether we've seen a font that is currently loading a resource that may
// provide this character (so we should not start a new load).
bool loading = false;
if (!isJoinControl && !wasJoinCauser && !isVarSelector) {
// Do we need to explicitly look for a font that does or does not provide a
// color glyph for the given character?
// For characters with no `EMOJI` property, we'll use whatever the family
// list calls for; but if it's a potential emoji codepoint, we need to check
// if there's a variation selector specifically asking for Text-style or
// Emoji-style rendering and look for a suitable font.
eFontPresentation presentation = eFontPresentation::Any;
EmojiPresentation emojiPresentation = GetEmojiPresentation(aCh);
if (emojiPresentation != TextOnly) {
// If the prefer-emoji selector is present, or if it's a default-emoji char
// and the prefer-text selector is NOT present, or if there's a skin-tone
// modifier, we specifically look for a font with a color glyph.
// If the prefer-text selector is present, we specifically look for a font
// that will provide a monochrome glyph.
// Otherwise, we'll accept either color or monochrome font-family entries,
// so that a color font can be explicitly applied via font-family even to
// characters that are not inherently emoji-style.
if (aNextCh == kVariationSelector16 ||
(emojiPresentation == EmojiPresentation::EmojiDefault &&
aNextCh != kVariationSelector15) ||
(aNextCh >= kEmojiSkinToneFirst && aNextCh <= kEmojiSkinToneLast)) {
presentation = eFontPresentation::Emoji;
} else if (aNextCh == kVariationSelector15) {
presentation = eFontPresentation::Text;
}
}
if (!isJoinControl && !wasJoinCauser && !isVarSelector &&
!nextIsVarSelector && presentation == eFontPresentation::Any) {
gfxFont* firstFont = GetFontAt(0, aCh, &loading);
if (firstFont) {
if (firstFont->HasCharacter(aCh)) {
@ -3045,13 +3079,13 @@ gfxFont* gfxFontGroup::FindFontForChar(uint32_t aCh, uint32_t aPrevCh,
gfxFont* font = nullptr;
if (mFonts[0].CheckForFallbackFaces()) {
font = FindFallbackFaceForChar(mFonts[0], aCh);
font = FindFallbackFaceForChar(mFonts[0], aCh, aNextCh, presentation);
} else if (!firstFont->GetFontEntry()->IsUserFont()) {
// For platform fonts (but not userfonts), we may need to do
// fallback within the family to handle cases where some faces
// such as Italic or Black have reduced character sets compared
// to the family's Regular face.
font = FindFallbackFaceForChar(mFonts[0], aCh);
font = FindFallbackFaceForChar(mFonts[0], aCh, aNextCh, presentation);
}
if (font) {
*aMatchType = {FontMatchType::Kind::kFontGroup, mFonts[0].Generic()};
@ -3093,6 +3127,36 @@ gfxFont* gfxFontGroup::FindFontForChar(uint32_t aCh, uint32_t aPrevCh,
return aPrevMatchedFont;
}
// Used to remember the first "candidate" font that would provide a fallback
// text-style rendering if no color glyph can be found.
gfxFont* candidateFont = nullptr;
FontMatchType candidateMatchType;
// Handle a candidate font that could support the character, returning true
// if we should go ahead and return |f|, false to continue searching.
auto CheckCandidate = [&](gfxFont* f, FontMatchType t) -> bool {
// If no preference, then just accept the font.
if (presentation == eFontPresentation::Any) {
RefPtr<gfxFont> autoRefDeref(candidateFont);
*aMatchType = t;
return true;
}
// Does the candidate font provide a color glyph for the current character?
bool hasColorGlyph = f->HasColorGlyphFor(aCh, aNextCh);
// If the provided glyph matches the preference, accept the font.
if (hasColorGlyph == (presentation == eFontPresentation::Emoji)) {
RefPtr<gfxFont> autoRefDeref(candidateFont);
*aMatchType = t;
return true;
}
// Otherwise, remember the first potential fallback, but keep searching.
if (!candidateFont) {
candidateFont = f;
candidateMatchType = t;
}
return false;
};
// 1. check remaining fonts in the font group
for (uint32_t i = nextIndex; i < fontListLength; i++) {
FamilyFace& ff = mFonts[i];
@ -3107,9 +3171,11 @@ gfxFont* gfxFontGroup::FindFontForChar(uint32_t aCh, uint32_t aPrevCh,
if (font) {
// if available, use already-made gfxFont and check for character
if (font->HasCharacter(aCh)) {
*aMatchType = {FontMatchType::Kind::kFontGroup, ff.Generic()};
if (CheckCandidate(font,
{FontMatchType::Kind::kFontGroup, ff.Generic()})) {
return font;
}
}
} else {
// don't have a gfxFont yet, test charmap before instantiating
gfxFontEntry* fe = ff.FontEntry();
@ -3139,21 +3205,24 @@ gfxFont* gfxFontGroup::FindFontForChar(uint32_t aCh, uint32_t aPrevCh,
if (pfe && pfe->HasCharacter(aCh)) {
font = GetFontAt(i, aCh, &loading);
if (font) {
*aMatchType = {FontMatchType::Kind::kFontGroup,
mFonts[i].Generic()};
if (CheckCandidate(font, {FontMatchType::Kind::kFontGroup,
mFonts[i].Generic()})) {
return font;
}
}
}
} else if (fe && fe->HasCharacter(aCh)) {
// for normal platform fonts, after checking the cmap
// build the font via GetFontAt
font = GetFontAt(i, aCh, &loading);
if (font) {
*aMatchType = {FontMatchType::Kind::kFontGroup, mFonts[i].Generic()};
if (CheckCandidate(font, {FontMatchType::Kind::kFontGroup,
mFonts[i].Generic()})) {
return font;
}
}
}
}
// check other family faces if needed
if (ff.CheckForFallbackFaces()) {
@ -3171,33 +3240,38 @@ gfxFont* gfxFontGroup::FindFontForChar(uint32_t aCh, uint32_t aPrevCh,
"should only do fallback once per font family");
}
#endif
font = FindFallbackFaceForChar(ff, aCh);
font = FindFallbackFaceForChar(ff, aCh, aNextCh, presentation);
if (font) {
*aMatchType = {FontMatchType::Kind::kFontGroup, ff.Generic()};
if (CheckCandidate(font,
{FontMatchType::Kind::kFontGroup, ff.Generic()})) {
return font;
}
}
} else {
// For platform fonts, but not user fonts, consider intra-family
// fallback to handle styles with reduced character sets (see
// also above).
gfxFontEntry* fe = ff.FontEntry();
if (fe && !fe->mIsUserFontContainer && !fe->IsUserFont()) {
font = FindFallbackFaceForChar(ff, aCh);
font = FindFallbackFaceForChar(ff, aCh, aNextCh, presentation);
if (font) {
*aMatchType = {FontMatchType::Kind::kFontGroup, ff.Generic()};
if (CheckCandidate(font,
{FontMatchType::Kind::kFontGroup, ff.Generic()})) {
return font;
}
}
}
}
}
if (fontListLength == 0) {
gfxFont* defaultFont = GetDefaultFont();
if (defaultFont->HasCharacter(aCh)) {
*aMatchType = FontMatchType::Kind::kFontGroup;
if (CheckCandidate(defaultFont, FontMatchType::Kind::kFontGroup)) {
return defaultFont;
}
}
}
// If character is in Private Use Area, or is unassigned in Unicode, don't do
// matching against pref or system fonts. We only support such codepoints
@ -3208,34 +3282,56 @@ gfxFont* gfxFontGroup::FindFontForChar(uint32_t aCh, uint32_t aPrevCh,
// fallback has already noted a failure.
if (gfxPlatformFontList::PlatformFontList()->SkipFontFallbackForChar(aCh) ||
GetGeneralCategory(aCh) == HB_UNICODE_GENERAL_CATEGORY_UNASSIGNED) {
return nullptr;
if (candidateFont) {
*aMatchType = candidateMatchType;
}
return candidateFont;
}
// 2. search pref fonts
gfxFont* font = WhichPrefFontSupportsChar(aCh, aNextCh);
gfxFont* font = WhichPrefFontSupportsChar(aCh, aNextCh, presentation);
if (font) {
*aMatchType = FontMatchType::Kind::kPrefsFallback;
if (CheckCandidate(font, FontMatchType::Kind::kPrefsFallback)) {
return font;
}
}
// For fallback searches, we don't want to use a color-emoji font unless
// emoji-style presentation is specifically required, so we map Any to
// Text here.
if (presentation == eFontPresentation::Any) {
presentation = eFontPresentation::Text;
}
// 3. use fallback fonts
// -- before searching for something else check the font used for the
// previous character
if (aPrevMatchedFont && aPrevMatchedFont->HasCharacter(aCh)) {
*aMatchType = FontMatchType::Kind::kSystemFallback;
if (CheckCandidate(aPrevMatchedFont,
FontMatchType::Kind::kSystemFallback)) {
return aPrevMatchedFont;
}
}
// for known "space" characters, don't do a full system-fallback search;
// we'll synthesize appropriate-width spaces instead of missing-glyph boxes
if (GetGeneralCategory(aCh) == HB_UNICODE_GENERAL_CATEGORY_SPACE_SEPARATOR &&
GetFirstValidFont()->SynthesizeSpaceWidth(aCh) >= 0.0) {
RefPtr<gfxFont> autoRefDeref(candidateFont);
return nullptr;
}
// -- otherwise look for other stuff
*aMatchType = FontMatchType::Kind::kSystemFallback;
return WhichSystemFontSupportsChar(aCh, aNextCh, aRunScript);
font = WhichSystemFontSupportsChar(aCh, aNextCh, aRunScript, presentation);
if (font) {
if (CheckCandidate(font, FontMatchType::Kind::kSystemFallback)) {
return font;
}
}
if (candidateFont) {
*aMatchType = candidateMatchType;
}
return candidateFont;
}
template <typename T>
@ -3307,7 +3403,8 @@ void gfxFontGroup::ComputeRanges(nsTArray<TextRange>& aRanges, const T* aString,
(!IsClusterExtender(ch) && ch != NARROW_NO_BREAK_SPACE &&
!gfxFontUtils::IsJoinControl(ch) &&
!gfxFontUtils::IsJoinCauser(prevCh) &&
!gfxFontUtils::IsVarSelector(ch)))) {
!gfxFontUtils::IsVarSelector(ch) &&
GetEmojiPresentation(ch) == TextOnly))) {
matchType = {FontMatchType::Kind::kFontGroup, mFonts[0].Generic()};
} else {
font =
@ -3493,12 +3590,12 @@ bool gfxFontGroup::ContainsUserFont(const gfxUserFontEntry* aUserFont) {
return false;
}
gfxFont* gfxFontGroup::WhichPrefFontSupportsChar(uint32_t aCh,
uint32_t aNextCh) {
gfxFont* gfxFontGroup::WhichPrefFontSupportsChar(
uint32_t aCh, uint32_t aNextCh, eFontPresentation aPresentation) {
eFontPrefLang charLang;
gfxPlatformFontList* pfl = gfxPlatformFontList::PlatformFontList();
if (ShouldPreferEmojiFont(aCh, aNextCh)) {
if (aPresentation == eFontPresentation::Emoji) {
charLang = eFontPrefLang_Emoji;
} else {
// get the pref font list if it hasn't been set up already
@ -3574,8 +3671,10 @@ gfxFont* gfxFontGroup::WhichPrefFontSupportsChar(uint32_t aCh,
// alternative face in the same family.
if (!prefFont) {
prefFont = family.mIsShared
? FindFallbackFaceForChar(family.mShared, aCh)
: FindFallbackFaceForChar(family.mUnshared, aCh);
? FindFallbackFaceForChar(family.mShared, aCh, aNextCh,
aPresentation)
: FindFallbackFaceForChar(family.mUnshared, aCh, aNextCh,
aPresentation);
}
if (prefFont) {
mLastPrefFamily = family;
@ -3593,14 +3692,15 @@ gfxFont* gfxFontGroup::WhichPrefFontSupportsChar(uint32_t aCh,
return nullptr;
}
gfxFont* gfxFontGroup::WhichSystemFontSupportsChar(uint32_t aCh,
uint32_t aNextCh,
Script aRunScript) {
gfxFont* gfxFontGroup::WhichSystemFontSupportsChar(
uint32_t aCh, uint32_t aNextCh, Script aRunScript,
eFontPresentation aPresentation) {
FontVisibility visibility;
gfxFontEntry* fe =
gfxFont* font =
gfxPlatformFontList::PlatformFontList()->SystemFindFontForChar(
aCh, aNextCh, aRunScript, &mStyle, &visibility, mFontMatchingStats);
if (fe) {
aCh, aNextCh, aRunScript, aPresentation, &mStyle, &visibility,
mFontMatchingStats);
if (font) {
if (mFontMatchingStats) {
switch (visibility) {
case FontVisibility::Unknown:
@ -3628,7 +3728,7 @@ gfxFont* gfxFontGroup::WhichSystemFontSupportsChar(uint32_t aCh,
break;
}
}
return fe->FindOrMakeFont(&mStyle);
return font;
}
return nullptr;

View File

@ -1113,10 +1113,12 @@ class gfxFontGroup final : public gfxTextRunFactory {
// search through pref fonts for a character, return nullptr if no matching
// pref font
gfxFont* WhichPrefFontSupportsChar(uint32_t aCh, uint32_t aNextCh);
gfxFont* WhichPrefFontSupportsChar(uint32_t aCh, uint32_t aNextCh,
eFontPresentation aPresentation);
gfxFont* WhichSystemFontSupportsChar(uint32_t aCh, uint32_t aNextCh,
Script aRunScript);
Script aRunScript,
eFontPresentation aPresentation);
template <typename T>
void ComputeRanges(nsTArray<TextRange>& aRanges, const T* aString,
@ -1468,12 +1470,17 @@ class gfxFontGroup final : public gfxTextRunFactory {
// Helper for font-matching:
// search all faces in a family for a fallback in cases where it's unclear
// whether the family might have a font for a given character
gfxFont* FindFallbackFaceForChar(const FamilyFace& aFamily, uint32_t aCh);
gfxFont* FindFallbackFaceForChar(const FamilyFace& aFamily, uint32_t aCh,
uint32_t aNextCh,
eFontPresentation aPresentation);
gfxFont* FindFallbackFaceForChar(mozilla::fontlist::Family* aFamily,
uint32_t aCh);
uint32_t aCh, uint32_t aNextCh,
eFontPresentation aPresentation);
gfxFont* FindFallbackFaceForChar(gfxFontFamily* aFamily, uint32_t aCh);
gfxFont* FindFallbackFaceForChar(gfxFontFamily* aFamily, uint32_t aCh,
uint32_t aNextCh,
eFontPresentation aPresentation);
// helper methods for looking up fonts

View File

@ -663,9 +663,9 @@ static const char kFontUtsaah[] = "Utsaah";
static const char kFontYuGothic[] = "Yu Gothic";
void gfxWindowsPlatform::GetCommonFallbackFonts(
uint32_t aCh, uint32_t aNextCh, Script aRunScript,
uint32_t aCh, Script aRunScript, eFontPresentation aPresentation,
nsTArray<const char*>& aFontList) {
if (ShouldPreferEmojiFont(aCh, aNextCh)) {
if (aPresentation == eFontPresentation::Emoji) {
aFontList.AppendElement(kFontSegoeUIEmoji);
aFontList.AppendElement(kFontTwemojiMozilla);
}

View File

@ -148,7 +148,8 @@ class gfxWindowsPlatform final : public gfxPlatform {
*/
void VerifyD2DDevice(bool aAttemptForce);
void GetCommonFallbackFonts(uint32_t aCh, uint32_t aNextCh, Script aRunScript,
void GetCommonFallbackFonts(uint32_t aCh, Script aRunScript,
eFontPresentation aPresentation,
nsTArray<const char*>& aFontList) override;
bool CanUseHardwareVideoDecoding() override;

View File

@ -182,19 +182,6 @@ inline EmojiPresentation GetEmojiPresentation(uint32_t aCh) {
return TextDefault;
}
inline bool ShouldPreferEmojiFont(uint32_t aCh, uint32_t aNextCh) {
EmojiPresentation emoji = GetEmojiPresentation(aCh);
if (emoji != EmojiPresentation::TextOnly) {
if (aNextCh == kVariationSelector16 ||
(aNextCh != kVariationSelector15 &&
emoji == EmojiPresentation::EmojiDefault) ||
(aNextCh >= kEmojiSkinToneFirst && aNextCh <= kEmojiSkinToneLast)) {
return true;
}
}
return false;
}
// returns the simplified Gen Category as defined in nsUGenCategory
inline nsUGenCategory GetGenCategory(uint32_t aCh) {
return sDetailedToGeneralCategory[GetGeneralCategory(aCh)];

View File

@ -12,7 +12,7 @@
</head>
<body>
<div id=test>&#x231a;&#x231b;&#x1f300;&#x1f301;</div>
<div id=test>&#x231a;&#xfe0e;&#x231b;&#xfe0e;&#x1f300;&#xfe0e;&#x1f301;&#xfe0e;</div>
</body>
</html>

View File

@ -5,6 +5,10 @@
<title>emoji fallback to text font</title>
<style>
#test {
/* Because of the presence of the U+FE0E variation selectors, we should prefer the
the monochrome fonts even though color-emoji fonts are listed first. */
font-family: 'Segoe UI Emoji', 'Twemoji Mozilla', 'Apple Color Emoji', 'Noto Color Emoji',
'Segoe UI Symbol', 'Apple Symbols', 'Noto Sans Symbols';
font-size: 24pt;
}
</style>

View File

@ -5,7 +5,7 @@
<title>emoji fallback to color font</title>
<style>
#test {
font-family: 'Segoe UI Emoji', 'Apple Color Emoji', 'Twemoji Mozilla', 'Noto Color Emoji';
font-family: 'Segoe UI Emoji', 'Twemoji Mozilla', 'Apple Color Emoji', 'Noto Color Emoji';
font-size: 24pt;
}
</style>

View File

@ -5,6 +5,10 @@
<title>emoji fallback to color font</title>
<style>
#test {
/* Because of the presence of the U+FE0F variation selectors, we should find
a color-emoji font even though the b/w symbol fonts are listed first. */
font-family: 'Segoe UI Symbol', 'Apple Symbols', 'Noto Sans Symbols',
'Segoe UI Emoji', 'Twemoji Mozilla', 'Apple Color Emoji', 'Noto Color Emoji';
font-size: 24pt;
}
</style>

View File

@ -19,4 +19,4 @@ span {
</style>
</head>
<body>
P<span> (fail) </span>A<span>&#x1234;&#x9876;&#xabcd;</span>S<span>&#x10400;&#x1f333;</span>S
P<span> (fail) </span>A<span>&#x1234;&#x9876;&#xabcd;</span>S<span>&#x10400;&#x1f333;&#xfe0e;</span>S

View File

@ -187,7 +187,7 @@ random-if(!winWidget) == arial-bold-lam-alef-1.html arial-bold-lam-alef-1-ref.ht
fails-if(/^Windows\x20NT\x206\.1/.test(http.oscpu)) == 1320665-cmap-format-13.html 1320665-cmap-format-13-ref.html # see bug 1320665 comments 8-9
== 1331339-script-extensions-shaping-1.html 1331339-script-extensions-shaping-1-ref.html
skip-if(!cocoaWidget) != 1349308-1.html 1349308-notref.html # macOS-specific test for -apple-system glyph metrics
fuzzy-if(Android,0-128,0-233) fails-if(/^Windows\x20NT\x2010\.0/.test(http.oscpu)) == 1463020-letter-spacing-text-transform-1.html 1463020-letter-spacing-text-transform-1-ref.html # Win10: regional indicators not supported by system emoji font
fuzzy-if(Android,0-128,0-233) == 1463020-letter-spacing-text-transform-1.html 1463020-letter-spacing-text-transform-1-ref.html
fails-if(Android) == 1463020-letter-spacing-text-transform-2.html 1463020-letter-spacing-text-transform-2-ref.html # missing font coverage on Android
== 1507661-spurious-hyphenation-after-explicit.html 1507661-spurious-hyphenation-after-explicit-ref.html
fuzzy-if(!webrender,12-66,288-1681) fails-if(gtkWidget&&!webrender) == 1522857-1.html 1522857-1-ref.html # antialiasing fuzz in non-webrender cases

View File

@ -21,7 +21,9 @@
<script>
<![CDATA[
function text(n) {
return "\u{1F310}".repeat(n);
// Include U+FE0E variation selector to ensure font selection doesn't
// seek out a color-emoji font in preference to Fira.
return "\u{1F310}\u{FE0E}".repeat(n);
}
var e = "\u{2026}";
document.getElementById("start").value = e + text(4);

View File

@ -21,7 +21,9 @@
<script>
<![CDATA[
function text(n) {
return "\u{1F310}".repeat(n);
// Include U+FE0E variation selector to ensure font selection doesn't
// seek out a color-emoji font in preference to Fira.
return "\u{1F310}\u{FE0E}".repeat(n);
}
document.getElementById("start").value = text(10);
document.getElementById("end").value = text(10);