Directwrite: Getting a font's height
Asked Answered
E

2

11

My objective: I want to get the height of an IDWriteTextFormat's font so I can calculate how many lines of text can fit in an IDWriteTextLayout of a certain height.

My problem: Right now I'm using this code to calculate the visible number of lines:

inline int kmTextCtrl::GetVisLines() const
{

    /* pTextFormat is an IDWriteTextFormat pointer, dpi_y is the desktop's vertical dpi,
       and GetHeight() returns the height (in pixels) of the render target. */
    float size = (pTextFormat->GetFontSize()/72.0f)*dpi_y;
    return (int)(GetHeight()/size);
}

The calculation seems to be accurate for some fonts, but not for any of the TrueType fonts (e.g.: Courier New, Arial, Times New Roman). For these fonts, the text shown is clipped well short of the lower vertical boundary of the render target.

Some context: I am making a text scroll back buffer control which uses an IDWriteTextLayout to put text to the control's render target. I use the result of GetVisLines() to determine how many lines of text from a circular buffer (which stores text in std::strings by the line) to pull into the layout, and recreate it every time the window is scrolled or resized.

This is being done using "native" Win32 API C++.

Evzone answered 8/4, 2011 at 2:52 Comment(0)
U
11

The simplest and most robust approach is to just ask the layout itself for text metrics, as that's one of the two things it was designed for, drawing and measurement. You would create an IDWriteTextLayout using the text format and call GetMetrics to get the DWRITE_TEXT_METRICS::height. I'm guessing you're using ID2D1RenderTarget::DrawText and passing a text format, so you may not have created a layout directly, but calling DrawText is just like calling CreateTextLayout yourself followed by DrawTextLayout.

Beware that going through the lower layers to get this answer (IDWriteFontFace and the like) makes certain assumptions that a generic world ready text control should not assume, such as assuming the base font will be used and that all the lines being the same height. So long as all characters are present in the given base font, this happens to work out (chances are you're mostly displaying English which is why all appears well), but throw in some CJK or RTL languages or emoji (which a base font like Times New Roman certainly doesn't support), and the line height will grow or shrink accordingly to the substituted fonts. GDI rescales substituted fonts such that they fit into the base font's height, but this leads to poorly scrunched letters in languages like Thai and Tibetan which need more breathing room for ascenders and descenders. IDWriteTextLayout and other layouts like those in WPF/Word keep all the font glyphs at the same em size, which means they line up more nicely when adjacent to each other; but it does mean the line height is variable.

If you do just draw each line of text as if they were all the same height, you can see overlap between glyphs and non-uniform baselines between lines, or clipping at the top and bottom of the control. So the ideal thing to do is to use the actual height of each line; but if you need them to all be the same height (or if it complicates the control too much), then at least set an explicit line spacing using SetLineSpacing with DWRITE_LINE_SPACING_UNIFORM to that of the base font - that way the baselines are uniformly spaced.

Though for the curious, IDWriteTextLayout computes the line height as the maximum of all run heights on that line, and the height of a single run (same font and em size) just uses the design metrics: ascent + descent, plus any lineGap that happens to be present (most fonts set this to zero, but Gabriola is a good example of large line gap). Note all em sizes are in DIP's (which at typical 96DPI means 1:1, DIP's exactly == pixels), not points (1/72 inch).

(ascent + descent + lineGap) * emSize / designUnitsPerEm

Ullage answered 10/9, 2011 at 7:40 Comment(6)
While CJK or RTL might be disregarded (should not), emojis are hard to argue against. Emojis will trigger font substitution for many fonts. :-) Or rather, πŸ˜€πŸ€¦πŸ˜΅. – Vickeyvicki
What does it means when GetMetrics returns +infinity for DWRITE_TEXT_METRICS.width? for a regular text, no wrapping, with float max value passed for w & h? – Ayotte
@SimonMourier: πŸ€” Could be bad font metrics. I vaguely recall seeing infinity returned in the width for huge glyph advances (DWRITE_TEXT_METRICS::width is essentially an std::accumulate() of the glyph advances on that row). I'd narrow it down by trying a different font and different text, and although this shouldn't be the cause, passing a smaller max value (FLT_MAX/2). – Ullage
The font is Segoe UI size 16. Actually, I see no reason why it would be infinite, but thanks, I'll try that :-) – Ayotte
After some tests, it turns out its a string like "hello world", with DWRITE_TEXT_ALIGNMENT_JUSTIFIED and an unbounded width. Because there's a space, I guess the engine puts hello at the far left, and world at the far right with an infinite space... So in fact, it does make some sense. Is there any setting to prevent that behavior (like "max space size"...)? – Ayotte
@SimonMourier: Ahh yes, justification. When calling CreateTextLayout, set the maxWidth parameter to the actual limits of your bounds (e.g. a button control). Otherwise when using DWRITE_WORD_WRAPPING_NO_WRAP, layout is permitted to justify to the extents passed (and infinity is very wide πŸ˜‰), but justification was really meant to be used with DWRITE_WORD_WRAPPING_WRAP anyway. – Ullage
E
7

I have found an answer. To find the spacing of a line (font height plus gap) in Directwrite, you must do something akin to the following:

inline int kmTextCtrl::GetVisLines() const
{

    IDWriteFontCollection* collection;
    TCHAR name[64]; UINT32 findex; BOOL exists;
    pTextFormat->GetFontFamilyName(name, 64);
    pTextFormat->GetFontCollection(&collection);
    collection->FindFamilyName(name, &findex, &exists); 
    IDWriteFontFamily *ffamily;
    collection->GetFontFamily(findex, &ffamily);
    IDWriteFont* font;
    ffamily->GetFirstMatchingFont(pTextFormat->GetFontWeight(), pTextFormat->GetFontStretch(), pTextFormat->GetFontStyle(), &font);
    DWRITE_FONT_METRICS metrics;
    font->GetMetrics(&metrics);
    float ratio = pTextFormat->GetFontSize() / (float)metrics.designUnitsPerEm;
    float size = (metrics.ascent + metrics.descent + metrics.lineGap) * ratio;
    float height = GetHeight();
    int retval = static_cast<int>(height/size);
    ffamily->Release();
    collection->Release();
    font->Release();
    return retval;
}

Of course, you probably don't want to do all that every time you have to call a frequently-used inline function.

Evzone answered 10/4, 2011 at 6:23 Comment(2)
This is definitely a wrong approach. See Dwayne's response for a proper solution. – Egidio
@DmitriNesteruk, because it makes too many assumptions. For example you start measuring lines without even considering that different font could be used later depending on actual text data. Next it ignores line spacing mode, and assumes the way layout calculates spacing as (ascent + descent + lineGap). See top rated answer above, it explains better why this is unreliable solution. – Egidio

© 2022 - 2024 β€” McMap. All rights reserved.