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 <allan.jensen@qt.io>
This commit is contained in:
Eskil Abrahamsen Blomfeldt 2023-07-06 13:16:50 +02:00
parent 3aff1e1678
commit 5469d6a6cc
10 changed files with 293 additions and 20 deletions

View File

@ -12,6 +12,7 @@
#include <qscreen.h>
#include <qpa/qplatformscreen.h>
#include <QtCore/QUuid>
#include <QtCore/QLoggingCategory>
#include <QtGui/QPainterPath>
#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<const TT_OS2 *>(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<const TT_OS2 *>(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

View File

@ -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;

View File

@ -10,6 +10,7 @@
#include <QtCore/QLibraryInfo>
#include <QtCore/QDir>
#include <QtCore/QtEndian>
#include <QtCore/QLoggingCategory>
#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<FT_Face>(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);

View File

@ -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);
};

View File

@ -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;

View File

@ -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);
}

View File

@ -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<const char *>(fam));
families << family;
}
populateFromPattern(pattern, applicationFont);
populateFromPattern(pattern, applicationFont, face, this);
if (face)
FT_Done_Face(face);
FcFontSetAdd(set, pattern);

View File

@ -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"
)

View File

@ -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"

Binary file not shown.