dialer
dialer

Reputation: 4835

Get GDI HFONT line height as interpreted by DrawText[Ex]

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:

For most fonts on my machine, this is true:

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

Answers (2)

dialer
dialer

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

Jonathan Potter
Jonathan Potter

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

Related Questions