Account for rounding error when rounding height metrics

In Qt 5 we would ask FreeType for height metrics. FreeType
always returns rounded metrics, so for e.g. DejaVu Sans at
pixel size 21 it would return ascender=20 and descender=5,
which is actually incorrect, since the actual ascender is
19.49. The descender is rounded correctly. This is likely
due to the use of fixed point math internally.

In Qt, we would account for this error by setting the
leading to -1, so that the rounded height still becomes
24. (This is also technically incorrect, since the line gap
of the font is 0.)

In Qt 6, we got the same fixed point rounding error (so
ascender becomes 20 instead of 19), but we didn't account
for the error, so we would end up with lines that were 25
high instead of 24.

To reduce the chance of getting this error, we do the full
metrics calculation in floating point numbers instead. For
the case in question, we will then get arounded ascender of
19 and descender of 5, giving us a height of 24.

Fixed point numbers are still used for storing the results,
so while it's less likely, we could still end up with the
same error. Therefore, we also apply the same trick as in
Qt 5 when this occurs: Adapting the leading of the font to
account for the rounding error if it occurs.

[ChangeLog][QtGui][Text] Fixed an issue where the line
distance for hinted fonts would be off by one for specific
sizes of some fonts.

Fixes: QTBUG-134602
Pick-to: 6.8
Change-Id: I09f1806199b7b2b02a932bb65fe4da055bd60f51
Reviewed-by: Eirik Aavitsland <eirik.aavitsland@qt.io>
(cherry picked from commit 3728032e03b3bf95daa7115c734173a0070d85c8)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
Eskil Abrahamsen Blomfeldt 2025-03-24 15:09:22 +01:00 committed by Qt Cherry-pick Bot
parent 6dc3e49b36
commit a9418f0ab5

View File

@ -400,15 +400,14 @@ bool QFontEngine::processHheaTable() const
if (ascent == 0 && descent == 0) if (ascent == 0 && descent == 0)
return false; return false;
QFixed unitsPerEm = emSquareSize(); const qreal unitsPerEm = emSquareSize().toReal();
// Bail out if values are too large for QFixed // Bail out if values are too large for QFixed
const auto limitForQFixed = std::numeric_limits<int>::max() / (fontDef.pixelSize * 64); const auto limitForQFixed = std::numeric_limits<int>::max() / (fontDef.pixelSize * 64);
if (ascent > limitForQFixed || descent > limitForQFixed || leading > limitForQFixed) if (ascent > limitForQFixed || descent > limitForQFixed || leading > limitForQFixed)
return false; return false;
m_ascent = QFixed::fromReal(ascent * fontDef.pixelSize) / unitsPerEm; m_ascent = QFixed::fromReal(ascent * fontDef.pixelSize / unitsPerEm);
m_descent = -QFixed::fromReal(descent * fontDef.pixelSize) / unitsPerEm; m_descent = -QFixed::fromReal(descent * fontDef.pixelSize / unitsPerEm);
m_leading = QFixed::fromReal(leading * fontDef.pixelSize / unitsPerEm);
m_leading = QFixed::fromReal(leading * fontDef.pixelSize) / unitsPerEm;
return true; return true;
} }
@ -430,9 +429,10 @@ void QFontEngine::initializeHeightMetrics() const
processOS2Table(); processOS2Table();
if (!supportsSubPixelPositions()) { if (!supportsSubPixelPositions()) {
const QFixed actualHeight = m_ascent + m_descent + m_leading;
m_ascent = m_ascent.round(); m_ascent = m_ascent.round();
m_descent = m_descent.round(); m_descent = m_descent.round();
m_leading = m_leading.round(); m_leading = actualHeight.round() - m_ascent - m_descent;
} }
} }
@ -457,7 +457,7 @@ bool QFontEngine::processOS2Table() const
quint16 winDescent = qFromBigEndian<quint16>(ptr + 76); quint16 winDescent = qFromBigEndian<quint16>(ptr + 76);
enum { USE_TYPO_METRICS = 0x80 }; enum { USE_TYPO_METRICS = 0x80 };
QFixed unitsPerEm = emSquareSize(); const qreal unitsPerEm = emSquareSize().toReal();
if (preferTypoLineMetrics() || fsSelection & USE_TYPO_METRICS) { if (preferTypoLineMetrics() || fsSelection & USE_TYPO_METRICS) {
// Some fonts may have invalid OS/2 data. We detect this and bail out. // Some fonts may have invalid OS/2 data. We detect this and bail out.
if (typoAscent == 0 && typoDescent == 0) if (typoAscent == 0 && typoDescent == 0)
@ -467,9 +467,9 @@ bool QFontEngine::processOS2Table() const
if (typoAscent > limitForQFixed || typoDescent > limitForQFixed if (typoAscent > limitForQFixed || typoDescent > limitForQFixed
|| typoLineGap > limitForQFixed) || typoLineGap > limitForQFixed)
return false; return false;
m_ascent = QFixed::fromReal(typoAscent * fontDef.pixelSize) / unitsPerEm; m_ascent = QFixed::fromReal(typoAscent * fontDef.pixelSize / unitsPerEm);
m_descent = -QFixed::fromReal(typoDescent * fontDef.pixelSize) / unitsPerEm; m_descent = -QFixed::fromReal(typoDescent * fontDef.pixelSize / unitsPerEm);
m_leading = QFixed::fromReal(typoLineGap * fontDef.pixelSize) / unitsPerEm; m_leading = QFixed::fromReal(typoLineGap * fontDef.pixelSize / unitsPerEm);
} else { } else {
// Some fonts may have invalid OS/2 data. We detect this and bail out. // Some fonts may have invalid OS/2 data. We detect this and bail out.
if (winAscent == 0 && winDescent == 0) if (winAscent == 0 && winDescent == 0)
@ -477,8 +477,8 @@ bool QFontEngine::processOS2Table() const
const auto limitForQFixed = std::numeric_limits<int>::max() / (fontDef.pixelSize * 64); const auto limitForQFixed = std::numeric_limits<int>::max() / (fontDef.pixelSize * 64);
if (winAscent > limitForQFixed || winDescent > limitForQFixed) if (winAscent > limitForQFixed || winDescent > limitForQFixed)
return false; return false;
m_ascent = QFixed::fromReal(winAscent * fontDef.pixelSize) / unitsPerEm; m_ascent = QFixed::fromReal(winAscent * fontDef.pixelSize / unitsPerEm);
m_descent = QFixed::fromReal(winDescent * fontDef.pixelSize) / unitsPerEm; m_descent = QFixed::fromReal(winDescent * fontDef.pixelSize / unitsPerEm);
m_leading = QFixed{}; m_leading = QFixed{};
} }