Reputation: 4835
I want to know which metrics are used to calculate the correct line height (vertical distance between the baselines of 2 adjacent lines of text). "Correct" shall arbitrarily defined as "whatever DrawTextW
does".
The accepted answer here appears to follow what the graph provided in this MSDN article says:
TEXTMETRICW.tmHeight + TEXTMETRICW.tmExternalLeading;
But that does not appear to be correct. Some testing with 2 pieces of text, each of which consists of 2 lines:
// RECT rc is more than large enough to fit any text
int HeightChinese = DrawTextW(hdc, L"中\r\n文", -1, &rc, 0);
int HeightLatin = DrawTextW(hdc, L"Latin,\r\nlatin!", -1, &rc, 0);
The expected return values should be 2 * <SomethingUnknown>
.
One observation is that the return value of DrawTextW
will always match the RECT
output if DT_CALCRECT
were used, for all fonts that I have on my machine. So I will assume that using DT_CALCRECT
does not provide any additional value over using the return value of DrawTextW
.
For all fonts on my machine, these are true:
HeightChinese == HeightLatin
LOGFONTW.lfHeight == TEXTMETRICW.tmHeight
(1).For most fonts on my machine, this is true:
HeightXxx == 2 * TEXTMETRICW.tmHeight
Which already contradicts the formula provided in the other question (TEXTMETRICW.tmExternalLeading
does not play a role).
For example, "Arial" with LOGFONTW.lfHeight = 36
will have TEXTMETRICW.tmExternalLeading = 1
, and HeightXxx == 72
(not 74). The distance between the lines when taking a screenshot and measuing the pixels is also 72 (so it appears that the return value can be trusted).
At the same time, "Segoe UI" with LOGFONTW.lHeight = 43
will have TEXTMETRICW.tmExternalLeading = 0
, and HeightXxx == 84
(not 86).
This is a list of all anomalous fonts on my system:
"FontName" -- "DrawText return value" vs "2 * TEXTMETRICW.tmHeight"
Ebrima -- 84 vs 86
Leelawadee UI -- 84 vs 86
Leelawadee UI Semilight -- 84 vs 86
Lucida Sans Unicode -- 96 vs 98
Malgun Gothic -- 84 vs 86
Malgun Gothic Semilight -- 84 vs 86
Microsoft Tai Le -- 80 vs 82
Microsoft YaHei -- 82 vs 84
Microsoft YaHei UI Light -- 82 vs 84
MS Gothic -- 66 vs 64
MS UI Gothic -- 66 vs 64
MS PGothic -- 66 vs 64
Nirmala UI -- 84 vs 86
Nirmala UI Semilight -- 84 vs 86
Palatino Linotype -- 84 vs 86
Segoe UI -- 84 vs 86
Segoe UI Black -- 84 vs 86
Segoe UI Historic -- 84 vs 86
Segoe UI Light -- 84 vs 86
Segoe UI Semibold -- 84 vs 86
Segoe UI Semilight -- 84 vs 86
Segoe UI Symbol -- 84 vs 86
SimSun -- 66 vs 64
NSimSun -- 66 vs 64
SimSun-ExtB -- 66 vs 64
Verdana -- 76 vs 78
Webdings -- 62 vs 64
Yu Gothic UI -- 84 vs 86
Yu Gothic UI Semibold -- 84 vs 86
Yu Gothic UI Light -- 84 vs 86
Yu Gothic UI Semilight -- 84 vs 86
MS Mincho -- 66 vs 64
MS PMincho -- 66 vs 64
Ubuntu Mono -- 62 vs 64
Sometimes the return value is 2 bigger, sometimes it is 2 smaller than the calculated value.
I have looked at the other values in TEXTMETRICW
, and I've also looked at the extra data available int OUTLINETEXTMETRICW
, but I could not find any pattern that would explain the observations.
So then, what are the correct metrics to calculate line height? I understand that I could call DrawTextW
with DT_CALCRECT
to get this value, but I want to understand where this information comes from (and thus, how a font designer could control it in a predictable way).
Here is a gist with a complete Windows application that demonstrates this. All the interesting stuff is in WM_PAINT
. Search for @EDIT
for some interesting code switches and breakpoints. At the time of posting this question, my GitHub account has been flagged, and the Gist is temporarily unavailable. I hope this can be resolved quickly.
(1) I am using EnumFontFamiliesEx
to enumerate all fonts, and it happens to provide LOGFONTW
structs with positive lfHeight
values. That means I am using cell height rather than character height. While character height is the more typical way of specifying font height, that is sort of irrelevant here, it just so happens that cell height is equal to TEXTMETRICW.tmHeight
, but character height isn't. The relevant value for calculations is TEXTMETRICW.tmHeight
, and not LOGFONTW.lfHeight
.
Upvotes: 4
Views: 563
Reputation: 4835
As Jonathan Potter pointed out, the formula TEXTMETRICW.tmHeight
should have been correct, and if the DT_EXTERNALLEADING
flag is set, then it's TEXTMETRICW.tmHeight + TEXTMETRICW.tmExternalLeading
.
I reverse-engineered DrawTextExW
with Ghidra and the reason the numbers were sometimes off is not DrawTextExW
itself. DrawTextExW
internally uses a DT_InitDrawTextInfo
, which in turn uses GetTextMetricsW
and calculates the line height according to the above formula.
However, consider this code to probe all fonts:
LOGFONTW Probe = {};
Probe.lfCharSet = DEFAULT_CHARSET;
EnumFontFamiliesExW(hdc, &Probe, InitializeFontInfo_EnumFontFamiliesCallback, NULL, 0);
static int CALLBACK InitializeFontInfo_EnumFontFamiliesCallback(const LOGFONTW *LogFont, const TEXTMETRICW *TextMetric, DWORD FontType, LPARAM lParam)
{
FONT_INFO tmp = {};
tmp.LogFont = *LogFont;
tmp.TextMetric = *TextMetric;
FontInfo.push_back(tmp);
return 1;
}
Here, for the Segoe UI font, for example, LogFont->lfHeight
will be 43.
And so, TextMetric->tmHeight
will also be 43, which, you would think, makes sense to some degree.
However:
If you go ahead and select this LogFont
into a HDC
, and then use GetTextMetricsW
, like so:
HFONT Font = CreateFontIndirectW(LogFont);
SelectObject(hdc, Font);
TEXTMETRICW TextMetric = {};
GetTextMetricsW(hdc, &TextMetric);
Then TextMetric->tmHeight == 42
even though LogFont->lfHeight == 43
.
In other words, the values provided to the EnumFontFamiliesExW
callback for its TEXTMETRICW
parameter cannot be trusted. Although you could argue that the bug is elsewhere, and selecting a LogFont->lfHeight == 43
font should really also produce a TextMetric->tmHeight == 43
text metric, but I suppose that's too much to ask. My guess is that there's a floating point conversion going on somewhere in there, and that occasionally produces a rounding error for some numbers.
Upvotes: 3
Reputation: 37142
DrawText()
only uses TEXTMETRIC.tmExternalLeading
if the DT_EXTERNALLEADING
flag is set when you call it - you don't seem to have taken that into account.
The line height formula is basically:
int iLineHeight = tm.tmHeight + ((format & DT_EXTERNALLEADING) ? tm.tmExternalLeading : 0);
Upvotes: 2