Support application fallbacks for common script

While common script typically adapts the surrounding script, it is
possible for strings of only punctuation (for instance) to remain
undetermined. In addition, Qt considers Latin and Common the same in
font matching, so in order to get merging for latin, we need to
conflate them in the application fallback API as well.

This also adds a couple of missing clears of caches (clearing
the font cache itself when adding a new fallback, since the
fallbacks are kept as part of the cached font engine + clearing
the fallback cache when adding a new application font, since the
new application font may be a fallback candidate).

Note: This also adds some missing removeApplicationFont() calls
from other tests, since these were causing issues with the
new test.

Task-number: QTBUG-124914
Change-Id: Idbfa0f6b492a9194eca67b57101e674f7b8a4ec4
Reviewed-by: Eirik Aavitsland <eirik.aavitsland@qt.io>
bb10
Eskil Abrahamsen Blomfeldt 2024-05-02 14:30:57 +02:00
parent daf494c585
commit 99e94f1035
5 changed files with 160 additions and 8 deletions

View File

@ -670,7 +670,7 @@ static QStringList fallbacksForFamily(const QString &family, QFont::Style style,
return *fallbacks;
// make sure that the db has all fallback families
QStringList userFallbacks = db->applicationFallbackFontFamilies.value(script == QChar::Script_Common ? QChar::Script_Latin : script);
QStringList userFallbacks = db->applicationFallbackFontFamilies.value(script == QChar::Script_Latin ? QChar::Script_Common : script);
QStringList retList = userFallbacks + QGuiApplicationPrivate::platformIntegration()->fontDatabase()->fallbacksForFamily(family,style,styleHint,script);
QStringList::iterator i;
@ -2187,6 +2187,8 @@ int QFontDatabasePrivate::addAppFont(const QByteArray &fontData, const QString &
// loaded, so it has to be flushed.
QFontCache::instance()->clear();
fallbacksCache.clear();
emit qApp->fontDatabaseChanged();
return i;
@ -2386,23 +2388,32 @@ bool QFontDatabase::removeAllApplicationFonts()
be prioritized in reverse order, so that the last family added will be checked first and so
on.
\note Qt's font matching algorithm considers \c{QChar::Script_Common} (undetermined script)
and \c{QChar::Script_Latin} the same. Adding a fallback for either of these will also apply
to the other.
\sa setApplicationFallbackFontFamilies(), removeApplicationFallbackFontFamily(), applicationFallbackFontFamilies()
*/
void QFontDatabase::addApplicationFallbackFontFamily(QChar::Script script, const QString &familyName)
{
QMutexLocker locker(fontDatabaseMutex());
if (script < QChar::Script_Latin) {
if (script < QChar::Script_Common) {
qCWarning(lcFontDb) << "Invalid script passed to addApplicationFallbackFontFamily:" << script;
return;
}
if (script == QChar::Script_Latin)
script = QChar::Script_Common;
auto *db = QFontDatabasePrivate::instance();
auto it = db->applicationFallbackFontFamilies.find(script);
if (it == db->applicationFallbackFontFamilies.end())
it = db->applicationFallbackFontFamilies.insert(script, QStringList{});
it->prepend(familyName);
QFontCache::instance()->clear();
db->fallbacksCache.clear();
}
@ -2420,6 +2431,14 @@ bool QFontDatabase::removeApplicationFallbackFontFamily(QChar::Script script, co
{
QMutexLocker locker(fontDatabaseMutex());
if (script < QChar::Script_Common) {
qCWarning(lcFontDb) << "Invalid script passed to removeApplicationFallbackFontFamily:" << script;
return false;
}
if (script == QChar::Script_Latin)
script = QChar::Script_Common;
auto *db = QFontDatabasePrivate::instance();
auto it = db->applicationFallbackFontFamilies.find(script);
if (it != db->applicationFallbackFontFamilies.end()) {
@ -2452,11 +2471,14 @@ void QFontDatabase::setApplicationFallbackFontFamilies(QChar::Script script, con
{
QMutexLocker locker(fontDatabaseMutex());
if (script < QChar::Script_Latin) {
if (script < QChar::Script_Common) {
qCWarning(lcFontDb) << "Invalid script passed to setApplicationFallbackFontFamilies:" << script;
return;
}
if (script == QChar::Script_Latin)
script = QChar::Script_Common;
auto *db = QFontDatabasePrivate::instance();
db->applicationFallbackFontFamilies[script] = familyNames;
@ -2476,6 +2498,9 @@ QStringList QFontDatabase::applicationFallbackFontFamilies(QChar::Script script)
{
QMutexLocker locker(fontDatabaseMutex());
if (script == QChar::Script_Latin)
script = QChar::Script_Common;
auto *db = QFontDatabasePrivate::instance();
return db->applicationFallbackFontFamilies.value(script);
}

View File

@ -47,6 +47,8 @@ set(testdata_resource_files
"../../../shared/resources/testfont_open.otf"
"../../../shared/resources/testfont_variable.ttf"
"LED_REAL.TTF"
"QtTestLimitedFont-Regular.ttf"
"QtTestFallbackFont-Regular.ttf"
)
qt_internal_add_resource(tst_qfontdatabase "testdata"

View File

@ -78,6 +78,8 @@ private:
QString m_testFontCondensed;
QString m_testFontItalic;
QString m_testFontVariable;
QString m_limitedFont;
QString m_fallbackFont;
};
tst_QFontDatabase::tst_QFontDatabase()
@ -91,11 +93,15 @@ void tst_QFontDatabase::initTestCase()
m_testFontCondensed = QFINDTESTDATA("testfont_condensed.ttf");
m_testFontItalic = QFINDTESTDATA("testfont_italic.ttf");
m_testFontVariable = QFINDTESTDATA("testfont_variable.ttf");
m_limitedFont = QFINDTESTDATA("QtTestLimitedFont-Regular.ttf");
m_fallbackFont = QFINDTESTDATA("QtTestFallbackFont-Regular.ttf");
QVERIFY(!m_ledFont.isEmpty());
QVERIFY(!m_testFont.isEmpty());
QVERIFY(!m_testFontCondensed.isEmpty());
QVERIFY(!m_testFontItalic.isEmpty());
QVERIFY(!m_testFontVariable.isEmpty());
QVERIFY(!m_limitedFont.isEmpty());
QVERIFY(!m_fallbackFont.isEmpty());
}
void tst_QFontDatabase::styles_data()
@ -391,8 +397,14 @@ void tst_QFontDatabase::condensedFontWidthNoFontMerging()
void tst_QFontDatabase::condensedFontWidth()
{
QFontDatabase::addApplicationFont(m_testFont);
QFontDatabase::addApplicationFont(m_testFontCondensed);
int testFontId = QFontDatabase::addApplicationFont(m_testFont);
int testFontCondensedId = QFontDatabase::addApplicationFont(m_testFontCondensed);
auto cleanup = qScopeGuard([&testFontId, &testFontCondensedId] {
if (testFontId >= 0)
QFontDatabase::removeApplicationFont(testFontId);
if (testFontCondensedId >= 0)
QFontDatabase::removeApplicationFont(testFontCondensedId);
});
QVERIFY(QFontDatabase::hasFamily("QtBidiTestFont"));
if (!QFontDatabase::hasFamily("QtBidiTestFontCondensed"))
@ -410,10 +422,16 @@ void tst_QFontDatabase::condensedFontWidth()
void tst_QFontDatabase::condensedFontMatching()
{
QFontDatabase::removeAllApplicationFonts();
QFontDatabase::addApplicationFont(m_testFontCondensed);
int testFontCondensedId = QFontDatabase::addApplicationFont(m_testFontCondensed);
if (!QFontDatabase::hasFamily("QtBidiTestFont"))
QSKIP("This platform doesn't support preferred font family names (QTBUG-53478)");
QFontDatabase::addApplicationFont(m_testFont);
int testFontId = QFontDatabase::addApplicationFont(m_testFont);
auto cleanup = qScopeGuard([&testFontId, &testFontCondensedId] {
if (testFontId >= 0)
QFontDatabase::removeApplicationFont(testFontId);
if (testFontCondensedId >= 0)
QFontDatabase::removeApplicationFont(testFontCondensedId);
});
// Test we correctly get the condensed font using different font matching methods:
QFont tfcByStretch("QtBidiTestFont");
@ -561,11 +579,17 @@ void tst_QFontDatabase::addApplicationFontFallback()
{
int ledId = -1;
int id = -1;
auto cleanup = qScopeGuard([&id, &ledId] {
int limitedId = -1;
int fallbackId = -1;
auto cleanup = qScopeGuard([&id, &ledId, &limitedId, &fallbackId] {
if (id >= 0)
QFontDatabase::removeApplicationFont(id);
if (ledId >= 0)
QFontDatabase::removeApplicationFont(ledId);
if (limitedId >= 0)
QFontDatabase::removeApplicationFont(limitedId);
if (fallbackId >= 0)
QFontDatabase::removeApplicationFont(fallbackId);
});
const QChar hebrewChar(0x05D0); // Hebrew 'aleph'
@ -633,6 +657,107 @@ void tst_QFontDatabase::addApplicationFontFallback()
QCOMPARE(hebrewFontNow, defaultHebrewFont);
}
limitedId = QFontDatabase::addApplicationFont(m_limitedFont);
QVERIFY(limitedId >= 0);
fallbackId = QFontDatabase::addApplicationFont(m_fallbackFont);
QVERIFY(fallbackId >= 0);
QFontDatabase::addApplicationFallbackFontFamily(QChar::Script_Common, u"QtTestFallbackFont"_s);
// The fallback for Common will be used also for Latin, because Latin and Common are
// considered the same script by the font matching engine.
{
QTextLayout layout;
layout.setText(u"A'B,"_s);
layout.setFont(QFont(u"QtTestLimitedFont"_s));
layout.beginLayout();
layout.createLine();
layout.endLayout();
QList<QGlyphRun> glyphRuns = layout.glyphRuns();
QVERIFY(glyphRuns.size() > 1);
for (int i = 0; i < glyphRuns.size(); ++i) {
QVERIFY(glyphRuns.at(i).rawFont().familyName() == u"QtTestFallbackFont"_s
|| glyphRuns.at(i).rawFont().familyName() == u"QtTestLimitedFont"_s);
}
}
// When the text only consists of common script characters, the fallback font will also be used.
{
QTextLayout layout;
layout.setText(u"',"_s);
layout.setFont(QFont(u"QtTestLimitedFont"_s));
layout.beginLayout();
layout.createLine();
layout.endLayout();
QList<QGlyphRun> glyphRuns = layout.glyphRuns();
QCOMPARE(glyphRuns.size(), 2);
for (int i = 0; i < glyphRuns.size(); ++i) {
QVERIFY(glyphRuns.at(i).rawFont().familyName() == u"QtTestFallbackFont"_s
|| glyphRuns.at(i).rawFont().familyName() == u"QtTestLimitedFont"_s);
}
}
QVERIFY(QFontDatabase::removeApplicationFallbackFontFamily(QChar::Script_Common, u"QtTestFallbackFont"_s));
QFontDatabase::addApplicationFallbackFontFamily(QChar::Script_Latin, u"QtTestFallbackFont"_s);
// Latin fallback works just the same as Common fallback
{
QTextLayout layout;
layout.setText(u"A'B,"_s);
layout.setFont(QFont(u"QtTestLimitedFont"_s));
layout.beginLayout();
layout.createLine();
layout.endLayout();
QList<QGlyphRun> glyphRuns = layout.glyphRuns();
QCOMPARE(glyphRuns.size(), 2);
for (int i = 0; i < glyphRuns.size(); ++i) {
QVERIFY(glyphRuns.at(i).rawFont().familyName() == u"QtTestFallbackFont"_s
|| glyphRuns.at(i).rawFont().familyName() == u"QtTestLimitedFont"_s);
}
}
// When the common character is placed next to a Cyrillic characters, it gets adapted to this,
// so the fallback font will not be selected, even if it supports the character in question
{
QTextLayout layout;
layout.setText(u"A'Б,"_s);
layout.setFont(QFont(u"QtTestLimitedFont"_s));
layout.beginLayout();
layout.createLine();
layout.endLayout();
QList<QGlyphRun> glyphRuns = layout.glyphRuns();
QCOMPARE(glyphRuns.size(), 2);
for (int i = 0; i < glyphRuns.size(); ++i) {
QVERIFY(glyphRuns.at(i).rawFont().familyName() != u"QtTestFallbackFont"_s);
}
}
QFontDatabase::addApplicationFallbackFontFamily(QChar::Script_Cyrillic, u"QtTestFallbackFont"_s);
// When we set the fallback font for Cyrillic as well, it gets selected
{
QTextLayout layout;
layout.setText(u"A'Б,"_s);
layout.setFont(QFont(u"QtTestLimitedFont"_s));
layout.beginLayout();
layout.createLine();
layout.endLayout();
QList<QGlyphRun> glyphRuns = layout.glyphRuns();
QCOMPARE(glyphRuns.size(), 2);
for (int i = 0; i < glyphRuns.size(); ++i) {
QVERIFY(glyphRuns.at(i).rawFont().familyName() == u"QtTestFallbackFont"_s
|| glyphRuns.at(i).rawFont().familyName() == u"QtTestLimitedFont"_s);
}
}
QVERIFY(QFontDatabase::removeApplicationFallbackFontFamily(QChar::Script_Cyrillic, u"QtTestFallbackFont"_s));
QVERIFY(QFontDatabase::removeApplicationFallbackFontFamily(QChar::Script_Latin, u"QtTestFallbackFont"_s));
}
QTEST_MAIN(tst_QFontDatabase)