From 5469d6a6cc741cc8fb2adec3cd08c39b8ef6c75a Mon Sep 17 00:00:00 2001 From: Eskil Abrahamsen Blomfeldt Date: Thu, 6 Jul 2023 13:16:50 +0200 Subject: [PATCH] Support loading variable fonts as application fonts in Freetype When loading the fonts, we go through all the named instances and register these as subfamilies. In addition to exposing these variants by style name, we also register them with the according weights, italic style and stretch. This adds a field to FontFile to allow piping the instance index through to when we instantiate the face. [ChangeLog][Fonts] Added support for selecting named instances in variable application fonts when using the Freetype backend. Task-number: QTBUG-108624 Change-Id: I57ef6b4802756dd408c3aae1f8a6c792a89bee6a Reviewed-by: Allan Sandfeld Jensen --- src/gui/text/freetype/qfontengine_ft.cpp | 71 ++++++++++- src/gui/text/freetype/qfontengine_ft_p.h | 2 + .../text/freetype/qfreetypefontdatabase.cpp | 116 ++++++++++++++++++ .../text/freetype/qfreetypefontdatabase_p.h | 8 ++ src/gui/text/qfontengine.cpp | 5 + src/gui/text/qfontengine_p.h | 12 +- src/gui/text/unix/qfontconfigdatabase.cpp | 48 ++++++-- .../gui/text/qfontdatabase/CMakeLists.txt | 4 + .../text/qfontdatabase/tst_qfontdatabase.cpp | 47 +++++++ .../shared/resources/testfont_variable.ttf | Bin 0 -> 30660 bytes 10 files changed, 293 insertions(+), 20 deletions(-) create mode 100644 tests/auto/shared/resources/testfont_variable.ttf diff --git a/src/gui/text/freetype/qfontengine_ft.cpp b/src/gui/text/freetype/qfontengine_ft.cpp index 3256c74f3b..b7eb9f412b 100644 --- a/src/gui/text/freetype/qfontengine_ft.cpp +++ b/src/gui/text/freetype/qfontengine_ft.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #ifndef QT_NO_FREETYPE @@ -34,6 +35,7 @@ #include FT_GLYPH_H #include FT_MODULE_H #include FT_LCD_FILTER_H +#include FT_MULTIPLE_MASTERS_H #if defined(FT_CONFIG_OPTIONS_H) #include FT_CONFIG_OPTIONS_H @@ -53,6 +55,8 @@ QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcFontMatch) + using namespace Qt::StringLiterals; #define FLOOR(x) ((x) & -64) @@ -243,6 +247,15 @@ QFreetypeFace *QFreetypeFace::getFace(const QFontEngine::FaceId &face_id, } else if (FT_New_Face(freetypeData->library, face_id.filename, face_id.index, &face)) { return nullptr; } + +#if (FREETYPE_MAJOR*10000 + FREETYPE_MINOR*100 + FREETYPE_PATCH) >= 20900 + if (face_id.instanceIndex >= 0) { + qCDebug(lcFontMatch) + << "Selecting named instance" << (face_id.instanceIndex) + << "in" << face_id.filename; + FT_Set_Named_Instance(face, face_id.instanceIndex + 1); + } +#endif newFreetype->face = face; newFreetype->ref.storeRelaxed(1); @@ -747,6 +760,43 @@ bool QFontEngineFT::init(FaceId faceId, bool antialias, GlyphFormat format, static void dont_delete(void*) {} +static FT_UShort calculateActualWeight(FT_Face face, QFontEngine::FaceId faceId) +{ + if (faceId.instanceIndex >= 0) { + FT_MM_Var *var = nullptr; + FT_Get_MM_Var(face, &var); + if (var != nullptr && FT_UInt(faceId.instanceIndex) < var->num_namedstyles) { + for (FT_UInt axis = 0; axis < var->num_axis; ++axis) { + if (var->axis[axis].tag == MAKE_TAG('w', 'g', 'h', 't')) { + return var->namedstyle[faceId.instanceIndex].coords[axis] >> 16; + } + } + } + } + if (const TT_OS2 *os2 = reinterpret_cast(FT_Get_Sfnt_Table(face, ft_sfnt_os2))) { + return os2->usWeightClass; + } + + return 700; +} + +static bool calculateActualItalic(FT_Face face, QFontEngine::FaceId faceId) +{ + if (faceId.instanceIndex >= 0) { + FT_MM_Var *var = nullptr; + FT_Get_MM_Var(face, &var); + if (var != nullptr && FT_UInt(faceId.instanceIndex) < var->num_namedstyles) { + for (FT_UInt axis = 0; axis < var->num_axis; ++axis) { + if (var->axis[axis].tag == MAKE_TAG('i', 't', 'a', 'l')) { + return (var->namedstyle[faceId.instanceIndex].coords[axis] >> 16) == 1; + } + } + } + } + + return (face->style_flags & FT_STYLE_FLAG_ITALIC); +} + bool QFontEngineFT::init(FaceId faceId, bool antialias, GlyphFormat format, QFreetypeFace *freetypeFace) { @@ -778,18 +828,18 @@ bool QFontEngineFT::init(FaceId faceId, bool antialias, GlyphFormat format, FT_Face face = lockFace(); if (FT_IS_SCALABLE(face)) { - bool fake_oblique = (fontDef.style != QFont::StyleNormal) && !(face->style_flags & FT_STYLE_FLAG_ITALIC) && !qEnvironmentVariableIsSet("QT_NO_SYNTHESIZED_ITALIC"); + bool isItalic = calculateActualItalic(face, faceId); + bool fake_oblique = (fontDef.style != QFont::StyleNormal) && !isItalic && !qEnvironmentVariableIsSet("QT_NO_SYNTHESIZED_ITALIC"); if (fake_oblique) obliquen = true; FT_Set_Transform(face, &matrix, nullptr); freetype->matrix = matrix; // fake bold if ((fontDef.weight >= QFont::Bold) && !(face->style_flags & FT_STYLE_FLAG_BOLD) && !FT_IS_FIXED_WIDTH(face) && !qEnvironmentVariableIsSet("QT_NO_SYNTHESIZED_BOLD")) { - if (const TT_OS2 *os2 = reinterpret_cast(FT_Get_Sfnt_Table(face, ft_sfnt_os2))) { - if (os2->usWeightClass < 700 && - (fontDef.pixelSize < 64 || qEnvironmentVariableIsSet("QT_NO_SYNTHESIZED_BOLD_LIMIT"))) { - embolden = true; - } + FT_UShort actualWeight = calculateActualWeight(face, faceId); + if (actualWeight < 700 && + (fontDef.pixelSize < 64 || qEnvironmentVariableIsSet("QT_NO_SYNTHESIZED_BOLD_LIMIT"))) { + embolden = true; } } // underline metrics @@ -2191,6 +2241,15 @@ Qt::HANDLE QFontEngineFT::handle() const return non_locked_face(); } +bool QFontEngineFT::supportsVariableApplicationFonts() const +{ +#if (FREETYPE_MAJOR*10000 + FREETYPE_MINOR*100 + FREETYPE_PATCH) >= 20900 + return true; +#else + return false; +#endif +} + QT_END_NAMESPACE #endif // QT_NO_FREETYPE diff --git a/src/gui/text/freetype/qfontengine_ft_p.h b/src/gui/text/freetype/qfontengine_ft_p.h index e195174960..85f1d87a3c 100644 --- a/src/gui/text/freetype/qfontengine_ft_p.h +++ b/src/gui/text/freetype/qfontengine_ft_p.h @@ -159,6 +159,8 @@ private: return supportsHorizontalSubPixelPositions(); } + bool supportsVariableApplicationFonts() const override; + bool getSfntTableData(uint tag, uchar *buffer, uint *length) const override; int synthesized() const override; diff --git a/src/gui/text/freetype/qfreetypefontdatabase.cpp b/src/gui/text/freetype/qfreetypefontdatabase.cpp index cf1ca42ab4..2441deaa38 100644 --- a/src/gui/text/freetype/qfreetypefontdatabase.cpp +++ b/src/gui/text/freetype/qfreetypefontdatabase.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #undef QT_NO_FREETYPE #include "qfontengine_ft_p.h" @@ -18,8 +19,14 @@ #include FT_TRUETYPE_TABLES_H #include FT_ERRORS_H +#include FT_MULTIPLE_MASTERS_H +#include FT_SFNT_NAMES_H +#include FT_TRUETYPE_IDS_H + QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcFontDb) + using namespace Qt::StringLiterals; void QFreeTypeFontDatabase::populateFontDatabase() @@ -54,6 +61,7 @@ QFontEngine *QFreeTypeFontDatabase::fontEngine(const QFontDef &fontDef, void *us QFontEngine::FaceId faceId; faceId.filename = QFile::encodeName(fontfile->fileName); faceId.index = fontfile->indexValue; + faceId.instanceIndex = fontfile->instanceIndex; return QFontEngineFT::create(fontDef, faceId, fontfile->data); } @@ -77,6 +85,110 @@ void QFreeTypeFontDatabase::releaseHandle(void *handle) extern FT_Library qt_getFreetype(); +void QFreeTypeFontDatabase::addNamedInstancesForFace(void *face_, + int faceIndex, + const QString &family, + const QString &styleName, + QFont::Weight weight, + QFont::Stretch stretch, + QFont::Style style, + bool fixedPitch, + const QSupportedWritingSystems &writingSystems, + const QByteArray &fileName, + const QByteArray &fontData) +{ + FT_Face face = reinterpret_cast(face_); + + // Note: The following does not actually depend on API from 2.9, but the + // FT_Set_Named_Instance() was added in 2.9, so to avoid populating the database with + // named instances that cannot be selected, we disable the feature on older Freetype + // versions. +#if (FREETYPE_MAJOR*10000 + FREETYPE_MINOR*100 + FREETYPE_PATCH) >= 20900 + FT_MM_Var *var = nullptr; + FT_Get_MM_Var(face, &var); + if (var != nullptr) { + for (FT_UInt i = 0; i < var->num_namedstyles; ++i) { + FT_UInt id = var->namedstyle[i].strid; + + QFont::Weight instanceWeight = weight; + QFont::Stretch instanceStretch = stretch; + QFont::Style instanceStyle = style; + for (FT_UInt axis = 0; axis < var->num_axis; ++axis) { + if (var->axis[axis].tag == MAKE_TAG('w', 'g', 'h', 't')) { + instanceWeight = QFont::Weight(var->namedstyle[i].coords[axis] >> 16); + } else if (var->axis[axis].tag == MAKE_TAG('w', 'd', 't', 'h')) { + instanceStretch = QFont::Stretch(var->namedstyle[i].coords[axis] >> 16); + } else if (var->axis[axis].tag == MAKE_TAG('i', 't', 'a', 'l')) { + FT_UInt ital = var->namedstyle[i].coords[axis] >> 16; + if (ital == 1) + instanceStyle = QFont::StyleItalic; + else + instanceStyle = QFont::StyleNormal; + } + } + + FT_UInt count = FT_Get_Sfnt_Name_Count(face); + for (FT_UInt j = 0; j < count; ++j) { + FT_SfntName name; + if (FT_Get_Sfnt_Name(face, j, &name)) + continue; + + if (name.name_id != id) + continue; + + // Only support Unicode for now + if (name.encoding_id != TT_MS_ID_UNICODE_CS) + continue; + + // Sfnt names stored as UTF-16BE + QString instanceName; + for (FT_UInt k = 0; k < name.string_len; k += 2) + instanceName += QChar((name.string[k] << 8) + name.string[k + 1]); + if (instanceName != styleName) { + FontFile *variantFontFile = new FontFile{ + QFile::decodeName(fileName), + faceIndex, + int(i), + fontData + }; + + qCDebug(lcFontDb) << "Registering named instance" << i + << ":" << instanceName + << "for font family" << family + << "with weight" << instanceWeight + << ", style" << instanceStyle + << ", stretch" << instanceStretch; + + registerFont(family, + instanceName, + QString(), + instanceWeight, + instanceStyle, + instanceStretch, + true, + true, + 0, + fixedPitch, + writingSystems, + variantFontFile); + } + } + } + } +#else + Q_UNUSED(face); + Q_UNUSED(family); + Q_UNUSED(styleName); + Q_UNUSED(weight); + Q_UNUSED(stretch); + Q_UNUSED(style); + Q_UNUSED(fixedPitch); + Q_UNUSED(writingSystems); + Q_UNUSED(fontData); +#endif + +} + QStringList QFreeTypeFontDatabase::addTTFile(const QByteArray &fontData, const QByteArray &file, QFontDatabasePrivate::ApplicationFont *applicationFont) { FT_Library library = qt_getFreetype(); @@ -194,6 +306,7 @@ QStringList QFreeTypeFontDatabase::addTTFile(const QByteArray &fontData, const Q FontFile *fontFile = new FontFile{ QFile::decodeName(file), index, + -1, fontData }; @@ -211,6 +324,9 @@ QStringList QFreeTypeFontDatabase::addTTFile(const QByteArray &fontData, const Q } registerFont(family, styleName, QString(), weight, style, stretch, true, true, 0, fixedPitch, writingSystems, fontFile); + + addNamedInstancesForFace(face, index, family, styleName, weight, stretch, style, fixedPitch, writingSystems, file, fontData); + families.append(family); FT_Done_Face(face); diff --git a/src/gui/text/freetype/qfreetypefontdatabase_p.h b/src/gui/text/freetype/qfreetypefontdatabase_p.h index ffb10a2e5c..46ffa4353c 100644 --- a/src/gui/text/freetype/qfreetypefontdatabase_p.h +++ b/src/gui/text/freetype/qfreetypefontdatabase_p.h @@ -26,6 +26,7 @@ struct FontFile { QString fileName; int indexValue; + int instanceIndex = -1; // Note: The data may be implicitly shared throughout the // font database and platform font database, so be careful @@ -42,6 +43,13 @@ public: QStringList addApplicationFont(const QByteArray &fontData, const QString &fileName, QFontDatabasePrivate::ApplicationFont *applicationFont = nullptr) override; void releaseHandle(void *handle) override; + static void addNamedInstancesForFace(void *face, int faceIndex, + const QString &family, const QString &styleName, + QFont::Weight weight, QFont::Stretch stretch, + QFont::Style style, bool fixedPitch, + const QSupportedWritingSystems &writingSystems, + const QByteArray &fileName, const QByteArray &fontData); + static QStringList addTTFile(const QByteArray &fontData, const QByteArray &file, QFontDatabasePrivate::ApplicationFont *applicationFont = nullptr); }; diff --git a/src/gui/text/qfontengine.cpp b/src/gui/text/qfontengine.cpp index 8f2336f2ac..66b0e88598 100644 --- a/src/gui/text/qfontengine.cpp +++ b/src/gui/text/qfontengine.cpp @@ -247,6 +247,11 @@ bool QFontEngine::supportsTransformation(const QTransform &transform) const return transform.type() < QTransform::TxProject; } +bool QFontEngine::supportsVariableApplicationFonts() const +{ + return false; +} + bool QFontEngine::expectsGammaCorrectedBlending() const { return true; diff --git a/src/gui/text/qfontengine_p.h b/src/gui/text/qfontengine_p.h index dbad0d95f9..50e427e46e 100644 --- a/src/gui/text/qfontengine_p.h +++ b/src/gui/text/qfontengine_p.h @@ -127,10 +127,11 @@ public: virtual bool getSfntTableData(uint tag, uchar *buffer, uint *length) const; struct FaceId { - FaceId() : index(0), encoding(0) {} + FaceId() : index(0), instanceIndex(-1), encoding(0) {} QByteArray filename; QByteArray uuid; int index; + int instanceIndex; int encoding; }; virtual FaceId faceId() const { return FaceId(); } @@ -212,6 +213,7 @@ public: inline bool canRender(uint ucs4) const { return glyphIndex(ucs4) != 0; } virtual bool canRender(const QChar *str, int len) const; + virtual bool supportsVariableApplicationFonts() const; virtual bool supportsTransformation(const QTransform &transform) const; @@ -370,13 +372,17 @@ Q_DECLARE_OPERATORS_FOR_FLAGS(QFontEngine::ShaperFlags) inline bool operator ==(const QFontEngine::FaceId &f1, const QFontEngine::FaceId &f2) { - return f1.index == f2.index && f1.encoding == f2.encoding && f1.filename == f2.filename && f1.uuid == f2.uuid; + return f1.index == f2.index + && f1.encoding == f2.encoding + && f1.filename == f2.filename + && f1.uuid == f2.uuid + && f1.instanceIndex == f2.instanceIndex; } inline size_t qHash(const QFontEngine::FaceId &f, size_t seed = 0) noexcept(noexcept(qHash(f.filename))) { - return qHashMulti(seed, f.filename, f.uuid, f.index, f.encoding); + return qHashMulti(seed, f.filename, f.uuid, f.index, f.instanceIndex, f.encoding); } diff --git a/src/gui/text/unix/qfontconfigdatabase.cpp b/src/gui/text/unix/qfontconfigdatabase.cpp index 474644b871..4a402cd764 100644 --- a/src/gui/text/unix/qfontconfigdatabase.cpp +++ b/src/gui/text/unix/qfontconfigdatabase.cpp @@ -29,6 +29,8 @@ QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcFontDb) + static inline int mapToQtWeightForRange(int fcweight, int fcLower, int fcUpper, int qtLower, int qtUpper) { return qtLower + ((fcweight - fcLower) * (qtUpper - qtLower)) / (fcUpper - fcLower); @@ -366,7 +368,10 @@ static inline bool requiresOpenType(int writingSystem) || writingSystem == QFontDatabase::Khmer || writingSystem == QFontDatabase::Nko); } -static void populateFromPattern(FcPattern *pattern, QFontDatabasePrivate::ApplicationFont *applicationFont = nullptr) +static void populateFromPattern(FcPattern *pattern, + QFontDatabasePrivate::ApplicationFont *applicationFont = nullptr, + FT_Face face = nullptr, + QFontconfigDatabase *db = nullptr) { QString familyName; QString familyNameLang; @@ -489,6 +494,20 @@ static void populateFromPattern(FcPattern *pattern, QFontDatabasePrivate::Applic } QPlatformFontDatabase::registerFont(familyName,styleName,QLatin1StringView((const char *)foundry_value),weight,style,stretch,antialias,scalable,pixel_size,fixedPitch,writingSystems,fontFile); + if (applicationFont != nullptr && face != nullptr && db != nullptr) { + db->addNamedInstancesForFace(face, + indexValue, + familyName, + styleName, + weight, + stretch, + style, + fixedPitch, + writingSystems, + QByteArray((const char*)file_value), + applicationFont->data); + } + // qDebug() << familyName << (const char *)foundry_value << weight << style << &writingSystems << scalable << true << pixel_size; for (int k = 1; FcPatternGetString(pattern, FC_FAMILY, k, &value) == FcResultMatch; ++k) { @@ -702,6 +721,7 @@ QFontEngine *QFontconfigDatabase::fontEngine(const QFontDef &f, void *usrPtr) QFontEngine::FaceId fid; fid.filename = QFile::encodeName(fontfile->fileName); fid.index = fontfile->indexValue; + fid.instanceIndex = fontfile->instanceIndex; // FIXME: Unify with logic in QFontEngineFT::create() QFontEngineFT *engine = new QFontEngineFT(f); @@ -803,26 +823,28 @@ QStringList QFontconfigDatabase::fallbacksForFamily(const QString &family, QFont return fallbackFamilies; } -static FcPattern *queryFont(const FcChar8 *file, const QByteArray &data, int id, FcBlanks *blanks, int *count) +static FcPattern *queryFont(const FcChar8 *file, const QByteArray &data, int id, FcBlanks *blanks, int *count, FT_Face *face) { #if FC_VERSION < 20402 Q_UNUSED(data); + *face = nullptr; return FcFreeTypeQuery(file, id, blanks, count); #else - if (data.isEmpty()) + if (data.isEmpty()) { + *face = nullptr; return FcFreeTypeQuery(file, id, blanks, count); + } FT_Library lib = qt_getFreetype(); FcPattern *pattern = nullptr; - FT_Face face; - if (!FT_New_Memory_Face(lib, (const FT_Byte *)data.constData(), data.size(), id, &face)) { - *count = face->num_faces; + if (!FT_New_Memory_Face(lib, (const FT_Byte *)data.constData(), data.size(), id, face)) { + *count = (*face)->num_faces; - pattern = FcFreeTypeQueryFace(face, file, id, blanks); - - FT_Done_Face(face); + pattern = FcFreeTypeQueryFace(*face, file, id, blanks); + } else { + *face = nullptr; } return pattern; @@ -850,8 +872,9 @@ QStringList QFontconfigDatabase::addApplicationFont(const QByteArray &fontData, FcPattern *pattern; do { + FT_Face face; pattern = queryFont((const FcChar8 *)QFile::encodeName(fileName).constData(), - fontData, id, blanks, &count); + fontData, id, blanks, &count, &face); if (!pattern) return families; @@ -860,7 +883,10 @@ QStringList QFontconfigDatabase::addApplicationFont(const QByteArray &fontData, QString family = QString::fromUtf8(reinterpret_cast(fam)); families << family; } - populateFromPattern(pattern, applicationFont); + populateFromPattern(pattern, applicationFont, face, this); + + if (face) + FT_Done_Face(face); FcFontSetAdd(set, pattern); diff --git a/tests/auto/gui/text/qfontdatabase/CMakeLists.txt b/tests/auto/gui/text/qfontdatabase/CMakeLists.txt index 245fd6cf77..18b96ded5d 100644 --- a/tests/auto/gui/text/qfontdatabase/CMakeLists.txt +++ b/tests/auto/gui/text/qfontdatabase/CMakeLists.txt @@ -37,11 +37,15 @@ set_source_files_properties("../../../shared/resources/testfont_italic.ttf" set_source_files_properties("../../../shared/resources/testfont_open.otf" PROPERTIES QT_RESOURCE_ALIAS "testfont_open.otf" ) +set_source_files_properties("../../../shared/resources/testfont_variable.ttf" + PROPERTIES QT_RESOURCE_ALIAS "testfont_variable.ttf" +) set(testdata_resource_files "../../../shared/resources/testfont.ttf" "../../../shared/resources/testfont_condensed.ttf" "../../../shared/resources/testfont_italic.ttf" "../../../shared/resources/testfont_open.otf" + "../../../shared/resources/testfont_variable.ttf" "LED_REAL.TTF" ) diff --git a/tests/auto/gui/text/qfontdatabase/tst_qfontdatabase.cpp b/tests/auto/gui/text/qfontdatabase/tst_qfontdatabase.cpp index 172cb2480b..81c402cd89 100644 --- a/tests/auto/gui/text/qfontdatabase/tst_qfontdatabase.cpp +++ b/tests/auto/gui/text/qfontdatabase/tst_qfontdatabase.cpp @@ -61,6 +61,8 @@ private slots: void stretchRespected(); + void variableFont(); + #ifdef Q_OS_WIN void findCourier(); #endif @@ -70,6 +72,7 @@ private: QString m_testFont; QString m_testFontCondensed; QString m_testFontItalic; + QString m_testFontVariable; }; tst_QFontDatabase::tst_QFontDatabase() @@ -82,10 +85,12 @@ void tst_QFontDatabase::initTestCase() m_testFont = QFINDTESTDATA("testfont.ttf"); m_testFontCondensed = QFINDTESTDATA("testfont_condensed.ttf"); m_testFontItalic = QFINDTESTDATA("testfont_italic.ttf"); + m_testFontVariable = QFINDTESTDATA("testfont_variable.ttf"); QVERIFY(!m_ledFont.isEmpty()); QVERIFY(!m_testFont.isEmpty()); QVERIFY(!m_testFontCondensed.isEmpty()); QVERIFY(!m_testFontItalic.isEmpty()); + QVERIFY(!m_testFontVariable.isEmpty()); } void tst_QFontDatabase::styles_data() @@ -505,5 +510,47 @@ void tst_QFontDatabase::findCourier() } #endif +void tst_QFontDatabase::variableFont() +{ + { + QFont f; + f.setStyleStrategy(QFont::NoFontMerging); + QFontPrivate *font_d = QFontPrivate::get(f); + if (!font_d->engineForScript(QChar::Script_Common)->supportsVariableApplicationFonts()) + QSKIP("Variable application fonts only supported on Freetype currently"); + } + + int id = QFontDatabase::addApplicationFont(m_testFontVariable); + if (id == -1) + QSKIP("Skip the test since app fonts are not supported on this system"); + + QString family = QFontDatabase::applicationFontFamilies(id).first(); + { + QFont font(family); + QCOMPARE(QFontInfo(font).styleName(), u"Regular"_s); + QCOMPARE(QFontInfo(font).weight(), QFont::Normal); + } + + { + QFont font(family); + font.setWeight(QFont::ExtraBold); + QCOMPARE(QFontInfo(font).styleName(), u"QtExtraBold"_s); + QCOMPARE(QFontInfo(font).weight(), QFont::ExtraBold); + } + + { + QFont regularFont(family); + QFont extraBoldFont(family); + extraBoldFont.setStyleName(u"QtExtraBold"_s); + + QFontMetricsF regularFm(regularFont); + QFontMetricsF extraBoldFm(extraBoldFont); + + QVERIFY(regularFm.horizontalAdvance(QLatin1Char('1')) < extraBoldFm.horizontalAdvance(QLatin1Char('1'))); + } + + QFontDatabase::removeApplicationFont(id); +} + QTEST_MAIN(tst_QFontDatabase) #include "tst_qfontdatabase.moc" diff --git a/tests/auto/shared/resources/testfont_variable.ttf b/tests/auto/shared/resources/testfont_variable.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f183b9333d2fd6d23d2ea2040217a18ede618f74 GIT binary patch literal 30660 zcmd75d0>>q{Xaf4&#^~#H`&}FA=%C5&L$+A`ygyK2MGy~gpdOivXB!4az-vO$RS0v z)>;p=3V754>IM|gwv^J=wrbU?A8IWgwbhEXmY?+i^5pxP=h=iLVy*U{--c&r=9zis zc)#ECzUKWbgc3r6@g)*lO-p&xUB?|82%%0~f0#ROnycc)_=gA)(1>)*Yc6lTy0UK{ z${mD|!llc-D{uSt_vWK~JK9$+9aw9_eSG>%__B1b>{-4f^NWor$DnBH9`LTdX5NY? zG1wcZFYNKIS&1|V^&(PzPydGQ1GA%^AVdl!q^P$C?SGiL%uYzzWFx4%CS88^ zK0+?%&*k^xhG~v26Xgd{PUu~}cKzc&39dl-In3#B|CLLdk-OO-OANnE4LggIY!7;DQJHe z;jkhEb0hDnIsV1(nbhxae52&XA8FU}ynbXSBSfK%?1%J%kSNR|57SMI(g-@2#?te2 zKsYNfwuC*yK4Wec!`>CbX)Qa-a)oVzK}cp<>{=Gb?q&aEr-aW~A9K;E^m%%g{*vkG zuW2Mb!BQwCBUBQsERB9a7tqBlnEs2Mq(^8!ZD7Sh6|E$@*&OzT&?4>^!`N!}8f7A( z&yu~glpGRQvVX9t>{GUo&Sp_WCT9dul4PCUUC6^$pe`s#hY!BYW~A2OkkGL3 zh{&iZ(a|weW8-XgTf8G7F)2AAB{d)|AV@H!J2Nt;Wo5f^a!q;p1%-t|QL$7~I=!%L zMi6>&m-Fs|OrDBLokx(+yQ#_~3PIJCGizpAL)1@gSOh-N_=LyV_(x|;#3#i*D_#B6 zWlX~-2cL5EkB_df1fR0PvU-36V8=Htm~A$j=ggU-(+P7M8XALwl66hb$oCl{8ZYD#DpUA7L4P*%vmgCDjDCoq^Y;^Ci?qc6?n$;H^mFf| zeyr#x%HL1tc{(yO@-`tOKg0FYksF48HvE&}XNI31-v8FIw~oGbY z-dj0uO?&fKZ$5qcjnjWT{nF_dPoE&<^si1ob^7kp+obo^RaRm~9$eq-15AzkP+}qL z$Y0d{f&fPf*fC;M;SM-+SPM4Z9$;l!naC75ewPa1g`LRX1sn=QM-oUOsdLvP=$RxJ zP$sc@BFRja*dTfkL?ZOy0a+5|KA@4ZR>BxvtV521v)JPzBg_FhRHHA!s+S|ubF6l2 zqTOnb$S=sxafOG4$~p@T3yp9j<>dzFS;_}k;fVsbz!8~7*HBu2nZ;%7-t#`Mh=VLkDRB!k$j-o7~*nI!zu^` z|DAB5 zMEw-yj*EGWyAI6Z7b*?Jkv8{ykurhp2%rYNVP~Mk1dALbP$JMCvkp64Pg;3O^ovbK z25lc<8J2(QXPB$MQm8DynUCjMl*D=q zHPSjPlNxpas6aU}k{!gzj6lAQ?I4m~)Jx(ZC|}q~bb3+O3f?K|+W`a7D^pP~f+~s% zF-&H#)f!@rv0B4Sa$Guv$OzAI7373Cl9H2>t+AkaSr_KWLkT4Bu;#?l94iWWxdr(J zc_G#^nwOiLBv@sgjNK8=$}4M)hNvJ{Pj+eltQr5h_n-IIJ~KzXlkIxG=C%2kUER08 z_UebK%09lTp`{>K4(irN%Nfewc4ovHhTpyX(T9Keg7u&O5FBy_t$VzMZK`Nlc=rQ+ zZI;dp*9_lny?f8viV_;g^ioCb@+AfNoc1JAHu4wQBpoGzBn=BHBW*0U&KJ7~Z#I-dzoxkjG~pps77dFf|$I`vtd z7tq!5CoSJ0zA5V3{U5g@Yg&3bCG#4pD+=?cm1UKs zXQpRnI1}R|Lk&6-NCVAscsg})y0%b9d?v`Zj0&+-rTQ{jkPmXsq{&I?)IvE)htM3u zC7YMfOSGmxU-D#c*OOhHPj>e{`91S|cTtf}p3xg-7ENt4ii-lK_l5;777jOGXc3Iw zr+CY*C;Tl-iUKAzl#VWWtaItnuCAj?J0Dx3Ty@#K=_yp`Y{@UO*)P98ElF6?Qd|;u zey{Y{xjHto^s%L#M?3Kk8D#)%lfTrI^Xc11K9)B~4}(wFkgMGrqbSjpyO`M!L``M^ zG83FZ(hq{DiC#wr!B+IdxP_PvCWF~D2*!ciqP~w9!F$X`LmvUZrR~IIL26<>;Q3tU zP@&Oco>^VtDJv~0E-J`KPf1QnOmNs@LxasVmYQU1pd6W=NaF$D@CYyzSH2XVl+5u# zAQB2VQXm+{F(L_8+9B?(M6bWiUhpW_Dy2bj~jKa|Tp&QF3kW{kh1^4qEaoldCI&Gq`O;!;ymV9hyP(|2o{h@VL=h$&_>t=_KQOehl`7fiVrs&`UPvJVf5Tj&Yt|aax7dq zcKqbopU^LO7sbkr^xRmVn#dpnQT}NGu-3dFF#MeG;1DC-J+4Fc3+2YMCy&!v;dIu| z(VZeIXYi*(pRdCzN|e)~hc&t9wp+kqb+eNgWiyMN0%)Ke`7;MVm_z%Qrr3%Kx79~r%)bw$sL_QYw)iP;Hu(K9V4IKUWeonn$jV~lK$ z%87|iO>s`I3@|f&+|*baXEhm(iAj1xKuD|vlgc0ln5(BdQIabdMQb0kBEigFLppT6RrxDiK6~)D# zy1*zF78YwXS_7@|Q}Y6(0A`qK4zh+gf=z;4n61%TD-9Fw5qA(XNp&YdmF2RYNINw} zt5s99M4*?y1 zayq$6d5Nxp%3&f?+!3Y#gN}EmDVLlGLp6d?6=Dah#@yt*kDTo{R@Pd!QnsyS`I+?e z?_ZUcrpk4OTjQ2;{@iJwk$bL_6EG}LidN-FJ4 zO`&(h%$quO9)E5t^xaL=Ktf2QI}E*0Snf!?sG^N2@qAm%pdgM(n)vr7sjxr#vf@gK zdE^DQQPm&$UPFgarO1F)U1o%X=3L&urZ%{w=Q!`2J@TILV^z;cC+Y4KKt@w4REvV^ z+k8rlHd5k<5A%x~NfkHY!C|2c^q0h7J>(1F!Q;hE5>qKzlQ;By{|jqZJ->40^Q+dp zu-~$HX1QCItJg#ZN*3?T@)@#Jxi&ILI(l5W>%<9V-^r7-NkKbCBT&p#~VU1Vm zRyDI6d1a6g;bB444!u-oFCgsY!<`5F!jo@b{`hCZH_!*Jotxb=oK4wc(R9m+ZP~8= zvp(hA@amQCyV$*aKJ!LC5U#;|Qb@JCB3g!Vs4gB%NgBlNp%ScgSnrTzeX<4#Bt-OL zx+0L7lo)Rd548qa%p`@T=z!2f7vxYTjJKMK6 zZdq|<<&|mQZ@TljjoUAOPI>FG+kcjQIQ;MT_5N;JNXBhDSKMP>;JGI>`oa5K?=H3c z>+XA%b91sg=fS*XBptB)8}P1ojfDPK04Gf=mBKha?ZEJKFbZh*T>aOvHl>Ya*VHT3 z^^L=SV7J7F>?qCdp~u+6!%dxs*125kA9RR`7h<-*uhG|R!1!M21rke2+=bdclE8!| zc@XtXAkYn9rb~jZkI14Zw-Z_B0G7qZg!s?|tJP}P%dzRvejE!fQH5f%21Y0ds>%J= zo9Lb%gKLH|ulF5Vsl5Hz?H!k89^Q7_jyE1j5t>M}Z!%G0iZjJUM}=4|W&kaK2Ixo8&s3cxn1;zj z0Ph)9C|MqS8JS(1pIy7~rk~M`L&~eo9S8ZAex&LD`-~n4_~j3NN2TBW;11>Y(t}wh zWk`AD^3{J>yY{8EwCZK*W-wW!V1u}?Jfes7|I)0vB&GoT=Bc8gLCnogeSQ+t^pHIv6KytXu;H2QjlXP@`gf*p1X8) zMIi%Mbi2(eQw+FTN=xY(WtcswsgaQ_i#iT1TyVH!(flZ(ENW?8d1y%E&53rQr9L|+ zW@^JtOT%`pSr}~XeKN}_uJo3bCnT=?&0I@ZA$acZR;~ISIB#K?#b9#Y-P*dZrDb1h z>)lQ>hs{O)&*{kHo)HAj2Dpe|7TA#h45R@S;9rs?)%yawl1fZ!{3be>r0;@`l=Gk?ntiS<%LF&I(@#gr z(=RK>hp*@JRpqAeQ_xW|iFe204)!4?I9YKU7apI~k_GR^I@#Z8SC0X{C21aRU($b;>ZCmI^AP4rTe3K_4GOrGI#zltI(oHUc+ z$0zG7t>ov>ul@+nt$b7#s4LyT!e|~9?iv0rTXo?Hp+xWG88011G1fWL80 z0eEtLD~OFL3CUCJk{q2LqAAy3c5Ge>zS{>^Z{Jsb*@7O&G}qr3&1+t?a1z!fSMRRX zRX46#^@GY8TXxqjr?)O^TGV*{u}Ki;blIsa(k;NejiiBextCZBdO;T5Gnk%1tcijw zh>#jaNDCvRg@pM_AX+&9EJudh20iHcvh)Un>Vh$N3A}xcrhvNIit@73yqpYYN>aQ% zZfbB)K!d3v(Zo3u_uYo)zZ`a^e-l_T2Hlr?b>avEL-lo0#FRy4}a~9OV zdaSAKTmIukPu>^$$}g3_KCgVIsRC5J_`5WiW+-1M&wZ>^fATqf`V3u0Kk_}oRv7ne zz2af%0DWV|@SQY1F|pt`J6i9VX$i{9s#-Y!%^){k( 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 z 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 o>nZ&p+H$WU$9+f&}Qd#-`0J1#qfB*mh literal 0 HcmV?d00001