Colorful emojis in Unicode are not isolated to specific ranges of code points like other writing systems. Instead, there are a set of rules defining whether a sequence of characters should be displayed in color or black/white. http://www.unicode.org/reports/tr51/ For instance, appending a variation selector to a character can turn it into a color emoji, even if it is a code point that predates the invention of emojis. In addition, sequences of joined characters that are determined to be a color emoji sequence should be parsed by a single emoji font, so that it can apply things like skin color, etc. In general, users expect emojis and emoji sequences to be shown in the preferred color font of the system, even if a selected font has black/white characters for the symbols. This patch applies the emoji segmenter to strings to isolate sequences that should be in color. As an implementation hack, we mark this in the QScriptItems as a special "emoji" script. Note that this is not a real Unicode script and only exists internally for this reason, because the "emojiness" of the resulting glyph overrides the original script of the individual characters when selecting fonts. This way, we can use a lot of the same logic for itemizing the strings and looking up fonts, and we don't need to increase the size of the QScriptItem. (It is just an implementation detail and is not exposed to the user, so it can be replaced by other approaches later if we need to.) When matching an emoji sequence, we always try to apply a color font and ignore all others. The exception is if there is no color font at all on the system, then we will find a black and white font which supports the characters instead as a final failsafe. In addition, each platform will put its default emoji font at the top of the fallbacks list in order to make this the preference in case there are more than one. This patch also adds API to override this with an application-defined emoji font, since this is a common use case. Note: The font includes an environment variable to disable the feature as a fail safe. A flag to disable it per QFont will be added in a follow-up. Fixes: QTBUG-111801 Change-Id: I9431ec34d56772ab8688814963073b83b23002ae Reviewed-by: Lars Knoll <lars@knoll.priv.no> Reviewed-by: <carl@carlschwan.eu>
405 lines
12 KiB
C++
405 lines
12 KiB
C++
// Copyright (C) 2018 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
|
|
|
#include "qwasmfontdatabase.h"
|
|
#include "qwasmintegration.h"
|
|
|
|
#include <QtCore/qfile.h>
|
|
#include <QtCore/private/qstdweb_p.h>
|
|
#include <QtCore/private/qeventdispatcher_wasm_p.h>
|
|
#include <QtGui/private/qguiapplication_p.h>
|
|
|
|
#include <emscripten.h>
|
|
#include <emscripten/val.h>
|
|
#include <emscripten/bind.h>
|
|
|
|
#include <map>
|
|
#include <array>
|
|
|
|
QT_BEGIN_NAMESPACE
|
|
|
|
using namespace emscripten;
|
|
using namespace Qt::StringLiterals;
|
|
|
|
|
|
namespace {
|
|
|
|
class FontData
|
|
{
|
|
public:
|
|
FontData(val fontData)
|
|
:m_fontData(fontData) {}
|
|
|
|
QString family() const
|
|
{
|
|
return QString::fromStdString(m_fontData["family"].as<std::string>());
|
|
}
|
|
|
|
QString fullName() const
|
|
{
|
|
return QString::fromStdString(m_fontData["fullName"].as<std::string>());
|
|
}
|
|
|
|
QString postscriptName() const
|
|
{
|
|
return QString::fromStdString(m_fontData["postscriptName"].as<std::string>());
|
|
}
|
|
|
|
QString style() const
|
|
{
|
|
return QString::fromStdString(m_fontData["style"].as<std::string>());
|
|
}
|
|
|
|
val value() const
|
|
{
|
|
return m_fontData;
|
|
}
|
|
|
|
private:
|
|
val m_fontData;
|
|
};
|
|
|
|
val makeObject(const char *key, const char *value)
|
|
{
|
|
val obj = val::object();
|
|
obj.set(key, std::string(value));
|
|
return obj;
|
|
}
|
|
|
|
void printError(val err) {
|
|
qCWarning(lcQpaFonts)
|
|
<< QString::fromStdString(err["name"].as<std::string>())
|
|
<< QString::fromStdString(err["message"].as<std::string>());
|
|
QWasmFontDatabase::endAllFontFileLoading();
|
|
}
|
|
|
|
void checkFontAccessPermitted(std::function<void(bool)> callback)
|
|
{
|
|
const val permissions = val::global("navigator")["permissions"];
|
|
if (permissions.isUndefined()) {
|
|
callback(false);
|
|
return;
|
|
}
|
|
|
|
qstdweb::Promise::make(permissions, "query", {
|
|
.thenFunc = [callback](val status) {
|
|
callback(status["state"].as<std::string>() == "granted");
|
|
},
|
|
}, makeObject("name", "local-fonts"));
|
|
}
|
|
|
|
void queryLocalFonts(std::function<void(const QList<FontData> &)> callback)
|
|
{
|
|
emscripten::val window = emscripten::val::global("window");
|
|
qstdweb::Promise::make(window, "queryLocalFonts", {
|
|
.thenFunc = [callback](emscripten::val fontArray) {
|
|
QList<FontData> fonts;
|
|
const int count = fontArray["length"].as<int>();
|
|
fonts.reserve(count);
|
|
for (int i = 0; i < count; ++i)
|
|
fonts.append(FontData(fontArray.call<emscripten::val>("at", i)));
|
|
callback(fonts);
|
|
},
|
|
.catchFunc = printError
|
|
});
|
|
}
|
|
|
|
void readBlob(val blob, std::function<void(const QByteArray &)> callback)
|
|
{
|
|
qstdweb::Promise::make(blob, "arrayBuffer", {
|
|
.thenFunc = [callback](emscripten::val fontArrayBuffer) {
|
|
QByteArray fontData = qstdweb::Uint8Array(qstdweb::ArrayBuffer(fontArrayBuffer)).copyToQByteArray();
|
|
callback(fontData);
|
|
},
|
|
.catchFunc = printError
|
|
});
|
|
}
|
|
|
|
void readFont(FontData font, std::function<void(const QByteArray &)> callback)
|
|
{
|
|
qstdweb::Promise::make(font.value(), "blob", {
|
|
.thenFunc = [callback](val blob) {
|
|
readBlob(blob, [callback](const QByteArray &data) {
|
|
callback(data);
|
|
});
|
|
},
|
|
.catchFunc = printError
|
|
});
|
|
}
|
|
|
|
emscripten::val getLocalFontsConfigProperty(const char *name) {
|
|
emscripten::val qt = val::module_property("qt");
|
|
if (qt.isUndefined())
|
|
return emscripten::val();
|
|
emscripten::val localFonts = qt["localFonts"];
|
|
if (localFonts.isUndefined())
|
|
return emscripten::val();
|
|
return localFonts[name];
|
|
};
|
|
|
|
bool getLocalFontsBoolConfigPropertyWithDefault(const char *name, bool defaultValue) {
|
|
emscripten::val prop = getLocalFontsConfigProperty(name);
|
|
if (prop.isUndefined())
|
|
return defaultValue;
|
|
return prop.as<bool>();
|
|
};
|
|
|
|
QString getLocalFontsStringConfigPropertyWithDefault(const char *name, QString defaultValue) {
|
|
emscripten::val prop = getLocalFontsConfigProperty(name);
|
|
if (prop.isUndefined())
|
|
return defaultValue;
|
|
return QString::fromStdString(prop.as<std::string>());
|
|
};
|
|
|
|
QStringList getLocalFontsStringListConfigPropertyWithDefault(const char *name, QStringList defaultValue) {
|
|
emscripten::val array = getLocalFontsConfigProperty(name);
|
|
if (array.isUndefined())
|
|
return defaultValue;
|
|
|
|
QStringList list;
|
|
int size = array["length"].as<int>();
|
|
for (int i = 0; i < size; ++i) {
|
|
emscripten::val element = array.call<emscripten::val>("at", i);
|
|
QString string = QString::fromStdString(element.as<std::string>());
|
|
if (!string.isEmpty())
|
|
list.append(string);
|
|
}
|
|
return list;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
QWasmFontDatabase::QWasmFontDatabase()
|
|
:QFreeTypeFontDatabase()
|
|
{
|
|
m_localFontsApiSupported = val::global("window")["queryLocalFonts"].isUndefined() == false;
|
|
if (m_localFontsApiSupported)
|
|
beginFontDatabaseStartupTask();
|
|
}
|
|
|
|
QWasmFontDatabase *QWasmFontDatabase::get()
|
|
{
|
|
return static_cast<QWasmFontDatabase *>(QWasmIntegration::get()->fontDatabase());
|
|
}
|
|
|
|
// Populates the font database with local fonts. Will make the browser ask
|
|
// the user for permission if needed. Does nothing if the Local Font Access API
|
|
// is not supported.
|
|
void QWasmFontDatabase::populateLocalfonts()
|
|
{
|
|
// Decide which font families to populate based on user preferences
|
|
QStringList selectedLocalFontFamilies;
|
|
bool allFamilies = false;
|
|
|
|
switch (m_localFontFamilyLoadSet) {
|
|
case NoFontFamilies:
|
|
default:
|
|
// keep empty selectedLocalFontFamilies
|
|
break;
|
|
case DefaultFontFamilies: {
|
|
const QStringList webSafeFontFamilies =
|
|
{"Arial", "Verdana", "Tahoma", "Trebuchet", "Times New Roman",
|
|
"Georgia", "Garamond", "Courier New"};
|
|
selectedLocalFontFamilies = webSafeFontFamilies;
|
|
} break;
|
|
case AllFontFamilies:
|
|
allFamilies = true;
|
|
break;
|
|
}
|
|
|
|
selectedLocalFontFamilies += m_extraLocalFontFamilies;
|
|
|
|
if (selectedLocalFontFamilies.isEmpty() && !allFamilies) {
|
|
endAllFontFileLoading();
|
|
return;
|
|
}
|
|
|
|
populateLocalFontFamilies(selectedLocalFontFamilies, allFamilies);
|
|
}
|
|
|
|
namespace {
|
|
QStringList toStringList(emscripten::val array)
|
|
{
|
|
QStringList list;
|
|
int size = array["length"].as<int>();
|
|
for (int i = 0; i < size; ++i) {
|
|
emscripten::val element = array.call<emscripten::val>("at", i);
|
|
QString string = QString::fromStdString(element.as<std::string>());
|
|
if (!string.isEmpty())
|
|
list.append(string);
|
|
}
|
|
return list;
|
|
}
|
|
}
|
|
|
|
void QWasmFontDatabase::populateLocalFontFamilies(emscripten::val families)
|
|
{
|
|
if (!m_localFontsApiSupported)
|
|
return;
|
|
populateLocalFontFamilies(toStringList(families), false);
|
|
}
|
|
|
|
void QWasmFontDatabase::populateLocalFontFamilies(const QStringList &fontFamilies, bool allFamilies)
|
|
{
|
|
queryLocalFonts([fontFamilies, allFamilies](const QList<FontData> &fonts) {
|
|
refFontFileLoading();
|
|
QList<FontData> filteredFonts;
|
|
std::copy_if(fonts.begin(), fonts.end(), std::back_inserter(filteredFonts),
|
|
[fontFamilies, allFamilies](FontData fontData) {
|
|
return allFamilies || fontFamilies.contains(fontData.family());
|
|
});
|
|
|
|
for (const FontData &font: filteredFonts) {
|
|
refFontFileLoading();
|
|
readFont(font, [font](const QByteArray &fontData){
|
|
QFreeTypeFontDatabase::registerFontFamily(font.family());
|
|
QFreeTypeFontDatabase::addTTFile(fontData, QByteArray());
|
|
derefFontFileLoading();
|
|
});
|
|
}
|
|
derefFontFileLoading();
|
|
});
|
|
|
|
}
|
|
|
|
void QWasmFontDatabase::populateFontDatabase()
|
|
{
|
|
// Load bundled font file from resources.
|
|
const QString fontFileNames[] = {
|
|
QStringLiteral(":/fonts/DejaVuSansMono.ttf"),
|
|
QStringLiteral(":/fonts/DejaVuSans.ttf"),
|
|
};
|
|
for (const QString &fontFileName : fontFileNames) {
|
|
QFile theFont(fontFileName);
|
|
if (!theFont.open(QIODevice::ReadOnly))
|
|
break;
|
|
|
|
QFreeTypeFontDatabase::addTTFile(theFont.readAll(), fontFileName.toLatin1());
|
|
}
|
|
|
|
// Get config options for controlling local fonts usage
|
|
m_queryLocalFontsPermission = getLocalFontsBoolConfigPropertyWithDefault("requestPermission", false);
|
|
QString fontFamilyLoadSet = getLocalFontsStringConfigPropertyWithDefault("familiesCollection", "DefaultFontFamilies");
|
|
m_extraLocalFontFamilies = getLocalFontsStringListConfigPropertyWithDefault("extraFamilies", QStringList());
|
|
|
|
if (fontFamilyLoadSet == "NoFontFamilies") {
|
|
m_localFontFamilyLoadSet = NoFontFamilies;
|
|
} else if (fontFamilyLoadSet == "DefaultFontFamilies") {
|
|
m_localFontFamilyLoadSet = DefaultFontFamilies;
|
|
} else if (fontFamilyLoadSet == "AllFontFamilies") {
|
|
m_localFontFamilyLoadSet = AllFontFamilies;
|
|
} else {
|
|
m_localFontFamilyLoadSet = NoFontFamilies;
|
|
qWarning() << "Unknown fontFamilyLoadSet value" << fontFamilyLoadSet;
|
|
}
|
|
|
|
if (!m_localFontsApiSupported)
|
|
return;
|
|
|
|
// Populate the font database with local fonts. Either try unconditianlly
|
|
// if displyaing a fonts permissions dialog at startup is allowed, or else
|
|
// only if we already have permission.
|
|
if (m_queryLocalFontsPermission) {
|
|
populateLocalfonts();
|
|
} else {
|
|
checkFontAccessPermitted([this](bool granted) {
|
|
if (granted)
|
|
populateLocalfonts();
|
|
else
|
|
endAllFontFileLoading();
|
|
});
|
|
}
|
|
}
|
|
|
|
QFontEngine *QWasmFontDatabase::fontEngine(const QFontDef &fontDef, void *handle)
|
|
{
|
|
QFontEngine *fontEngine = QFreeTypeFontDatabase::fontEngine(fontDef, handle);
|
|
return fontEngine;
|
|
}
|
|
|
|
QStringList QWasmFontDatabase::fallbacksForFamily(const QString &family, QFont::Style style,
|
|
QFont::StyleHint styleHint,
|
|
QFontDatabasePrivate::ExtendedScript script) const
|
|
{
|
|
QStringList fallbacks
|
|
= QFreeTypeFontDatabase::fallbacksForFamily(family, style, styleHint, script);
|
|
|
|
// Add the DejaVuSans.ttf font (loaded in populateFontDatabase above) as a falback font
|
|
// to all other fonts (except itself).
|
|
static const QString wasmFallbackFonts[] = { "DejaVu Sans" };
|
|
for (auto wasmFallbackFont : wasmFallbackFonts) {
|
|
if (family != wasmFallbackFont && !fallbacks.contains(wasmFallbackFont))
|
|
fallbacks.append(wasmFallbackFont);
|
|
}
|
|
|
|
return fallbacks;
|
|
}
|
|
|
|
void QWasmFontDatabase::releaseHandle(void *handle)
|
|
{
|
|
QFreeTypeFontDatabase::releaseHandle(handle);
|
|
}
|
|
|
|
QFont QWasmFontDatabase::defaultFont() const
|
|
{
|
|
return QFont("DejaVu Sans"_L1);
|
|
}
|
|
|
|
namespace {
|
|
int g_pendingFonts = 0;
|
|
bool g_fontStartupTaskCompleted = false;
|
|
}
|
|
|
|
// Registers font loading as a startup task, which makes Qt delay
|
|
// sending onLoaded event until font loading has completed.
|
|
void QWasmFontDatabase::beginFontDatabaseStartupTask()
|
|
{
|
|
g_fontStartupTaskCompleted = false;
|
|
QEventDispatcherWasm::registerStartupTask();
|
|
}
|
|
|
|
// Ends the font loading startup task.
|
|
void QWasmFontDatabase::endFontDatabaseStartupTask()
|
|
{
|
|
if (!g_fontStartupTaskCompleted) {
|
|
g_fontStartupTaskCompleted = true;
|
|
QEventDispatcherWasm::completeStarupTask();
|
|
}
|
|
}
|
|
|
|
// Registers that a font file will be loaded.
|
|
void QWasmFontDatabase::refFontFileLoading()
|
|
{
|
|
g_pendingFonts += 1;
|
|
}
|
|
|
|
// Registers that one font file has been loaded, and sends notifactions
|
|
// when all pending font files have been loaded.
|
|
void QWasmFontDatabase::derefFontFileLoading()
|
|
{
|
|
if (--g_pendingFonts <= 0) {
|
|
QFontCache::instance()->clear();
|
|
emit qGuiApp->fontDatabaseChanged();
|
|
endFontDatabaseStartupTask();
|
|
}
|
|
}
|
|
|
|
// Unconditionally ends local font loading, for instance if there
|
|
// are no fonts to load or if there was an unexpected error.
|
|
void QWasmFontDatabase::endAllFontFileLoading()
|
|
{
|
|
bool hadPandingfonts = g_pendingFonts > 0;
|
|
if (hadPandingfonts) {
|
|
// The hadPandingfonts counter might no longer be correct; disable counting
|
|
// and send notifications unconditionally.
|
|
g_pendingFonts = 0;
|
|
QFontCache::instance()->clear();
|
|
emit qGuiApp->fontDatabaseChanged();
|
|
}
|
|
|
|
endFontDatabaseStartupTask();
|
|
}
|
|
|
|
|
|
QT_END_NAMESPACE
|