From 99e94f10352fe333a92195ca6d5482392231f307 Mon Sep 17 00:00:00 2001 From: Eskil Abrahamsen Blomfeldt Date: Thu, 2 May 2024 14:30:57 +0200 Subject: [PATCH] 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 --- src/gui/text/qfontdatabase.cpp | 31 +++- .../gui/text/qfontdatabase/CMakeLists.txt | 2 + .../QtTestFallbackFont-Regular.ttf | Bin 0 -> 5664 bytes .../QtTestLimitedFont-Regular.ttf | Bin 0 -> 5280 bytes .../text/qfontdatabase/tst_qfontdatabase.cpp | 135 +++++++++++++++++- 5 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 tests/auto/gui/text/qfontdatabase/QtTestFallbackFont-Regular.ttf create mode 100644 tests/auto/gui/text/qfontdatabase/QtTestLimitedFont-Regular.ttf diff --git a/src/gui/text/qfontdatabase.cpp b/src/gui/text/qfontdatabase.cpp index d3a13d801b..3d6d3b7886 100644 --- a/src/gui/text/qfontdatabase.cpp +++ b/src/gui/text/qfontdatabase.cpp @@ -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); } diff --git a/tests/auto/gui/text/qfontdatabase/CMakeLists.txt b/tests/auto/gui/text/qfontdatabase/CMakeLists.txt index 18b96ded5d..0cb6e8d7c8 100644 --- a/tests/auto/gui/text/qfontdatabase/CMakeLists.txt +++ b/tests/auto/gui/text/qfontdatabase/CMakeLists.txt @@ -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" diff --git a/tests/auto/gui/text/qfontdatabase/QtTestFallbackFont-Regular.ttf b/tests/auto/gui/text/qfontdatabase/QtTestFallbackFont-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ae21fec9a5de01d81e817ce2d4ef14365a243b0a GIT binary patch literal 5664 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 S!AcANUV4iU4lDd7So1#=DWJaq literal 0 HcmV?d00001 diff --git a/tests/auto/gui/text/qfontdatabase/QtTestLimitedFont-Regular.ttf b/tests/auto/gui/text/qfontdatabase/QtTestLimitedFont-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2891f8aeffb1d64764ef072f1ac49956b9aba292 GIT binary patch literal 5280 zcmbVQU2GKB6+SaNJG(Z#Dh8Th|CfOlPnDMTEXfZfI zz)Al|6I!}aqEwZliu56`NPW0tXiAd)pgcutm4`l6O{=PXXey=ZTdOopaKAIN-r58j zwX@ne-#zEtbI&>V+9(ne5#AM9Mt4PGP}aRL1Bu+P^{jVBx}f z0g9{sSGd*AF3v4Xr~mmJk=ll4&76B~k%dIDAAm;ZPCYyO-fPeO8LYp+#*6baZn5jv zm$>g=@gJB6qvoet7Bcjww)utgOSS({47A@vyZ+Rff=eIKMZn*oUA^EgEhAK#9woWQ@aiNd?&h07OwUv)oKGb#JQXmWNn$OXwx7R6Z&y-gwOwsVwa34(e1vGS(X4OV`4^ia`RS>}bgF+~`NYESl)a^r5 z)ZbBl1UAP)p`VAZf(WAU>vTi;6zt=4k-Y?$y=O}?+*vzD8V!P0Qxq~4y*gwW=fSj$aH6J&;@iTsSc+@i2%N@}JI zC1rCa&*RCKOs1?57eyg&O-QZd`0`d|V=C1elFdsy?-AOhM9Q zUeY)#C(w?M%Lf@fIPTu8rvhu-4AZo27hPK_N!u<*V2dK+F)TYJ!=}gyskOU^DUzAU z%ZOpgs9|9S(8Wnf-A^|%A&OpP+7c|}aN~jde3#_p3NqZCfG{IUqJ*)$o^U%dbR_Rg z#NA0dZ`cW2$dRLY$l{!rvMVVWF=bUIxr}o1vmQm$und%oVYyPBo|Q@gj>t%NN>-VI zy{$!98cjn5{*E{{t2kNT+iG*Us+KZ2t2pl3ePa!Ua+spHk+8c_#C4{psySJ7 zvng3`A_O61UFH#v3;;7E>$x?FR=q!84KP&iCq*C!1voAnGL9%YLN*|6DcNX_Pvt$W zn6=yF#u;NNC7aCgBl+>80W+QezscvvOph8f$MRldV@4{jB^#2wy6x5z& zMN#~EXg0|sH8+))jfN$1vKHmH5i{pl!uiRjO$s$o6IqtU8HhoqxLz!plrJUYTTtFD zh_N}DlC7qvFx`ZFGriCBLQFTCp2jq8dSRybo1V_}0n>{xebDrxOdm46DyB&j*$DD4 zbqAR?L|Q6O@@h#*bB(a2B78a!rq&2MD#EjYP?#j^lJ|@gLH#%gmE&9+Ujp$7__YP` zF*Oh$(>BD%v>ov=?Ld4?I}sn#t%#3l7vf{ujrf?RO)=~*q-~~fJ)pmdX6hjZtwQON^>rcO_67@ zZ%umcrL)5n{l4e@uol?KeSVw3?sHdfnNKY@a8wax_ZWlT4yBo69Y8)1z+L+|SS;>f zO71k%t-~of_(iHH>;lwxVT!1wU8KbrZwW~4fl_JA7{eBn$H9kP7bnu7qBO^l;oaD~ zT4XEKG;F8szM5Asm*6 zvu*KCTa8T{*w`QL?Bl{U?El87 zI~X@G!YJYd@kfEZy9@$HcuUr?iC{9qC^(!TcX$GeE}vHHLVjF^n`lToK7?Y$igC8e=>t&*Gj{nIBA$X-QN0v>3*L&fcp56GkgjA@z2&4nS0S zb_pi&^Offq<=Mi{pOSl5NAt5^o-T(UaPACwb&5x@f228!qp~%VpNQj36~lJg>rrA@ zxBJ#fC*u?Aq}Dp=YTvc??(C5<G$#t6PlJia53kH z%w^mWWZuOCniYi3+tG{rg$ zjr!enJtmmUAS8vI#w9etI1!SG`0c4$Ld>t0B&INRS zxMKC9-|~Ab1(16@8$ecn4t5?6Onx zzO1RC9A27mmf2QPMrUNGZDMKlKu-}~0NIBQWZsgoK#gg zp#dp}i6VMfgIcH|thXln8WIidiH5`tt?$$4)n9(rAHMp@>xIV_K1wFP zf3dq8z6D<$!ng1xW;-1kc_gY(6o&9ZeYpy27G|}%NH|ojDR9#ckfNT@;jVhPzD}jI z+11(6-nQj|&CN}X4Rzb=w^OZJyFqVBDv92m1N}QXLyF!UYi&{bniKtfz0Bw^5?dl6 zaEBD1w^JGD>upt9RH3|nUsWjbhubTWP&D*G`nKu}iTRpcw;$SDx0D!iZXZt19Z>rk zn`3p#Z?o#Ljp68q26gDO_v<%SMH)Z*eQ)nz*M23E*mC=m{oOlzd#`D~=;-*@zH7*M zlxC4wMXLgy=Na&^LR;uU8LP;_d>MymPld0o(FxNcW;5ukui;nIC45PmqGutW!ALF@ zKm}dHe=E3W=ovZ-TmV{tj*D+w&!R%o;Pp}u{yTiUk51tyXp&CS9K1PC=ls?T=nO1e zz`Y3R45ZIM(*^Gcw5Gs02YL|w7Vtms>y2Wx)4;pwF|^M@B= z!b|qhT;t~ut=i4N<_vVs!zxF&-H)Qjx1C0>U9i6$`~mP&H0}E}g{V&YQO%aU6Zp>N z`kX?=9!Dt= 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 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 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 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 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 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)