From 50cd0daf29d434b78cc70bbf732ee33b2bc18600 Mon Sep 17 00:00:00 2001 From: Eskil Abrahamsen Blomfeldt Date: Wed, 25 Nov 2015 13:47:27 +0100 Subject: [PATCH] Fix kerned advances in QRawFont on OS X and Windows On Windows, the wrong value was used to calculate the design-to-device scale. The assumption has been that tmHeight in the TEXTMETRIC is the pixel size of the em square, but it is not, it's the height of the font (ascent + descent). The pixel size of the font is defined to be the em square size in pixels. On OS X, the kerning data was never actually read from the font. I've added a lazy initialization for this similar to the one in the FT engine. This was discovered when investigating QTBUG-48546, as it turned out that the kerning information extracted by Qt in this case was different from the one used by Harfbuzz. I've changed testfont.ttf to kern "_2" so that the digit is positioned directly on top of the underscore and constructed a test. [ChangeLog][QRawFont] Fixed kerning on advances in QRawFont for OS X and Windows. Change-Id: Ic9a321ad119ea880cef89b861c75a820ab8d3182 Reviewed-by: Konstantin Ritt --- .../fontdatabases/mac/qfontengine_coretext.mm | 15 ++++++++ .../mac/qfontengine_coretext_p.h | 2 ++ .../platforms/windows/qwindowsfontengine.cpp | 2 +- tests/auto/gui/text/qrawfont/tst_qrawfont.cpp | 34 ++++++++++++++++++ tests/auto/shared/resources/testfont.ttf | Bin 63212 -> 78432 bytes 5 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/platformsupport/fontdatabases/mac/qfontengine_coretext.mm b/src/platformsupport/fontdatabases/mac/qfontengine_coretext.mm index 732aead62a4..be1696dfe8f 100644 --- a/src/platformsupport/fontdatabases/mac/qfontengine_coretext.mm +++ b/src/platformsupport/fontdatabases/mac/qfontengine_coretext.mm @@ -215,6 +215,8 @@ void QCoreTextFontEngine::init() Q_ASSERT((void *)(&ctfont + 1) == (void *)&cgFont); faceData.user_data = &ctfont; faceData.get_font_table = ct_getSfntTable; + + kerningPairsLoaded = false; } glyph_t QCoreTextFontEngine::glyphIndex(uint ucs4) const @@ -788,4 +790,17 @@ QFontEngine::Properties QCoreTextFontEngine::properties() const return result; } +void QCoreTextFontEngine::doKerning(QGlyphLayout *g, ShaperFlags flags) const +{ + if (!kerningPairsLoaded) { + kerningPairsLoaded = true; + qreal emSquare = CTFontGetUnitsPerEm(ctfont); + qreal scale = emSquare / CTFontGetSize(ctfont); + + const_cast(this)->loadKerningPairs(QFixed::fromReal(scale)); + } + + QFontEngine::doKerning(g, flags); +} + QT_END_NAMESPACE diff --git a/src/platformsupport/fontdatabases/mac/qfontengine_coretext_p.h b/src/platformsupport/fontdatabases/mac/qfontengine_coretext_p.h index 1c33ae7d84b..c22d1ddc0aa 100644 --- a/src/platformsupport/fontdatabases/mac/qfontengine_coretext_p.h +++ b/src/platformsupport/fontdatabases/mac/qfontengine_coretext_p.h @@ -97,6 +97,7 @@ public: glyph_metrics_t alphaMapBoundingBox(glyph_t glyph, QFixed, const QTransform &matrix, GlyphFormat) Q_DECL_OVERRIDE; QImage bitmapForGlyph(glyph_t, QFixed subPixelPosition, const QTransform &t) Q_DECL_OVERRIDE; QFixed emSquareSize() const Q_DECL_OVERRIDE; + void doKerning(QGlyphLayout *g, ShaperFlags flags) const Q_DECL_OVERRIDE; bool supportsTransformation(const QTransform &transform) const Q_DECL_OVERRIDE; @@ -134,6 +135,7 @@ private: CGAffineTransform transform; QFixed avgCharWidth; QFontEngine::FaceId face_id; + mutable bool kerningPairsLoaded; }; CGAffineTransform qt_transform_from_fontdef(const QFontDef &fontDef); diff --git a/src/plugins/platforms/windows/qwindowsfontengine.cpp b/src/plugins/platforms/windows/qwindowsfontengine.cpp index ca3e2527e9c..b6bc65055fa 100644 --- a/src/plugins/platforms/windows/qwindowsfontengine.cpp +++ b/src/plugins/platforms/windows/qwindowsfontengine.cpp @@ -197,7 +197,7 @@ void QWindowsFontEngine::getCMap() designToDevice = QFixed((int)otm->otmEMSquare)/QFixed::fromReal(fontDef.pixelSize); unitsPerEm = otm->otmEMSquare; x_height = (int)otm->otmsXHeight; - loadKerningPairs(QFixed((int)otm->otmEMSquare)/int(otm->otmTextMetrics.tmHeight)); + loadKerningPairs(designToDevice); _faceId.filename = QFile::encodeName(QString::fromWCharArray((wchar_t *)((char *)otm + (quintptr)otm->otmpFullName))); lineWidth = otm->otmsUnderscoreSize; fsType = otm->otmfsType; diff --git a/tests/auto/gui/text/qrawfont/tst_qrawfont.cpp b/tests/auto/gui/text/qrawfont/tst_qrawfont.cpp index c12acf65cc0..b1e292f0947 100644 --- a/tests/auto/gui/text/qrawfont/tst_qrawfont.cpp +++ b/tests/auto/gui/text/qrawfont/tst_qrawfont.cpp @@ -93,6 +93,8 @@ private slots: void multipleRawFontsFromData(); void rawFontFromInvalidData(); + + void kernedAdvances(); private: QString testFont; QString testFontBoldItalic; @@ -954,6 +956,38 @@ void tst_QRawFont::rawFontFromInvalidData() QVERIFY(!font.isValid()); } +#define FUZZY_LTEQ(X, Y) (X < Y || qFuzzyCompare(X, Y)) + +void tst_QRawFont::kernedAdvances() +{ + const int emSquareSize = 1000; + const qreal pixelSize = 16.0; + const int underScoreAW = 500; + const int underscoreTwoKerning = -500; + const qreal errorMargin = 1.0 / 16.0; // Fixed point error margin + + QRawFont font(testFont, pixelSize); + QVERIFY(font.isValid()); + + QVector glyphIndexes = font.glyphIndexesForString(QStringLiteral("__")); + QCOMPARE(glyphIndexes.size(), 2); + + QVector advances = font.advancesForGlyphIndexes(glyphIndexes, QRawFont::KernedAdvances); + QCOMPARE(advances.size(), 2); + + qreal expectedAdvanceWidth = pixelSize * underScoreAW / emSquareSize; + QVERIFY(FUZZY_LTEQ(qAbs(advances.at(0).x() - expectedAdvanceWidth), errorMargin)); + + glyphIndexes = font.glyphIndexesForString(QStringLiteral("_2")); + QCOMPARE(glyphIndexes.size(), 2); + + advances = font.advancesForGlyphIndexes(glyphIndexes, QRawFont::KernedAdvances); + QCOMPARE(advances.size(), 2); + + expectedAdvanceWidth = pixelSize * (underScoreAW + underscoreTwoKerning) / emSquareSize; + QVERIFY(FUZZY_LTEQ(qAbs(advances.at(0).x() - expectedAdvanceWidth), errorMargin)); +} + #endif // QT_NO_RAWFONT QTEST_MAIN(tst_QRawFont) diff --git a/tests/auto/shared/resources/testfont.ttf b/tests/auto/shared/resources/testfont.ttf index d6042d2e5858da67a520866915aa47c4dc766810..93b728c77655b83de06083b936b808ec96520d82 100644 GIT binary patch delta 22207 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 Mq5j$Zouv)`0=Jxw;{X5v