PETZOLD BOOK BLOG

Charles Petzold on writing books, reading books, and exercising the internal UTM


Recent Entries
< PreviousBrowse the ArchivesNext >
Subscribe to the RSS Feed

Character Formatting Extensions with DirectWrite

January 28, 2014
New York, N.Y.

Yes, it's true. I suffered from glyphophobia — the irrational fear of glyphs and glyph runs. My glyphophobia began over 10 years ago when I started exploring the Windows Presentation Foundatation. I managed to suppress this fear sufficiently when called upon to do some work involving parsing and rendering XPS documents, but still the fear persisted, causing distinct shivers of anxiety whenever I encountered something involving glyphs.

I am happy to report that my glyphophobia is now nearly gone. One essential part of my cure involved writing a little DirectX program that created some visuals I had never seen before: a display of all the glyphs in some selected font files. Here's what I saw (click for the full 1920 × 1080 view):

If you glance at this thing real quick, you might think it's an ASCII table, but it's obviously not. Check out all those ligatures and swashed capitals. This is a display of all the glyphs in the selected font file. These glyphs are directly addressable by index if you use DirectWrite to render glyph runs, but only available in a roundabout manner if you display text normally.

That program and others are part of the November 2013 installment of my DirectX Factor column for MSDN Magazine, appropriately entitled "Who's Afraid of Glyph Runs?".

In the December 2013 column, "Character Outline Geometries Gone Wild", I used the GetGlyphRunOutline method of IDWriteFontFace to obtain geometry objects of the glyph outlines and perform manipulations on them, like so:

Of course, the actual program animates the rippling effect.

Due to circumstances beyond my control connected with the difficulties of publishing a magazine, the DirectX Factor column does not appear in the January 2014 issue of MSDN Magazine. It resumes in the February issue with a topic I'm very excited about and which I'll be discussing in a blog entry in early February.

If I had continued the DirectWrite arc in the DirectX Factor column, I would surely have begun exploring IDWriteTextRenderer, because that is the interface that allows the programmer the greatest insights into the role of glyphs in rendering text, as well as providing means to extend the character formatting normally available.

I will not be discussing that topic in the DirectX Factor column in MSDN Magazine, but I will tackle it right here. By the end of this blog entry, I'll be showing you how to render a paragraph that looks like this:

If you're familiar with DirectWrite, you know that background brushes, highlighting, color specifications for underline and strikethrough, double underline, triple underline, double strikethrough, triple strikethrough, and squiggly underline are not among the character formatting options supported by DirectWrite. Yet, this paragraph was rendered using a normal IDWriteTextLayout object, and DirectWrite continued to be responsible for determining the paragraph line breaks, which is generally not a pleasant job for the programmer to undertake.

Let's begin with a review of normal character formatting, which I covered in more detail in the October 2013 DirectX Factor column, "Text Formatting and Scrolling with DirectWrite".

Standard DirectWrite Character Formatting

If you've been doing any DirectWrite programming, you know that you can render a paragraph of text by first creating an IDWriteTextFormat object using the CreateTextFormat method of IDWriteFactory. In creating this object you specify the font family, size, style (italics), and weight (bold). You can add paragraph formatting (such as alignment and line spacing) on the IDWriteTextFormat object by calling methods defined by that interface. You then render some text on the screen by passing this IDWriteTextFormat object to the DrawText method, which is defined in the MSDN documentation like so:

void ID2D1RenderTarget::DrawText(

            [in] WCHAR *string,
            UINT stringLength,
            [in] IDWriteTextFormat *textFormat,
            const D2D1_RECT_F &layoutRect,
            [in] ID2D1Brush *defaultForegroundBrush,
            D2D1_DRAW_TEXT_OPTIONS options = D2D1_DRAW_TEXT_OPTIONS_NONE,
            DWRITE_MEASURING_MODE measuringMode = DWRITE_MEASURING_MODE_NATURAL)

The text string, the rectangle in which to render the text, and the foreground brush are all specified in this call. The text is formatted into multiple lines so that it fits within the width of the rectangle.

The problem with IDWriteTextFormat and DrawText is that the entire paragraph is restricted to the same uniform character formatting. You can't apply italics or boldface to individual words.

The ability to apply character formatting comes with IDWriteTextLayout, which is created by a call to the IDWriteFactory method CreateTextLayout based on an existing IDWriteTextFormat object and specifying the text and the layout width and height. You can then (as I'll demonstrate shortly) apply formatting to character runs of this paragraph and render the paragraph with DrawTextLayout:

void ID2D1RenderTarget::DrawTextLayout(

            D2D1_POINT_2F origin,
            [in] IDWriteTextLayout *textLayout,
            [in] ID2D1Brush *defaultForegroundBrush,
            D2D1_DRAW_TEXT_OPTIONS options = D2D1_DRAW_TEXT_OPTIONS_NONE)

This call is somewhat simpler that DrawText because the text and layout rectangle have already been defined as part of the IDWriteTextLayout object. The DrawTextLayout call requires only an indication of the upper-left corner of the text's destination on the rendering surface and a brush.

Internal to DirectWrite, apparently the DrawText method of ID2D1RenderTarget creates its own IDWriteTextLayout object from the IDWriteTextFormat object and passes that to DrawTextLayout. Therefore, for performance reasons programmers usually create their own IDWriteTextLayout objects and not bother with DrawText.

IDWriteTextLayout becomes essential when you want to vary the character formatting within the text. The CharacterFormattingDemo project demonstrates the use of IDWriteTextLayout to define such character formatting. I created the project in Visual Studio 2013 using the C++ / Windows Store / DirectX App (XAML) template.

The constructor of the CharacterFormattingDemoRenderer class creates the IDWriteTextFormat and IDWriteTextLayout objects and applies character formatting:

CharacterFormattingDemoRenderer::CharacterFormattingDemoRenderer(...) ...
{
    ...

    ComPtr<IDWriteTextFormat> textFormat;

    DX::ThrowIfFailed(
        m_deviceResources->GetDWriteFactory()->CreateTextFormat(
            L"Times New Roman",
            nullptr,
            DWRITE_FONT_WEIGHT_NORMAL,
            DWRITE_FONT_STYLE_NORMAL,
            DWRITE_FONT_STRETCH_NORMAL,
            24.0f,
            L"en-us",
            &textFormat)
        );

    std::wstring text = L"This paragraph of text rendered with "
                        L"DirectWrite is based on "
                        L"IDWriteTextFormat and IDWriteTextLayout "
                        L"objects and is capable of numerous types "
                        L"of character formatting, including "
                        L"italic, bold, underline, strikethrough, "
                        L"and using different font sizes and "
                        L"families, such as Courier New "
                        L"or the ever-popular Comic Sans MS.";

    DX::ThrowIfFailed(
        m_deviceResources->GetDWriteFactory()->CreateTextLayout(
            text.c_str(),
            (uint32) text.length(),
            textFormat.Get(),
            440.0f,         // Max width for paragraph
            std::numeric_limits<float>::infinity(),
            &m_textLayout)
        );

    // Set character formatting
    DWRITE_TEXT_RANGE textRange;

    std::wstring strFind = L"IDWriteTextFormat";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();
    DX::ThrowIfFailed(
        m_textLayout->SetFontStyle(DWRITE_FONT_STYLE_ITALIC, textRange)
        );

    strFind = L"IDWriteTextLayout";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();
    DX::ThrowIfFailed(
        m_textLayout->SetFontStyle(DWRITE_FONT_STYLE_ITALIC, textRange)
        );

    strFind = L"italic";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();
    DX::ThrowIfFailed(
        m_textLayout->SetFontStyle(DWRITE_FONT_STYLE_ITALIC, textRange)
        );

    strFind = L"bold";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();
    DX::ThrowIfFailed(
        m_textLayout->SetFontWeight(DWRITE_FONT_WEIGHT_BOLD, textRange)
        );

    strFind = L"underline";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();
    DX::ThrowIfFailed(
        m_textLayout->SetUnderline(true, textRange)
        );

    strFind = L"strikethrough";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();
    DX::ThrowIfFailed(
        m_textLayout->SetStrikethrough(true, textRange)
        );

    strFind = L"sizes";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();
    DX::ThrowIfFailed(
        m_textLayout->SetFontSize(48.0f, textRange)
        );

    strFind = L"Courier New";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();
    DX::ThrowIfFailed(
        m_textLayout->SetFontFamilyName(strFind.data(), textRange)
        );

    strFind = L"Comic Sans MS";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();
    DX::ThrowIfFailed(
        m_textLayout->SetFontFamilyName(strFind.data(), textRange)
        );

    ...
}

Notice the nine calls based on the DWRITE_TEXT_RANGE object. These calls apply formatting to text runs within the paragraph. The DWRITE_TEXT_RANGE structure indicates the character starting point and the number of characters relative to the start of the text. I've used a simple technique here of simply searching the text for specified words and applying the formatting to those. The Render method simply centers the paragraph on the screen, and here's what it looks like:

Notice that the line spacing is automatically adjusted to accomodate larger text sizes and different fonts. It's also possible to specify a uniform line spacing.

Besides SetFontStyle, SetFontWeight, SetFontSize, SetFontFamilyName, SetUnderline, and SetStrikethrough, the IDWriteTextLayout interface also defines similar range-based methods to set a different font collection, stretch, locale name, typography (to get at alternate glyphs), inline object (to display programmer-defined objects), and drawing effect (for an "application-defined drawing effect"). TextLayout1 adds range-based character spacing and kerning.

Text Foreground Colors

Did you notice something missing from the list of range-based formatting you can apply to a paragraph?

What about color?

You may go a little crazy trying to figure out how you can specify different colors on various words in a paragraph of text, but the magic method is actually SetDrawingEffect, which is defined like so:

HRESULT IDWriteTextLayout::SetDrawingEffect(

            IUnknown * drawingEffect,
            DWRITE_TEXT_RANGE textRange)

Notice that the first argument is not a specific type. In the page of MSDN documentation that lists all the methods defined by IDWriteTextLayout, this method is described as "Sets the application-defined drawing effect" (as I quoted earlier). This makes it seem as if it's for advanced use — and I'll be using it in that way later in this blog entry — but if you look at the more detailed documentation of SetDrawingEffect, the Remarks section says:

Gosh, I guess we're going to have to try it out. The ColorTextFormattingDemo project is very similar to the first project except the renderer constructor defines some different text, and saves it as a private data member, so the brush formatting can be applied in the CreateDeviceDependentResources method:

ColorTextFormattingDemoRenderer::ColorTextFormattingDemoRenderer(...) ...
{
    ...

    ComPtr<IDWriteTextFormat> textFormat;

    DX::ThrowIfFailed(
        m_deviceResources->GetDWriteFactory()->CreateTextFormat(
            L"Times New Roman",
            nullptr,
            DWRITE_FONT_WEIGHT_NORMAL,
            DWRITE_FONT_STYLE_NORMAL,
            DWRITE_FONT_STRETCH_NORMAL,
            24.0f,
            L"en-us",
            &textFormat)
        );
    
    m_text = L"This paragraph of text rendered with "
             L"DirectWrite is based on "
             L"IDWriteTextFormat and IDWriteTextLayout "
             L"objects and is capable of different "
             L"RBG RGB foreground colors, such as red, "
             L"green, and blue by passing brushes to "
             L"the SetDrawingEffect method.";

    DX::ThrowIfFailed(
        m_deviceResources->GetDWriteFactory()->CreateTextLayout(
            m_text.c_str(),
            (uint32) m_text.length(),
            textFormat.Get(),
            440.0f,         // Max width for paragraph
            std::numeric_limits<float>::infinity(),
            &m_textLayout)
        );

    // Set character formatting
    DWRITE_TEXT_RANGE textRange;

    std::wstring strFind = L"IDWriteTextFormat";
    textRange.startPosition = m_text.find(strFind.data());
    textRange.length = strFind.length();
    DX::ThrowIfFailed(
        m_textLayout->SetFontStyle(DWRITE_FONT_STYLE_ITALIC, textRange)
        );

    strFind = L"IDWriteTextLayout";
    textRange.startPosition = m_text.find(strFind.data());
    textRange.length = strFind.length();
    DX::ThrowIfFailed(
        m_textLayout->SetFontStyle(DWRITE_FONT_STYLE_ITALIC, textRange)
        );

    strFind = L"SetDrawingEffect";
    textRange.startPosition = m_text.find(strFind.data());
    textRange.length = strFind.length();
    DX::ThrowIfFailed(
        m_textLayout->SetFontStyle(DWRITE_FONT_STYLE_ITALIC, textRange)
        );

    DX::ThrowIfFailed(
        m_textLayout->GetMetrics(&m_textMetrics)
        );

    ...

    CreateDeviceDependentResources();
}

void ColorTextFormattingDemoRenderer::CreateDeviceDependentResources()
{
    // Create brushes and set them
    ID2D1DeviceContext1* context = m_deviceResources->GetD2DDeviceContext();

    ComPtr<ID2D1SolidColorBrush> redBrush;
    DX::ThrowIfFailed(
        context->CreateSolidColorBrush(ColorF(ColorF::Red), &redBrush)
        );

    ComPtr<ID2D1SolidColorBrush> greenBrush;
    DX::ThrowIfFailed(
        context->CreateSolidColorBrush(ColorF(ColorF::Green), &greenBrush)
        );

    ComPtr<ID2D1SolidColorBrush> blueBrush;
    DX::ThrowIfFailed(
        context->CreateSolidColorBrush(ColorF(ColorF::Blue), &blueBrush)
        );

    DWRITE_TEXT_RANGE textRange;

    std::wstring strFind = L"RBG";
    textRange.startPosition = m_text.find(strFind.data());
    textRange.length = strFind.length();
    DX::ThrowIfFailed(
        m_textLayout->SetStrikethrough(true, textRange)
        );

    textRange.length = 1;
    DX::ThrowIfFailed(
        m_textLayout->SetDrawingEffect(redBrush.Get(), textRange)
        );

    textRange.startPosition += 1;
    DX::ThrowIfFailed(
        m_textLayout->SetDrawingEffect(blueBrush.Get(), textRange)
        );

    textRange.startPosition += 1;
    DX::ThrowIfFailed(
        m_textLayout->SetDrawingEffect(greenBrush.Get(), textRange)
        );

    strFind = L"RGB";
    textRange.startPosition = m_text.find(strFind.data());
    textRange.length = strFind.length();
    DX::ThrowIfFailed(
        m_textLayout->SetUnderline(true, textRange)
        );

    textRange.length = 1;
    DX::ThrowIfFailed(
        m_textLayout->SetDrawingEffect(redBrush.Get(), textRange)
        );

    textRange.startPosition += 1;
    DX::ThrowIfFailed(
        m_textLayout->SetDrawingEffect(greenBrush.Get(), textRange)
        );

    textRange.startPosition += 1;
    DX::ThrowIfFailed(
        m_textLayout->SetDrawingEffect(blueBrush.Get(), textRange)
        );

    strFind = L" red";      // avoid "rendered"
    textRange.startPosition = m_text.find(strFind.data()) + 1;
    textRange.length = strFind.length() - 1;
    DX::ThrowIfFailed(
        m_textLayout->SetDrawingEffect(redBrush.Get(), textRange)
        );

    strFind = L"green";
    textRange.startPosition = m_text.find(strFind.data());
    textRange.length = strFind.length();
    DX::ThrowIfFailed(
        m_textLayout->SetDrawingEffect(greenBrush.Get(), textRange)
        );

    strFind = L"blue";
    textRange.startPosition = m_text.find(strFind.data());
    textRange.length = strFind.length();
    DX::ThrowIfFailed(
        m_textLayout->SetDrawingEffect(blueBrush.Get(), textRange)
        );

    // Create default text brush
    DX::ThrowIfFailed(
        context()->CreateSolidColorBrush(ColorF(ColorF::Black), &m_blackBrush)
        );
}

The brush creation needs to occur in the CreateDeviceDependentResources method because the brushes are device-dependent objects and they might need to be recreated at run time. And here's the result:

Of all the character formatting options, the foreground brush is strange in a couple respects. It's the only formatting option that's specified when the text is rendered, either in the DrawText or DrawTextLayout calls. Perhaps this is because if you're using a gradient brush to render the text, you probably need to wait until the text is on the verge of being rendered before defining the start and end points. Also — as you'll see shortly — if IDWriteTextLayout defined a SetForegroundBrush method, it would actually make an implementation of the IDWriteTextRenderer interface somewhat more complex, and implementing the IDWriteTextRenderer interface is the primary job for today.

The Inner Workings

Here's the DrawTextLayout method again:

void ID2D1RenderTarget::DrawTextLayout(

            D2D1_POINT_2F origin,
            [in] IDWriteTextLayout *textLayout,
            [in] ID2D1Brush *defaultForegroundBrush,
            D2D1_DRAW_TEXT_OPTIONS options = D2D1_DRAW_TEXT_OPTIONS_NONE)

When you call this method, you might think that the ID2D1RenderTarget is responsible for rendering the paragraph of text. Not so. Instead, the render target calls a method implemented by IDWriteTextLayout named Draw, which is defined like so:

HRESULT IDWriteTextLayout::Draw(

            void * clientDrawingContext,
            IDWriteTextRenderer * renderer,
            FLOAT originX,
            FLOAT originY)

The implementation of IDWriteTextRenderer passed to this Draw method is internal to DirectX, but (as I'll discuss soon) you can write your own implementations. Notice that the ID2D1Brush object in the DrawTextLayout method is not included among the arguments, but the clientDrawingContext argument can be a pointer to a structure containing any information later required to render the text, including the brush and the render target.

This Draw method does its work in a device-independent manner. One of the first jobs it performs is to use the font collection, font family name, and character formatting originally specified in the CreateTextFormat call to derive an IDWriteFontFace object. This font face is associated with an actual font file, which is the source of the outlines that define all the glyphs in the font, as well as font metric information crucial for formatting a paragraph into multiple lines of text.

The Draw method can then generate DWRITE_GLYPH_RUN objects, each of which defines a run of text with uniform formatting. These glyph runs are passed to the DrawGlyphRun method of the IDWriteTextRenderer and represent an intermediate step in the rendering of text:

HRESULT IDWriteTextRenderer::DrawGlyphRun(

            void * clientDrawingContext,
            FLOAT baselineOriginX,
            FLOAT baselineOriginY,
            DWRITE_MEASURING_MODE  measuringMode,
            [in] const DWRITE_GLYPH_RUN * glyphRun,
            [in] const DWRITE_GLYPH_RUN_DESCRIPTION * glyphRunDescription,
            IUnknown * clientDrawingEffect)

What this demonstrates is that the Draw method of IDWriteTextLayout is reponsible for converting a paragraph of formatted text into a series of glyph runs, and it is these glyph runs that are ultimately rendered on the display surface. Let me emphasize again that this conversion of text into glyph runs occurs in a device-independent manner.

As you can see from the method definitions, the IDWriteTextLayout object simply passes the clientDrawingContext argument in the Draw call to the first argument of DrawGlyphRun method in the IDWriteTextRenderer class. This argument can include information that the IDWriteTextRenderer might need to actually render the text, such as an ID2D1RenderTarget object and a default brush. (Or, that information could be supplied to the IDWriteTextRenderer in its constructor or through an additional method.)

The last argument to DrawGlyphRun is an object originally passed to the SetDrawingEffect method of IDWriteTextLayout. In the default text renderer, this object can specify a foreground brush to override the default brush passed to DrawTextLayout.

Underline and strikethrough are not part of a font and play no role in a glyph run. These are classified as text "decorations." The font file provides metrics recommending the placement and thickness of the underline and strikethrough, but these decorations must be rendered separately. For this reason, the IDWriteTextRenderer interface defines two additional callback methods named DrawUnderline and DrawStrikethrough.

It's also possible for the programmer to define custom inline objects to be displayed along with text. These are implementations of the IDWriteInlineObject interface. The GetMetrics, GetOverhangMetrics, and GetBreakConditions methods of this interface are called when the Draw method of IDWriteTextLayout is laying out the paragraph. Another callback method in IDWriteTextRenderer named DrawInlineObject gives the text renderer the opportunity to call the Draw method in the IDWriteInlineObject object.

Now that we've seen the overall picture, let's try creating a custom text renderer by implementing the IDWriteTextRenderer interface.

A Basic Custom Text Renderer

The TestTextRenderer project contains a simple implementation of IDWriteTextRenderer that I called BasicTextRenderer, which you can find in the Content folder of this project.

I chose to pass the ID2D1RenderTarget and the default ID2D1Brush object through the first argument of the IDWriteTextLayout::Draw method, so the BasicTextRenderer.h file contains a definition of a structure for that purpose:

struct DrawingContext
{
    DrawingContext(ID2D1RenderTarget * renderTarget,
                   ID2D1Brush * defaultBrush)
    {
        this->renderTarget = renderTarget;
        this->defaultBrush = defaultBrush;
    }

    ID2D1RenderTarget * renderTarget;
    ID2D1Brush * defaultBrush;
};

Watch out: There are way too many things named "renderer" in the TestTextRenderer project. Normally I use a naming convention that involves giving the class that renders the program's graphics a name consisting of the project name followed by the word Renderer, so I'm afraid it's named TestTextRendererRenderer. As with the earlier projects, the constructor of that class creates IDWriteTextFormat and IDWriteTextLayout objects. The text consists of the letters "DX" (for "DirectX") in a large font, with underlining and strikethrough applied.

The constructor also instantiates a BasicTextRenderer object that it saves in the private data member m_basicTextRenderer. This is the class that implements the IDWriteTextRenderer interface.

In the Render method of TestTextRendererRenderer, the same text is drawn twice, the first time with a conventional call to DrawTextLayout and then by calling Draw on the IDWriteTextLayout object with the custom renderer:

void TestTextRendererRenderer::Render()
{
    ...

    D2D1_POINT_2F origin = D2D1::Point2F();

    // Display text normally
    context->DrawTextLayout(origin,
                            m_textLayout.Get(),
                            m_blackBrush.Get());
    
    // Display text using custom renderer
    DrawingContext drawingContext(context, 
                                  m_overlayBrush.Get());

    DX::ThrowIfFailed(
        m_textLayout->Draw(&drawingContext, 
                           m_basicTextRenderer.Get(), 
                           origin.x, origin.y)
        );

    ...
}

My primarily motivation in this program was making sure I got the math correct for the underline and strikethrough, so the DrawTextLayout method is called with a black brush, and then the Draw method of the ID2D1TextLayout object is called with a semi-opaque red brush. If the two techniques succeed in rendering identical text, it should appear to be a dark red with no funny stuff going on at the edges:

Now let's take a look at the BasicTextRenderer class responsible for rendering the semi-opaque red text in this program. The methods in BasicTextRenderer are called when the program calls the Draw method of the ID2D1TextLayout object. The DrawGlyphRun callback in this class is implemented like so:

HRESULT BasicTextRenderer::DrawGlyphRun(void * clientDrawingContext,
                                        FLOAT baselineOriginX,
                                        FLOAT baselineOriginY,
                                        DWRITE_MEASURING_MODE measuringMode,
                                        _In_ const DWRITE_GLYPH_RUN * glyphRun,
                                        _In_ const DWRITE_GLYPH_RUN_DESCRIPTION * 
                                            glyphRunDescription,
                                        IUnknown * clientDrawingEffect)
{
    DrawingContext * drawingContext =
        static_cast<DrawingContext *>(clientDrawingContext);

    ID2D1RenderTarget * renderTarget = drawingContext->renderTarget;

    // Get brush
    ID2D1Brush * brush = drawingContext->defaultBrush;
        
    if (clientDrawingEffect != nullptr)
    {
        brush = static_cast<ID2D1Brush *>(clientDrawingEffect);
    }

    renderTarget->DrawGlyphRun(Point2F(baselineOriginX, baselineOriginY), 
                               glyphRun, brush, measuringMode);
    return S_OK;
}

The DrawingContext object in the first argument is the structure that contains the render target (the ID2D1DeviceContext used by the program) and the default brush (semi-opaque red). Any brush set with the SetDrawingEffect method comes through as the last argument, But otherwise the method is fairly simple and direct.

The DrawUnderline and DrawStrikethrough callbacks are similar. They are accompanied by objects of type DWRITE_UNDERLINE and DWRITE_STRIKETHROUGH that indicate the length of the decoration, its thickness, and its distance from the baseline. I implemented both these method using a private method called FillRectangle:

HRESULT BasicTextRenderer::DrawUnderline(void * clientDrawingContext,
                                         FLOAT baselineOriginX,
                                         FLOAT baselineOriginY,
                                         _In_ const DWRITE_UNDERLINE * 
                                             underline,
                                         IUnknown * clientDrawingEffect)

{
    FillRectangle(clientDrawingContext,
                  clientDrawingEffect,
                  baselineOriginX,
                  baselineOriginY + underline->offset,
                  underline->width,
                  underline->thickness,
                  underline->readingDirection,
                  underline->flowDirection);
    return S_OK;
}

HRESULT BasicTextRenderer::DrawStrikethrough(void * clientDrawingContext,
                                             FLOAT baselineOriginX,
                                             FLOAT baselineOriginY,
                                             _In_ const DWRITE_STRIKETHROUGH * 
                                                 strikethrough,
                                             IUnknown * clientDrawingEffect)
{
    FillRectangle(clientDrawingContext, 
                  clientDrawingEffect, 
                  baselineOriginX, 
                  baselineOriginY + strikethrough->offset, 
                  strikethrough->width, 
                  strikethrough->thickness, 
                  strikethrough->readingDirection, 
                  strikethrough->flowDirection);
    return S_OK;
}

void BasicTextRenderer::FillRectangle(void * clientDrawingContext, 
                                      IUnknown * clientDrawingEffect,
                                      float x, float y, 
                                      float width, float thickness, 
                                      DWRITE_READING_DIRECTION readingDirection, 
                                      DWRITE_FLOW_DIRECTION flowDirection)
{
    DrawingContext * drawingContext =
        static_cast<DrawingContext *>(clientDrawingContext);

    // Get brush
    ID2D1Brush * brush = drawingContext->defaultBrush;

    if (clientDrawingEffect != nullptr)
    {
        brush = static_cast<ID2D1Brush *>(clientDrawingEffect);
    }

    D2D1_RECT_F rect = RectF(x, y, x + width, y + thickness);
    drawingContext->renderTarget->FillRectangle(&rect, brush);
}

These decorations might also be applied to text that runs horizontally rather than vertically, in which case the rectangle must be constructed a little differently. However, I was unable to use a custom text renderer with vertical text. Because I couldn't test my code for vertical text so I didn't implement anything.

At first I believed that I'd also have to construct the rectangle differently for text that runs from right to left, such as Hebrew and Arabic. However, it is my experience that this is not necessary. Regardless, the FillRectangle call has unused arguments for DWRITE_READING_DIRECTION (which is left-to-right for English text) and DWRITE_FLOW_DIRECTION (which is always perpendicular to the reading direction and is top-to-bottom for English text). These arguments will disappear in the later projects.

Here's the DrawInlineObject method that I implemented quite simply:

HRESULT BasicTextRenderer::DrawInlineObject(void * clientDrawingContext,
                                            FLOAT originX,
                                            FLOAT originY,
                                            IDWriteInlineObject * inlineObject,
                                            BOOL isSideways,
                                            BOOL isRightToLeft,
                                            IUnknown * clientDrawingEffect)
{
    DrawingContext * drawingContext =
        static_cast<DrawingContext *>(clientDrawingContext);

    return inlineObject->Draw(clientDrawingContext, 
                              this, 
                              originX, 
                              originY, 
                              isSideways, 
                              isRightToLeft, 
                              clientDrawingEffect);
}

Of course, IDWriteTextRenderer derives from IUnknown so BasicTextRenderer also implements AddRef, Release, and QueryInterface.

But IDWriteTextRenderer also derives from IDWritePixelSnapping, and here's why: To render text with as much readability as possible, rendered glyphs should be aligned to pixel boundaries. The glyph runs that come through the DrawGlyphRun method of the IDWriteTextRenderer have already been aligned. But this alignment requires some information about the render target, and the process of generating the glyphs and decorations from the IDWriteTextLayout otherwise occurs in a device-independent manner.

That's where the IDWritePixelSnapping methods come into play. There are three of them. When your program calls the Draw method of your IDWriteTextRenderer implementation, the IsPixelSnappingDisabled method is called first. Here's how BasicTextRenderer implements it:

HRESULT BasicTextRenderer::IsPixelSnappingDisabled(void * clientDrawingContext, 
                                                   _Out_ BOOL * isDisabled)
{
    *isDisabled = false;
    return S_OK;
}

You probably shouldn't disable pixel snapping if you're rendering on the screen, but if your custom renderer is doing something different, you have that option.

If pixel snapping is not disabled, then the other two methods are called:

HRESULT BasicTextRenderer::GetPixelsPerDip(void * clientDrawingContext,
                                           _Out_ FLOAT * pixelsPerDip)
{
    DrawingContext * drawingContext =
        static_cast<DrawingContext *>(clientDrawingContext);

    float dpiX, dpiY;
    drawingContext->renderTarget->GetDpi(&dpiX, &dpiY);
    *pixelsPerDip = dpiX / 96;
    return S_OK;
}

HRESULT BasicTextRenderer::GetCurrentTransform(void * clientDrawingContext,
                                               DWRITE_MATRIX * transform)
{
    DrawingContext * drawingContext = 
        static_cast<DrawingContext *>(clientDrawingContext);

    // Matrix structures are defined identically
    drawingContext->renderTarget->GetTransform((D2D1_MATRIX_3X2_F *) transform);
    return S_OK;
}

This is all the information that's required for the Draw method of IDWriteTextLayout to convert drawing coordinates to pixels and vice versa for the purposes of pixel snapping.

Of course, the TestTextRenderer program barely puts BasicTextRenderer through its paces, so the class is also integrated into the first two projects I described: CharacterFormattingDemo and ColorTextFormattingDemo. Both programs contain a CheckBox on the page that lets you switch between the built-in text renderer and BasicTextRenderer. The Render method in both projects contains the following code:

    D2D1_POINT_2F origin = Point2F();

    if (!m_useCustomRenderer)
    {
        // Built-in text renderer
        context->DrawTextLayout(origin,
                                m_textLayout.Get(),
                                m_blackBrush.Get());
    }
    else
    {
        // Custom text renderer
        DrawingContext drawingContext(context,
                                      m_blackBrush.Get());

        m_textLayout->Draw(&drawingContext, m_textRenderer.Get(), 
                           origin.x, origin.y);
    }

And you'll discover that in both programs, the display of the paragraphs doesn't change when you check and uncheck the CheckBox.

The Sequence of Callbacks

You may be curious about the sequence of callbacks in the IDWriteTextRenderer implementation when you call the Draw method of the IDWriteTextLayout object. Let me use CharacterFormattingDemo as an example because that's the most interesting. You can explore this process on your own by setting breakpoints on the methods in BasicTextRenderer and perhaps inserting a few statements to obtain other information. (I'll ignore the three IUnknown methods in this discussion.)

When Draw is called, the first methods called in your IDWriteTextRenderer implementation are IsPixelSnappingDisabled and (if pixel snapping is not disabled) GetPixelsPerDip and GetCurrentTransform.

Next is a series of calls to DrawGlyphRun. It is my experience that DrawGlyphRun calls occur sequentially from the beginning of the paragraph to the end. A DrawGlyphRun call never involves text that straddles two lines. A new DrawGlyphRun call occurs whenever the character formatting changes. A separate DrawGlyphRun call occurs for the whitespace character (or characters) that ends each line within the formatted paragraph.

The DWRITE_GLYPH_RUN_DESCRIPTION object passed to DrawGlyphRun can help you match up the DrawGlyphRun calls to the original text specified when CreateTextLayout was called. The string field points to somewhere within the original string. The textPosition indicates the offset from the beginning of the original string, while stringLength indicates the number of characters represented by this glyph run. (Keep in mind that the number of glyphs often matches the number of characters but not always. For example, a ligature is a single glyph corresponding to two or three characters.)

For reference, here's the paragraph of text displayed by the CharacterFormattingDemo program:

The first three lines of the formatted paragraph are rendered with nine calls to DrawGlyphRun, with the following information available in the DWRITE_GLYPH_RUN_DESCRIPTION structure:

string textPosition stringLength
"This paragraph of text..." 0 36
" DirectWrite is based..." 36 1
"DirectWrite is based on..." 37 24
"IDWriteTextFormat and..." 61 17
" and IDWriteTextLayout..." 78 1
"and IDWriteTextLayout objects..." 79 4
"IDWriteTextLayout objects and..." 83 17
" objects and is capable of..." 100 23
" of numerous types of..." 123 1

Don't let the string field deceive you into how much text is involved in each DrawGlyphRun call! Look at the stringLength for the exact character count.

The DWRITE_GLYPH_RUN object provides other information, including the number of glyphs and the em size. The fontFace field references the actual font file involved in the text. For most of the glyph runs in this particular paragraph, the fontFace object references the times.ttf file, but the 4th and 7th DrawGlyphRun calls reference the italic version, timesi.ttf, and later on, the timesbd.ttf, cour.ttf, and comic.ttf files are referenced.

I suspect that DrawInlineObject is called between two DrawGlyphRun calls whenever such an object occurs in the paragraph. Following all the DrawGlyphRun calls, the DrawUnderline method is called (just once for this example) and then DrawStrikethrough (again, just once), at which point the Draw method of IDWriteTextLayout returns control back to the program.

A Couple Easy Variations

Because the DrawGlyphRun method in the IDWriteTextRenderer implementation has the actual glyph run, it can alternatively obtain the geometry outlines of the text characters and render those. Here's a possible DrawGlyphRun method that does just that:

HRESULT BasicTextRenderer::DrawGlyphRun(void * clientDrawingContext,
                                        FLOAT baselineOriginX,
                                        FLOAT baselineOriginY,
                                        DWRITE_MEASURING_MODE measuringMode,
                                        _In_ const DWRITE_GLYPH_RUN * glyphRun,
                                        _In_ const DWRITE_GLYPH_RUN_DESCRIPTION * 
                                            glyphRunDescription,
                                        IUnknown * clientDrawingEffect)
{
    DrawingContext * drawingContext =
        static_cast<DrawingContext *>(clientDrawingContext);

    ID2D1RenderTarget * renderTarget = drawingContext->renderTarget;

    // Get brush
    ID2D1Brush * brush = dynamic_cast<ID2D1Brush *>(clientDrawingEffect);

    if (brush == nullptr)
    {
        brush = drawingContext->defaultBrush;
    }

    // Create path geometry
    ComPtr<ID2D1Factory> factory;
    renderTarget->GetFactory(&factory);

    ComPtr<ID2D1PathGeometry> pathGeometry;
    HRESULT hr = factory->CreatePathGeometry(&pathGeometry);

    if (hr != S_OK)
        return hr;

    // Fill it with the glyph run outline
    ComPtr<ID2D1GeometrySink> geometrySink;
    hr = pathGeometry->Open(&geometrySink);

    if (hr != S_OK)
        return hr;

    hr = glyphRun->fontFace->GetGlyphRunOutline(glyphRun->fontEmSize, 
                                                glyphRun->glyphIndices, 
                                                glyphRun->glyphAdvances, 
                                                glyphRun->glyphOffsets, 
                                                glyphRun->glyphCount, 
                                                glyphRun->isSideways, 
                                                glyphRun->bidiLevel % 2 == 1,  
                                                geometrySink.Get());
    if (hr != S_OK)
        return hr;

    hr = geometrySink->Close();

    if (hr != S_OK)
        return hr;

    // Transform the geometry based on the baseline
    Matrix3x2F translation = Matrix3x2F::Translation(baselineOriginX,
                                                     baselineOriginY);
    Microsoft::WRL::ComPtr<ID2D1TransformedGeometry> xformedGeometry;

    hr = factory->CreateTransformedGeometry(pathGeometry.Get(), 
                                            translation, 
                                            &xformedGeometry);
    if (hr != S_OK)
        return hr;

    // Fill the transformed geometry
    renderTarget->FillGeometry(xformedGeometry.Get(), brush);

    return S_OK;
}

If you use this version in CharacterFormattingDemo (for example) you will now see a little difference as you click back and forth between the built-in text renderer and this custom text renderer. I am not sure why that is, but I suspect that it results from a loss of pixel alignment when the geometry is transformed in this code.

You can try these alternative DrawUnderline and DrawStrikethrough methods in ColorTextFormattingDemo:

HRESULT BasicTextRenderer::DrawUnderline(void * clientDrawingContext,
                                         FLOAT baselineOriginX,
                                         FLOAT baselineOriginY,
                                         _In_ const DWRITE_UNDERLINE * 
                                             underline,
                                         IUnknown * clientDrawingEffect)

{
    FillRectangle(clientDrawingContext,
                  clientDrawingEffect,
                  baselineOriginX,
                  baselineOriginY + underline->offset 
                                        - underline->thickness,
                  underline->width,
                  underline->thickness,
                  underline->readingDirection,
                  underline->flowDirection);

    FillRectangle(clientDrawingContext,
                  clientDrawingEffect,
                  baselineOriginX,
                  baselineOriginY + underline->offset 
                                        + underline->thickness,
                  underline->width,
                  underline->thickness,
                  underline->readingDirection,
                  underline->flowDirection);
    return S_OK;
}

HRESULT BasicTextRenderer::DrawStrikethrough(void * clientDrawingContext,
                                             FLOAT baselineOriginX,
                                             FLOAT baselineOriginY,
                                             _In_ const DWRITE_STRIKETHROUGH * 
                                                 strikethrough,
                                             IUnknown * clientDrawingEffect)
{
    FillRectangle(clientDrawingContext,
                  clientDrawingEffect,
                  baselineOriginX,
                  baselineOriginY + strikethrough->offset 
                                        + strikethrough->thickness,
                  strikethrough->width,
                  strikethrough->thickness,
                  strikethrough->readingDirection,
                  strikethrough->flowDirection);

    FillRectangle(clientDrawingContext,
                  clientDrawingEffect,
                  baselineOriginX,
                  baselineOriginY + strikethrough->offset 
                                        - strikethrough->thickness,
                  strikethrough->width,
                  strikethrough->thickness,
                  strikethrough->readingDirection,
                  strikethrough->flowDirection);
    return S_OK;
}

And now the custom text renderer draws double underlines and double strikethroughs:

Look close (not at the image on this web page but at the actual program output) and you'll notice that these underlines and strikeouts are anti-aliased somewhat differently. This results from inconsistent pixel alignment. Taking pixel alignment into account is certainly one of the goals in a better custom text renderer.

Another goal will be controlling this formatting so we're still able to get single underlines and single strikethroughs as well as the double versions.

A First Step at Custom Character Formatting

The key to controlling custom character formatting is defining a class that derives from IUnknown and passing an instance of that class to the SetDrawingEffect method of the IDWriteTextLayout object. The object passed to SetDrawingEffect is then available to the methods in the class that implements IDWriteTextRenderer, which can customize the rendering based on information in that instance.

To nail down some of the basics, I started simple. The UnderlineDemo project implements double and triple underlining. When I realized that much of the same logic is involved, I threw in double and triple strikethrough as well.

I also attempted to standardize my naming conventions. The class that implements IDWriteTextRenderer I will be calling a formatter, and in this project the class is named UnderlineFormatter. The class that implements IUnknown (instances of which are passed to the SetDrawingEffect method) is something I think of as a format specifier, and in this project is named UnderlineSpecifier.

(By the way, I don't like the name of the SetDrawingEffect method at all. The word "effect" is used in Direct2D for something else entirely, as documented here, so the SetDrawingEffect method should more properly be called something like SetTextFormattingObject.)

Primarily to simplify the coding, I decided to make UnderlineSpecifier objects immutable. Consequently, the class is so simple that the code file contains only the bodies of the IUnknown methods, and everything else is in the header:

class UnderlineSpecifier : public IUnknown
{
public:
    UnderlineSpecifier(int underlineCount, int strikethroughCount) :
        m_refCount(0),
        m_underlineCount(underlineCount),
        m_strikethroughCount(strikethroughCount)
    {
    }

    // IUnknown methods
    virtual ULONG STDMETHODCALLTYPE AddRef() override;
    virtual ULONG STDMETHODCALLTYPE Release() override;
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, 
                                                     void **ppvObject) override;

    int GetUnderlineCount() { return m_underlineCount; }
    int GetstrikethroughCount() { return m_strikethroughCount; }

private:
    LONG m_refCount;
    int  m_underlineCount;
    int  m_strikethroughCount;
};

The paragraph of text associated with the IDWriteTextLayout object created by the UnderlineDemoRenderer constructor is very similar to the text in the ColorTextFormattingDemo project, except with an added sentence to show off the new formatting. The CreateDeviceDependentResources method continues to use SetDrawingEffect for setting text brushes, while the constructor calls SetDrawingEffect with instances of the UnderlineSpecifier class:

UnderlineDemoRenderer::UnderlineDemoRenderer(...) ...
{
    ...

    m_text = L"This paragraph of text rendered with "
             L"DirectWrite is based on "
             L"IDWriteTextFormat and IDWriteTextLayout "
             L"objects and is capable of different "
             L"RBG RGB foreground colors, such as red, "
             L"green, and blue by passing brushes to "
             L"the SetDrawingEffect method. "
             L"Or, an instance of an UnderlineFormatter "
             L"can be passed to that method for double "
             L"underline, triple underline, double "
             L"strikethrough, triple strikethrough, "
             L"or combinations thereof.";

    ...

    // Set custom underlining and strikethrough
    strFind = L"double underline";
    textRange.startPosition = m_text.find(strFind.data());
    textRange.length = strFind.length();

    DX::ThrowIfFailed(
        m_textLayout->SetUnderline(true, textRange)
        );

    DX::ThrowIfFailed(
        m_textLayout->SetDrawingEffect(new UnderlineSpecifier(2, 0), textRange)
        );

    strFind = L"triple underline";
    textRange.startPosition = m_text.find(strFind.data());
    textRange.length = strFind.length();

    DX::ThrowIfFailed(
        m_textLayout->SetUnderline(true, textRange)
        );

    DX::ThrowIfFailed(
        m_textLayout->SetDrawingEffect(new UnderlineSpecifier(3, 0), textRange)
        );

    strFind = L"double strikethrough";
    textRange.startPosition = m_text.find(strFind.data());
    textRange.length = strFind.length();

    DX::ThrowIfFailed(
        m_textLayout->SetStrikethrough(true, textRange)
        );

    DX::ThrowIfFailed(
        m_textLayout->SetDrawingEffect(new UnderlineSpecifier(0, 2), textRange)
        );

    strFind = L"triple strikethrough";
    textRange.startPosition = m_text.find(strFind.data());
    textRange.length = strFind.length();

    DX::ThrowIfFailed(
        m_textLayout->SetStrikethrough(true, textRange)
        );

    DX::ThrowIfFailed(
        m_textLayout->SetDrawingEffect(new UnderlineSpecifier(0, 3), textRange)
        );

    strFind = L"combinations";
    textRange.startPosition = m_text.find(strFind.data());
    textRange.length = strFind.length();

    DX::ThrowIfFailed(
        m_textLayout->SetUnderline(true, textRange)
        );

    DX::ThrowIfFailed(
        m_textLayout->SetStrikethrough(true, textRange)
        );

    DX::ThrowIfFailed(
        m_textLayout->SetDrawingEffect(new UnderlineSpecifier(3, 2), textRange)
        );

    strFind = L"thereof";
    textRange.startPosition = m_text.find(strFind.data());
    textRange.length = strFind.length();

    DX::ThrowIfFailed(
        m_textLayout->SetUnderline(true, textRange)
        );

    DX::ThrowIfFailed(
        m_textLayout->SetStrikethrough(true, textRange)
        );

    DX::ThrowIfFailed(
        m_textLayout->SetDrawingEffect(new UnderlineSpecifier(2, 3), textRange)
        );

    // Get text metrics
    DX::ThrowIfFailed(
        m_textLayout->GetMetrics(&m_textMetrics)
        );

    // Instantiate UnderlineFormatter
    m_underlineFormatter = new UnderlineFormatter();

    CreateDeviceDependentResources();
}

Notice that for double and triple underlining and strikethroughs, the regular SetUnderline and SetStrikethough methods are called as well. These are necessary for the class that implements IDWriteTextRenderer to get DrawUnderline and DrawStrikethrough calls for those runs.

Towards the end of the constructor, the UnderlineFormatter class is instantiated. This is passed to the Draw method of the IDWriteTextLayout object in the Render method:

oid UnderlineDemoRenderer::Render()
{
    ...

    // Display paragraph of text with custom text renderer
    D2D1_POINT_2F origin = Point2F();
    DrawingContext drawingContext(context, m_blackBrush.Get());

    m_textLayout->Draw(&drawingContext, m_underlineFormatter.Get(),
                       origin.x, origin.y);

    ...
}

So, in the UnderlineFormatter class, the clientDrawingEffect argument to the DrawGlyphRun, DrawUnderline, DrawStrikethrough, and DrawInlineObject methods can be either nullptr or one of two types: an ID2D1Brush object or an instance of UnderlineSpecifier indicating the underline and strikethrough count.

To help figure out what that clientDrawingEffect is, the class defines the following method:

void UnderlineFormatter::GetTextDrawingEffects(IUnknown * clientDrawingEffect,
                                               ID2D1Brush ** pBrush,
                                               UnderlineSpecifier ** pUnderlineSpecifier)
{
    if (clientDrawingEffect != nullptr)
    {
        void * pInterface;

        if (S_OK == clientDrawingEffect->QueryInterface(__uuidof(ID2D1Brush), 
                                                        &pInterface))
            *pBrush = (ID2D1Brush *) pInterface;

        else
            *pUnderlineSpecifier = (UnderlineSpecifier *) clientDrawingEffect;
    }
}

The DrawGlyphRun method doesn't need the UnderlineSpecifier instance, but it calls the GetTextDrawingEffects method anyway to possibly obtain a brush object to override the default brush:

HRESULT UnderlineFormatter::DrawGlyphRun(void * clientDrawingContext,
                                         FLOAT baselineOriginX,
                                         FLOAT baselineOriginY,
                                         DWRITE_MEASURING_MODE measuringMode,
                                         _In_ const DWRITE_GLYPH_RUN * glyphRun,
                                         _In_ const DWRITE_GLYPH_RUN_DESCRIPTION *
                                             glyphRunDescription,
                                         IUnknown * clientDrawingEffect)
{
    DrawingContext * drawingContext =
        static_cast<DrawingContext *>(clientDrawingContext);

    ID2D1RenderTarget * renderTarget = drawingContext->renderTarget;

    // Get brush and underline specifier
    ID2D1Brush * brush = drawingContext->defaultBrush;
    UnderlineSpecifier * underlineSpecifier = nullptr;
    GetTextDrawingEffects(clientDrawingEffect, &brush, &underlineSpecifier);

    renderTarget->DrawGlyphRun(Point2F(baselineOriginX, baselineOriginY),
                               glyphRun, brush, measuringMode);

    return S_OK;
}

The DrawUnderline and DrawStrikethrough methods are implemented very similarly, so I'll just show you the underline code. Here's DrawUnderline that uses GetTextDrawingEffects to obtain either a brush or an UnderlineSpecifier, and then gets the underline count:

HRESULT UnderlineFormatter::DrawUnderline(void * clientDrawingContext,
                                          FLOAT baselineOriginX,
                                          FLOAT baselineOriginY,
                                          _In_ const DWRITE_UNDERLINE *
                                              underline,
                                          IUnknown * clientDrawingEffect)

{
    DrawingContext * drawingContext =
        static_cast<DrawingContext *>(clientDrawingContext);

    // Get brush and underline specifier
    ID2D1Brush * brush = drawingContext->defaultBrush;
    UnderlineSpecifier * underlineSpecifier = nullptr;
    GetTextDrawingEffects(clientDrawingEffect, &brush, &underlineSpecifier);

    // Get underline count
    int underlineCount = 1;

    if (underlineSpecifier != nullptr)
        underlineCount = underlineSpecifier->GetUnderlineCount();

    if (underlineCount < 1 || underlineCount > 3)
        return E_INVALIDARG;

    if (underlineCount == 1 || underlineCount == 3)
    {
        FillRectangle(drawingContext->renderTarget,
                      brush,
                      baselineOriginX,
                      baselineOriginY + underline->offset,
                      underline->width,
                      underline->thickness);
    }

    if (underlineCount == 2 || underlineCount == 3)
    {
        FillRectangle(drawingContext->renderTarget,
                      brush,
                      baselineOriginX,
                      baselineOriginY + underline->offset + 
                          (underlineCount - 1) * underline->thickness,
                      underline->width,
                      underline->thickness);

        FillRectangle(drawingContext->renderTarget,
                      brush,
                      baselineOriginX,
                      baselineOriginY + underline->offset - 
                          (underlineCount - 1) * underline->thickness,
                      underline->width,
                      underline->thickness);
    }

    return S_OK;
}

The FillRectangle method has changed a bit from the previous version, and now makes a calculation to snap the Y coordinate to the nearest pixel:

void UnderlineFormatter::FillRectangle(ID2D1RenderTarget * renderTarget,
                                       ID2D1Brush * brush,
                                       float x, float y,
                                       float width, float thickness)
{
    // Snap the y coordinate to the nearest pixel
    D2D1_POINT_2F pt = Point2F(0, y);
    pt = m_worldToPixel.TransformPoint(pt);
    pt.y = (float) (int) (pt.y + 0.5f);
    pt = m_pixelToWorld.TransformPoint(pt);
    y = pt.y;

    // Fill the rectangle
    D2D1_RECT_F rect = RectF(x, y, x + width, y + thickness);
    renderTarget->FillRectangle(&rect, brush);
}

Where do those transforms come from? The class maintains four private members of type Matrix3x2F and sets them during the calls to GetPixelsPerDip and GetCurrentTransform:

HRESULT UnderlineFormatter::GetPixelsPerDip(void * clientDrawingContext,
                                            _Out_ FLOAT * pixelsPerDip)
{
    ...

    // Save DPI as transform for pixel snapping
    m_dpiTransform = Matrix3x2F::Scale(dpiX / 96.0f, dpiY / 96.0f);
    m_worldToPixel = m_renderTransform * m_dpiTransform;
    m_pixelToWorld = m_worldToPixel;
    m_pixelToWorld.Invert();

    return S_OK;
}

HRESULT UnderlineFormatter::GetCurrentTransform(void * clientDrawingContext,
                                                DWRITE_MATRIX * transform)
{
    ...

    // Save transform for pixel snapping
    m_renderTransform = *(Matrix3x2F *) transform;
    m_worldToPixel = m_renderTransform * m_dpiTransform;
    m_pixelToWorld = m_worldToPixel;
    m_pixelToWorld.Invert();

    return S_OK;
}

The use of similar code in both methods ensures the same result regardless which method is called first.

And here's the result:

And would you believe I screwed up the pixel snapping? Yes, the top of each underline and strikethrough is now pixel-aligned, but due to rounding errors, the gap between the lines is not consistent. Look at the "triple strikethrough" text: On the first line (the penultimate line of the paragraph), the three lines are evenly spaced, but on the next line they are not.

Note to self: Improve pixel snapping logic.

But here's a potentially more serious problem: There is no way to combine this custom formatting of double and triples underlines and strikethroughs with a non-default color. That clientDrawingEffect object passed to the UnderlineFormatter class is either an ID2D1Brush object or an UnderlineSpecifier instance. It cannot be both! The underline and strikethrough on the colored text still works because the code assumes one underline or strikethrough in the DrawUnderline and DrawStrikethrough methods, but if that argument is a brush, it cannot also be an UnderlineSpecifier.

This implies that you probably want to be passing only objects of one type to the SetDrawingEffect method, and dealing only with objects of one type in the IDWriteTextRenderer methods. This means that the specifier class needs to incorporate text coloring as well as any other custom formatting you want to implement.

The Problem of Overlapping Format Specifications

Take a look at all the range-based Set methods defined by IDWriteTextLayout and IDWriteTextLayout1 that specify character formatting. Some involve a floating point value or values (SetFontSize, SetCharacterSpacing), some involve enumerations (SetFontStretch, SetFontStyle, SetFontWeight), some involve strings (SetFontFamilyName, SetLocaleName), some involve booleans (SetStrikethrough, SetUnderline, SetPairKerning), and some involve actual objects — SetFontCollection, SetInlineObject, SetTypography, and, of course, today's favorite, SetDrawingEffect.

These methods all work similarly because they all require an instance of the DWRITE_TEXT_RANGE structure, which has two members of type UINT32 named startPosition and length.

Suppose you have an IDWriteTextLayout object named textLayout and an instance of the DWRITE_TEXT_RANGE structure named textRange and you make the following call:

    textRange.startPosition = 15;
    textRange.length = 30;
    textLayout->SetFontSize(24.0f, textRange);

From the character at an index of 15 in the text up to (but not including) the character at index 45, the font size is now 24. Outside that range, it's the value originally specified in the IDWriteTextFormat object.

You then make the following call:

    textRange.startPosition = 25;
    textRange.length = 10;
    textLayout->SetFontSize(36.0f, textRange);

Now the characters from an index of 25 up to (but not including) an index of 35 have a larger font size. Within this range, the font size set with the prevous SetFontSize call has been replaced.

A similar thing happens when you call SetDrawingEffect using two instances of some kind of specifier class that indicates the formatting you want:

    textRange.startPosition = 15;
    textRange.length = 30;
    textLayout->SetDrawingEffect(specifier1, textRange);

    textRange.startPosition = 25;
    textRange.length = 10;
    textLayout->SetDrawingEffect(specifier2, textRange);

From character index 15 through 24, specifier1 is set; from 25 through 34, specifier2 is set; and from 35 through 44, specifier1 is set.

But suppose that specifier1 specifies formatting using a blue brush and specifier2 specifies double underlining. Did you want that double underlining to replace the blue brush for that range? Well, maybe. But it's much more likely that within that range you want a blue brush and double underlining.

This implies that you actually want a specifier3 object to apply to that middle range, where specifier3 includes both a blue brush and double underlining. And, if the specifier2 object is no longer being used for anything, it should be released and delete itself.

This little problem makes the coding of a general-purpose specifier class not quite as easy as it would be otherwise. You need unique instances of this specifier class for every combination of special formatting you want in your rendered text, and yet when your program is actually making the calls to SetDrawingEffect, you probably don't want to worry about potentially overlapping format specifications.

One method that will help you to implement a clean algorithm is GetDrawingEffect, which is similar to the other Get methods defined by IDWriteTextLayout:

    CharacterFormatSpecifier * specifier;
    textLayout->GetDrawingEffect(currentPosition, 
                                 (IUnknown **) &specifier, 
                                 &textRange);

The currentPosition argument is an index into the text string stored by the IDWriteTextLayout object. The method then sets the CharacterFormatSpecifier pointer and the DWRITE_TEXT_RANGE applicable for that currentPosition.

Suppose you've made the two calls to SetDrawingEffect shown above, and then you call GetDrawingEffect using a currentPosition of 5. On return, the specifier variable is set to nullptr, and the DWRITE_TEXT_RANGE object indicates the range surrounding this position for which there is nothing set: a startPosition of 0 and a length of 15. You'll get the same result for a currentPosition argument anywhere from 0 through 14.

If you call GetDrawingEffect with a currentPosition of 20 (or anything from 15 through 24), then the specifier variable will be set equal to the specifier1 object, and the DWRITE_TEXT_RANGE object will indicate a startPosition of 15 and a length of 10, which is the range where this CharacterFormatSpecifier instance is applicable. Notice that this DWRITE_TEXT_RANGE has a combination of startPosition and length values that did not appear in any earlier SetDrawingEffect call.

If you're like me, an algorithm to implement a general-purpose format specifier is already taking form in your mind, and you can either pursue your own ideas or take a look at mine.

An Approach to Character Formatting Extensions

The final downloadable project for this blog entry is CustomFormattingDemo. But I want to show you how the code evolved, so I'll be displaying some code that does not actually appear in the final project.

The format specifier in this project is named (like the example I just showed) CharacterFormatSpecifier. The first version of my specfier supported a text foreground brush, and double and triple underlining and strikethrough. However modest, this represented a step up over the UnderlineDemo project because it allows a combination of a foreground brush and underlining. In addition, I've also allowed using a separate brush for the underlines and strikethroughs.

The strategy I took in CharacterFormatSpecifier is to make all the Set methods static. This allows the methods themselves to instantiate CharacterFormatSpecifier instances to accomodate possibly overlapping formatting specifications. Sometimes an entirely new CharacterFormatSpecifier is required, and sometimes an earlier CharacterFormatSpecifier must be cloned and modified. This is how a particular instance can include combinations of different formatting. Consequently, the CharacterFormatSpecier constructor and this Clone method are protected so they can be used only within the class.

Here's the CharacterFormatSpecifier header file incorporating the bodies of the Get methods:

class CharacterFormatSpecifier : IUnknown
{
public:
    // IUnknown methods
    virtual ULONG STDMETHODCALLTYPE AddRef() override;
    virtual ULONG STDMETHODCALLTYPE Release() override;
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid,
                                                     void **ppvObject) override;

    // Public Set and Get methods
    //  - - - - - - - - - - - - -

    // Foreground brush
    static HRESULT SetForegroundBrush(IDWriteTextLayout * textLayout,
                                      ID2D1Brush * brush,
                                      DWRITE_TEXT_RANGE textRange);

    ID2D1Brush * GetForegroundBrush() 
    { 
        return m_foregroundBrush.Get(); 
    }

    // Underline
    static HRESULT SetUnderline(IDWriteTextLayout * textLayout,
                                int count,
                                ID2D1Brush * brush,
                                DWRITE_TEXT_RANGE textRange);

    void GetUnderline(int * pCount, ID2D1Brush ** pBrush)
    { 
        * pCount = m_underlineCount; 
        * pBrush = m_underlineBrush.Get(); 
    }

    // Strikethrough
    static HRESULT SetStrikethrough(IDWriteTextLayout * textLayout,
                                    int count,
                                    ID2D1Brush * brush,
                                    DWRITE_TEXT_RANGE textRange);

    void GetStrikethrough(int * pCount, ID2D1Brush ** pBrush) 
    { 
        * pCount = m_strikethroughCount;
        * pBrush = m_strikethroughBrush.Get(); 
    }

protected:
    CharacterFormatSpecifier();             // constructor
    CharacterFormatSpecifier * Clone();

    static HRESULT SetFormatting(IDWriteTextLayout * textLayout,
                                 DWRITE_TEXT_RANGE textRange,
        std::function<void(CharacterFormatSpecifier *)> setField);

private:
    LONG m_refCount;

    Microsoft::WRL::ComPtr<ID2D1Brush> m_foregroundBrush;

    int                                m_underlineCount;
    Microsoft::WRL::ComPtr<ID2D1Brush> m_underlineBrush;

    int                                m_strikethroughCount;
    Microsoft::WRL::ComPtr<ID2D1Brush> m_strikethroughBrush;
};

These Set methods work very similarly, so I implemented them with a common method using a lambda function as a callback. Here are the static SetForegroundBrush and SetUnderline methods. SetStrikethrough is very similar.

HRESULT CharacterFormatSpecifier::SetForegroundBrush(
                                        IDWriteTextLayout * textLayout,
                                        ID2D1Brush * brush,
                                        DWRITE_TEXT_RANGE textRange)
{
    return SetFormatting(textLayout, 
                         textRange, 
                         [brush](CharacterFormatSpecifier * specifier)
    {
        specifier->m_foregroundBrush = brush;
    });
}

HRESULT CharacterFormatSpecifier::SetUnderline(IDWriteTextLayout * textLayout,
                                               int count,
                                               ID2D1Brush * brush,
                                               DWRITE_TEXT_RANGE textRange)
{
    if (count < 0 || count > 3)
    {
        return E_INVALIDARG;
    }

    textLayout->SetUnderline(count > 0, textRange);

    return SetFormatting(textLayout, 
                         textRange, 
                         [count, brush](CharacterFormatSpecifier * specifier)
    {
        specifier->m_underlineCount = count;
        specifier->m_underlineBrush = brush;
    });
}

Notice that the SetUnderline method calls the regular SetUnderline method on the IDWriteTextLayout object to ensure that the formatter gets a call to DrawUnderline.

The static SetFormatting method called by each static Set method is responsible for calling the lambda function with a new CharacterFormatSpecifier instance, which may or may not incorporate previous formatting. If you consider that there might be several sub-ranges formatted differently, a single Set call could involve several CharacterFormatSpecifier instances, and several calls back to the lambda function.

So here's the all-important static SetFormatting method:

HRESULT CharacterFormatSpecifier::SetFormatting(IDWriteTextLayout * textLayout,
                                                DWRITE_TEXT_RANGE textRange,
                std::function<void(CharacterFormatSpecifier *)> setField)
{
    // Get information from the text range to set
    const UINT32 endPosition = textRange.startPosition + textRange.length;
    UINT32 currentPosition = textRange.startPosition;

    // Loop until we're at the end of the range
    while (currentPosition < endPosition)
    {
        // Get the drawing effect at the current position
        CharacterFormatSpecifier * specifier = nullptr;
        DWRITE_TEXT_RANGE queryTextRange;
        HRESULT hr;

        if (S_OK != (hr = textLayout->GetDrawingEffect(currentPosition, 
                                                       (IUnknown **) &specifier, 
                                                       &queryTextRange)))
        {
            return hr;
        }

        // Create a new CharacterFormatSpecifier or clone the existing one
        if (specifier == nullptr)
        {
            specifier = new CharacterFormatSpecifier();
        }
        else
        {
            specifier = specifier->Clone();
        }

        // Callback to set fields in the new CharacterFormatSpecifier!!!
        setField(specifier);

        // Determine the text range for the new CharacterFormatSpecifier
        UINT32 queryEndPos = queryTextRange.startPosition + queryTextRange.length;
        UINT32 setLength = min(endPosition, queryEndPos) - currentPosition;

        DWRITE_TEXT_RANGE setTextRange;
        setTextRange.startPosition = currentPosition;
        setTextRange.length = setLength;

        // Set it
        if (S_OK != (hr = textLayout->SetDrawingEffect((IUnknown *) specifier, 
                                                       setTextRange)))
        {
            return hr;
        }

        // Bump up the current position
        currentPosition += setLength;
    }
    return S_OK;
}

I haven't given this method a full workout, but I've tested it for several types of overlapping ranges, and it seems to be solid.

The CustomFormattingDemoRenderer constructor and CreateDeviceDependentResources method are similar to the previous projects, but so you can see what these Set methods look like in context, here are all the calls to the static CharacterFormatSpecifier methods:

CustomFormattingDemoRenderer::CreateDeviceDependentResources()
{
    ...

   
    strFind = L"RBG";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();

    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetStrikethrough(m_textLayout.Get(),
                                                   1,
                                                   nullptr,
                                                   textRange)
        );

    // Individual letters after strikethrough
    textRange.length = 1;
    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetForegroundBrush(m_textLayout.Get(),
                                                     redBrush.Get(), 
                                                     textRange)
        );

    textRange.startPosition += 1;
    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetForegroundBrush(m_textLayout.Get(),
                                                     blueBrush.Get(),
                                                     textRange)
        );

    textRange.startPosition += 1;
    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetForegroundBrush(m_textLayout.Get(),
                                                     greenBrush.Get(),
                                                     textRange)
        );

    strFind = L"RGB";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();

    // Individual letters before underline
    textRange.length = 1;
    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetForegroundBrush(m_textLayout.Get(),
                                                     redBrush.Get(),
                                                     textRange)
        );

    textRange.startPosition += 1;
    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetForegroundBrush(m_textLayout.Get(),
                                                     greenBrush.Get(),
                                                     textRange)
        );

    textRange.startPosition += 1;
    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetForegroundBrush(m_textLayout.Get(),
                                                     blueBrush.Get(),
                                                     textRange)
        );

    textRange.startPosition -= 2;
    textRange.length = 3;
    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetUnderline(m_textLayout.Get(),
                                               1,
                                               redBrush.Get(),
                                               textRange)
        );

    strFind = L" red";      // avoid "rendered"
    textRange.startPosition = text.find(strFind.data()) + 1;
    textRange.length = strFind.length() - 1;
    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetForegroundBrush(m_textLayout.Get(),
                                                     redBrush.Get(),
                                                     textRange)
        );

    strFind = L"green";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();
    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetForegroundBrush(m_textLayout.Get(),
                                                     greenBrush.Get(),
                                                     textRange)
        );

    strFind = L"blue";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();
    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetForegroundBrush(m_textLayout.Get(),
                                                     blueBrush.Get(),
                                                     textRange)
        );

    // Set custom underlining and strikethrough
    strFind = L"double underline";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();

    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetUnderline(m_textLayout.Get(),
                                               2, 
                                               redBrush.Get(),
                                               textRange)
        );

    strFind = L"triple underline";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();


    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetUnderline(m_textLayout.Get(),
                                               3,
                                               nullptr,
                                               textRange)
        );

    strFind = L"double strikethrough";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();

    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetStrikethrough(m_textLayout.Get(),
                                                   2,
                                                   nullptr,
                                                   textRange)
        );

    strFind = L"triple strikethrough";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();

    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetStrikethrough(m_textLayout.Get(),
                                                   3,
                                                   nullptr,
                                                   textRange)
        );

    strFind = L"combinations";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();

    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetUnderline(m_textLayout.Get(),
                                               3,
                                               nullptr,
                                               textRange)
        );

    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetStrikethrough(m_textLayout.Get(),
                                                   2,
                                                   redBrush.Get(),
                                                   textRange)
        );

    strFind = L"thereof";
    textRange.startPosition = text.find(strFind.data());
    textRange.length = strFind.length();

    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetUnderline(m_textLayout.Get(),
                                               2,
                                               blueBrush.Get(),
                                               textRange)
        );

    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetStrikethrough(m_textLayout.Get(),
                                                   3,
                                                   nullptr,
                                                   textRange)
        );

    ...
}

Even though some of the calls for underlining and strikethrough do not involve brushes, I found it convenient to consolidate them all in the CreateDeviceDependentResources method.

The Render method calls the Draw method of the IDWriteTextLayout object with an IDWriteTextRenderer implementation named CharacterFormatter:

void CustomFormattingDemoRenderer::Render()
{
    ...

    // Display paragraph of text with custom text renderer
    D2D1_POINT_2F origin = Point2F();
    DrawingContext drawingContext(context, m_blackBrush.Get());

    DX::ThrowIfFailed(
        m_textLayout->Draw(&drawingContext, 
                           m_characterFormatter.Get(), 
                           origin.x, origin.y)
        );

    ...
}

In some respects, the initial CharacterFormatter class in this project is simpler than the UnderlineFormatter in the previous project because it doesn't need to figure out if the clientDrawingContext argument to the methods is an ID2D1Brush object or a custom specifier. It's always either nullptr or an instance of CharacterFormatSpecifier. Here's the DrawGlyphRun method:

HRESULT CharacterFormatter::DrawGlyphRun(void * clientDrawingContext,
                                         FLOAT baselineOriginX,
                                         FLOAT baselineOriginY,
                                         DWRITE_MEASURING_MODE measuringMode,
                                         _In_ const DWRITE_GLYPH_RUN * glyphRun,
                                         _In_ const DWRITE_GLYPH_RUN_DESCRIPTION *
                                             glyphRunDescription,
                                         IUnknown * clientDrawingEffect)
{
    // Break out DrawingContext fields
    DrawingContext * drawingContext =
        static_cast<DrawingContext *>(clientDrawingContext);

    ID2D1RenderTarget * renderTarget = drawingContext->renderTarget;
    ID2D1Brush * foregroundBrush = drawingContext->defaultBrush;

    // Get text brush
    CharacterFormatSpecifier * specifier =
        (CharacterFormatSpecifier *)clientDrawingEffect;

    if (specifier != nullptr)
    {
        ID2D1Brush * brush = specifier->GetForegroundBrush();

        if (brush != nullptr)
        {
            foregroundBrush = brush;
        }
    }

    renderTarget->DrawGlyphRun(Point2F(baselineOriginX, baselineOriginY),
                               glyphRun, foregroundBrush, measuringMode);

    return S_OK;
}

I got a little obsessed and spent entirely too much time attempting trying to figure out to deal with pixel alignment on the underlines and strikethroughs. Early on I decided to use the thickness number to space multiple underlines and strikethroughs, but not making any adjustment caused visually different anti-aliasing, and trying to pixel-snap them all created single-pixel rounding errors.

I decided to pixel-snap the regular underline and strikeout, and space the others based on thickness, which causes them to have different anti-aliasing, but in a symmetric manner and I'm still not happy with the result. Here's the code for underlining that also shows how a brush is selected:

HRESULT CharacterFormatter::DrawUnderline(void * clientDrawingContext,
                                          FLOAT baselineOriginX,
                                          FLOAT baselineOriginY,
                                          _In_ const DWRITE_UNDERLINE *
                                              underline,
                                          IUnknown * clientDrawingEffect)
{
    // Break out DrawingContext fields
    DrawingContext * drawingContext =
        static_cast<DrawingContext *>(clientDrawingContext);

    ID2D1RenderTarget * renderTarget = drawingContext->renderTarget;
    ID2D1Brush * foregroundBrush = drawingContext->defaultBrush;

    // Get underline count and brush
    CharacterFormatSpecifier * specifier =
        (CharacterFormatSpecifier *) clientDrawingEffect;

    int underlineCount = 1;

    if (specifier != nullptr)
    {
        ID2D1Brush * brush;
        specifier->GetUnderline(&underlineCount, &brush);

        if (brush != nullptr)
        {
            foregroundBrush = brush;
        }
        else
        {
            brush = specifier->GetForegroundBrush();

            if (brush != nullptr)
            {
                foregroundBrush = brush;
            }
        }
    }

    if (underlineCount < 0 || underlineCount > 3)
        return E_INVALIDARG;

    if (underlineCount == 1 || underlineCount == 3)
    {
        FillRectangle(renderTarget,
                      foregroundBrush,
                      baselineOriginX,
                      baselineOriginY + underline->offset,
                      underline->width,
                      underline->thickness,
                      0);
    }

    if (underlineCount == 2 || underlineCount == 3)
    {
        FillRectangle(renderTarget,
                      foregroundBrush,
                      baselineOriginX,
                      baselineOriginY + underline->offset,
                      underline->width,
                      underline->thickness,
                      underlineCount - 1);

        FillRectangle(renderTarget,
                      foregroundBrush,
                      baselineOriginX,
                      baselineOriginY + underline->offset,
                      underline->width,
                      underline->thickness,
                      1 - underlineCount);
    }

    return S_OK;
}

...

void CharacterFormatter::FillRectangle(ID2D1RenderTarget * renderTarget,
                                       ID2D1Brush * brush,
                                       float x, float y,
                                       float width, float thickness,
                                       int offset)
{
    // Snap the y coordinate to the nearest pixel
    D2D1_POINT_2F pt = Point2F(0, y);
    pt = m_worldToPixel.TransformPoint(pt);
    pt.y = (float) (int) (pt.y + 0.5f);
    pt = m_pixelToWorld.TransformPoint(pt);
    y = pt.y;

    // Adjust for spacing
    y += offset * thickness;

    // Fill the rectangle
    D2D1_RECT_F rect = RectF(x, y, x + width, y + thickness);
    renderTarget->FillRectangle(&rect, brush);
}

And here's the result:

Underlines and Overlines

Early on, as I examined the DWRITE_UNDERLINE and DWRITE_STRIKETHROUGH structures with thoughts of sharing some logic by casting one to the other, I noticed a slight difference that prevented casting. The DWRITE_UNDERLINE structure defines a member named runHeight that is not in DWRITE_STRIKETHROUGH. This member is defined as "A value that indicates the height of the tallest run where the underline is applied."

Quickly I realized the likely purpose of such a member: to implement overlines.

The SetOverline method I added to CharacterFormatSpecifier allows specifying a custom brush, but I figured that double or triple overlines was just a little two weird. The DrawUnderline method in CharacterFormatter is now complicated somewhat because it has to draw overlines as well as underlines, and I had to offset it by two thickness amounts to prevent it from overlaying some text.

I also enhanced the underlining in another way. Instead of an underline count, I substituted an underline enumeration:

enum class UnderlineType
{
    None = 0,
    Single = 1,
    Double = 2,
    Triple = 3,
    Squiggly
};

In the DrawUnderline method of the CharacterFormatter class, the squiggly underline is handled like this:

HRESULT CharacterFormatter::DrawUnderline(void * clientDrawingContext,
                                          FLOAT baselineOriginX,
                                          FLOAT baselineOriginY,
                                          _In_ const DWRITE_UNDERLINE *
                                              underline,
                                          IUnknown * clientDrawingEffect)
{
    ...

    // Do squiggly underline
    if (underlineType == UnderlineType::Squiggly)
    {
        ComPtr<ID2D1Factory> factory;
        renderTarget->GetFactory(&factory);

        HRESULT hr;
        ComPtr<ID2D1PathGeometry> pathGeometry;

        if (S_OK != (hr = factory->CreatePathGeometry(&pathGeometry)))
            return hr;

        ComPtr<ID2D1GeometrySink> geometrySink;
        if (S_OK != (hr = pathGeometry->Open(&geometrySink)))
            return hr;

        float amplitude = 1 * underline->thickness;
        float period = 5 * underline->thickness;
        float xOffset = baselineOriginX;
        float yOffset = baselineOriginY + underline->offset;

        for (float t = 0; t < underline->width; t++)
        {
            float x = xOffset + t;
            float angle = DirectX::XM_2PI * std::fmod(x, period) / period;
            float y = yOffset + amplitude * DirectX::XMScalarSin(angle);
            D2D1_POINT_2F pt = Point2F(x, y);

            if (t == 0)
                geometrySink->BeginFigure(pt, D2D1_FIGURE_BEGIN_HOLLOW);
            else
                geometrySink->AddLine(pt);
        }

        geometrySink->EndFigure(D2D1_FIGURE_END_OPEN);

        if (S_OK != (hr = geometrySink->Close()))
            return hr;
        
        renderTarget->DrawGeometry(pathGeometry.Get(), 
                                   underlineBrush, 
                                   underline->thickness);
    }
    else
    {
        underlineCount = (int) underlineType;
    }

    ...
}

And here's the overline as well as the squiggly underline:

You could do something similar for squiggly strikethrough, but for strikethrough, something larger and more demonstrative might be appropriate — similar to someone crossing out text with a wide up-an-down pattern. But how large should it be? In the DrawStrikethrough call, there's no convenient way to obtain font metrics to determine how tall this strikethrough can be. It makes more sense to draw such a strikethrough during the DrawGlyphRun method. I ended up not implementing this, but by the end of this blog entry, it should be fairly clear how to do it.

Implementing a Background Brush

If you want to lay down a rectangular (or rounded rectangular) background for an entire paragraph, you don't need anything I've been showing you here. You can call GetMetrics on the IDWriteTextLayout object to get the rectangular dimensions of the paragraph. (This is something these programs have been doing to center the paragraph.) In the Render method you can then construct a rectangle based on the width and height members of the DWRITE_TEXT_METRICS object, and call FillRectangle.

However, if you want to draw a background for specific text runs within the paragraph, here's how to do it:

In the CharacterFormatSpecifier class, create a new formatting item for a background brush similar to the foreground brush.

In the CreateDeviceDependentResources method of the CustomFormattingDemoRenderer class, a background brush on a particular text run can be set like so:

    ComPtr<ID2D1SolidColorBrush> magentaBrush;

    DX::ThrowIfFailed(
        context->CreateSolidColorBrush(ColorF(ColorF::Magenta), 
                                              &magentaBrush)
        );

    strFind = L"IDWriteTextFormat and IDWriteTextLayout objects";
    textRange.startPosition = m_text.find(strFind.data());
    textRange.length = strFind.length();

    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetBackgroundBrush(m_textLayout.Get(), 
                                                     magentaBrush.Get(), 
                                                     textRange)
        );

In the DrawGlyphRun method of CharacterFormatter, if a call to the specifier's GetBackgroundBrush method does not return nullptr, construct a rectangle with the width and height of the text associated with the glyph run.

The width and height of the text? Where does that come from?

The two dimensions come from two different sources:

The width of the text can be calculated directly from the DWRITE_GLYPH_RUN object passed to DrawGlyphRun. Simply total up the members of the glyphAdvances array for the glyphs in the glyph run.

The height of the text requires a call to the GetMetrics method of the IDWriteFontFace object available from the fontFace member of the DWRITE_GLYPH_RUN object. This call returns an instance of the DWRITE_FONT_METRICS structure. The ascent and descent members indicate heights above and below the baseline. However, these members are in font design units. You must divide the values by the designUnitsPerEm member to get fractional heights, and then multiply by the fontEmSize member of the DWRITE_GLYPH_RUN object. Here's the complete calculation in the new DrawGlyphRun method:

HRESULT CharacterFormatter::DrawGlyphRun(void * clientDrawingContext,
                                         FLOAT baselineOriginX,
                                         FLOAT baselineOriginY,
                                         DWRITE_MEASURING_MODE measuringMode,
                                         _In_ const DWRITE_GLYPH_RUN * glyphRun,
                                         _In_ const DWRITE_GLYPH_RUN_DESCRIPTION *
                                             glyphRunDescription,
                                         IUnknown * clientDrawingEffect)
{
    // Break out DrawingContext fields
    DrawingContext * drawingContext =
        static_cast<DrawingContext *>(clientDrawingContext);

    ID2D1RenderTarget * renderTarget = drawingContext->renderTarget;
    ID2D1Brush * foregroundBrush = drawingContext->defaultBrush;
    ID2D1Brush * backgroundBrush = nullptr;

    // Get foreground and background brushes
    CharacterFormatSpecifier * specifier =
        (CharacterFormatSpecifier *)clientDrawingEffect;

    if (specifier != nullptr)
    {
        backgroundBrush = specifier->GetBackgroundBrush();

        ID2D1Brush * brush = specifier->GetForegroundBrush();

        if (brush != nullptr)
        {
            foregroundBrush = brush;
        }
    }

    if (backgroundBrush != nullptr)
    {
        // Get width of text
        float totalWidth = 0;

        for (UINT32 index = 0; index < glyphRun->glyphCount; index++)
        {
            totalWidth += glyphRun->glyphAdvances[index];
        }

        // Get height of text
        DWRITE_FONT_METRICS fontMetrics;
        glyphRun->fontFace->GetMetrics(&fontMetrics);
        float adjust = glyphRun->fontEmSize / fontMetrics.designUnitsPerEm;
        float ascent = adjust * fontMetrics.ascent;
        float descent = adjust * fontMetrics.descent;
        D2D1_RECT_F rect = RectF(baselineOriginX,
                                 baselineOriginY - ascent,
                                 baselineOriginX + totalWidth,
                                 baselineOriginY + descent);

        // Fill Rectangle
        renderTarget->FillRectangle(rect, backgroundBrush);
    }

    renderTarget->DrawGlyphRun(Point2F(baselineOriginX, baselineOriginY),
                               glyphRun, foregroundBrush, measuringMode);

    return S_OK;
}

And here's the result:

As you can see, there's a little gap between the backgrounds of the two successive lines. You may like that gap or you may not. You can close it up by adding the lineGap member of the DWRITE_FONT_METRICS object to the descent member:

float descent = adjust * (fontMetrics.descent + fontMetrics.lineGap);

And now it's gone:

Or rather, it's almost gone. I suspect there's still a little gap there due to pixel alignment. Perhaps you'd prefer to simply increment the descent value to close it up entirely.

This background brush is not rendered with one call to FillRectangle. Keep in mind that each change in formatting gets a new DrawGlyphRun call, and each new line in the paragraph gets a new DrawGlyphRun call, and the white space at the end of a line gets its own DrawGlyphRun call. This particular background is rendered in five calls to DrawGlyphRun, the first for the italicized "IDWriteTextFormat" string, then the space at the end of the line, the word "and " (with a space at the end), then the italicized "IDWriteTextLayout" and finally " objects" (with a space at the beginning).

Suppose there are actually 10 spaces after "IDWriteTextFormat" in the original text rather than just one. Those 10 spaces do not change how the paragraph is formatted into lines, but the spaces are retained in the calls to DrawGlyphRun, which means that they will get the background brush:

This background now extends beyond the formatted width of the paragraph! Such weirdness strongly suggests that the background brush should not be applied to the DrawGlyphRun call for the trailing white space at the end of each line. But how can the DrawGlyphRun method determine when it's being called to display that white space? That information is available in the DWRITE_LINE_METRICS structure, but an array of instances of that structure is only available from the GetLineMetrics call of the IDWriteTextLayout object, and that object is not available in the DrawGlyphRun method. (However, keep in mind that the DrawGlyphRun call results from a call to the Draw method of the IDWriteTextLayout object, so we're not too far away.)

Another problem: Suppose you set a larger font size for some text within the run that you're coloring with a background brush:

    strFind = L" and ";
    textRange.startPosition = m_text.find(strFind.data());
    textRange.length = strFind.length();

    DX::ThrowIfFailed(
        m_textLayout->SetFontSize(48.0f, textRange)
        );

That run will have a different fontEmSize field of the DWRITE_GLYPH_RUN object. (Something similar will happen if some text has a different font family except that the fontFace member of the DWRITE_GLYPH_RUN object will be different, and the GetMetrics method will return a different collection of metrics.) Here's the result for this example:

Because I've applied the larger font size to the string " and " with the two spaces surrounding the word, the larger size is also applied to the trailing white space at the end of the line, which makes the coloring of that white space appear all the more anomalous.

Another issue: Suppose you use the SetLineSpacing method to set a uniform line spacing:

    DX::ThrowIfFailed(
        m_textLayout->SetLineSpacing(DWRITE_LINE_SPACING_METHOD_UNIFORM, 36, 29)
        );

But the background brush is still based on the ascent and descent of the font and no longer encompasses that entire line spacing value:

In such a case you might want to base the background brush on the actual heights of the lines. Again, that information is available form the GetLineMetrics method of the IDWriteTextLayout object but that object is not available in the DrawGlyphRun method.

Here's another problem that causes a little visual hiccup in the text display — just a few pixels are involved — but with extremely serious implications. Suppose "IDWriteTextFormat" and "IDWriteTextLayout" are actually spelled "IDWriteTextFormaf" and "IDWriteTextLayouf" with a lower-case "f" at the end. Here's what happens:

Do you see it? The top part of the two "f" characters are both shaved off. This is a visual demonstration of the difference between a font character's black box and its layout box. The width of the black box encompasses the entire character glyph. For most characters, the width of the layout box is generally a little wider than the black box. The layout box is used to space successive characters of text, and being a little wider than the black box allows for slivers of space between successive characters. The width of the layout box is what shows up in the glyphAdvances array that the DrawGlyphRun method uses to calculate the width of the background brush.

However, for some characters — and the italic "f" is the archetypal example — the layout box is narrower than the black box. In other words, by design, the bottom and top of the italic "f" are supposed to extend beyond the width allowed for the character in layout.

That's why the lower-case italic "f" rendered in one DrawGlyphRun call is partially obscured by a background brush rendered in the next DrawGlyphRun call.

Now what?

One obvious way to solve this problem is with two separate series of calls to DrawGlyphRun. During the first series of calls, the method is responsible for rendering all the backgrounds, and during the second series of calls, all the glyphs. This could be done with two calls to the Draw method of the IDWriteTextLayout object, perhaps passing a flag in another field of the DrawingContext object so the DrawGlyphRun method knows what to do.

But we've also established that it would be very convenient to call GetLineMetrics on the IDWriteTextLayout object prior to rendering the background. Should the code calling the Draw method of the IDWriteTextLayout object perform this job as well, and pass the information through the DrawingContext?

Or would it be cleaner for the IDWriteTextRenderer implementation to define its own Draw method, and for that method to do the work?

I like that approach! And part of the reason is because I can get rid of the DrawingContext structure. Here's the current code in the Render method:

    D2D1_POINT_2F origin = Point2F();
    DrawingContext drawingContext(context, m_blackBrush.Get());

    DX::ThrowIfFailed(
        m_textLayout->Draw(&drawingContext, 
                           m_characterFormatter.Get(), 
                           origin.x, origin.y)
        );

With a Draw method defined in the CharacterFormatter class of this project, here's how the text is rendered instead:

    D2D1_POINT_2F origin = Point2F();

    DX::ThrowIfFailed(
        m_characterFormatter->Draw(context, 
                                   m_textLayout.Get(), 
                                   origin, 
                                   m_blackBrush.Get())
        );

In the CharacterFormatter header file I've declared the Draw method and a new enumeration and private data fields:

    Microsoft::WRL::ComPtr<ID2D1RenderTarget> m_renderTarget;
    Microsoft::WRL::ComPtr<ID2D1Brush>        m_defaultBrush;

    enum class RenderPass
    {
        Initial,
        Main,
        Final
    };

    RenderPass m_renderPass;

Although I've only established the need for two passes, I've allowed for three. Here's how a simple Draw method can be defined:

HRESULT CharacterFormatter::Draw(ID2D1RenderTarget * renderTarget,
                                 IDWriteTextLayout * textLayout,
                                 D2D1_POINT_2F origin,
                                 ID2D1Brush * defaultBrush)
{
    m_renderTarget = renderTarget;
    m_defaultBrush = defaultBrush;

    for (m_renderPass = RenderPass::Initial;
         m_renderPass <= RenderPass::Final;
         m_renderPass = (RenderPass)((int)m_renderPass + 1))
    {
        HRESULT hr = textLayout->Draw(nullptr, this, origin.x, origin.y);

        if (hr != S_OK)
        {
            return hr;
        }
    }
    return S_OK;
}

In the beginning of the DrawUnderline, DrawStrikethrough, and DrawInlineObject methods, I've inserted a simple check:

    if (m_renderPass != RenderPass::Main)
    {
        return S_OK;
    }

Changes were also necessary to use the m_renderTarget and m_defaultBrush private data members. By completely eliminating the DrawingContext structure I've also helped prevent the CharacterFormatter class being used by passing an instance to the Draw method of the IDWriteTextLayout object. That will no longer work. The Draw method of the CharacterFormatter instance must be called instead.

The DrawGlyphRun method required the most upheaval to accomodate the initial and main rendering passes, but not very much:

HRESULT CharacterFormatter::DrawGlyphRun(void * clientDrawingContext,
                                         FLOAT baselineOriginX,
                                         FLOAT baselineOriginY,
                                         DWRITE_MEASURING_MODE measuringMode,
                                         _In_ const DWRITE_GLYPH_RUN * glyphRun,
                                         _In_ const DWRITE_GLYPH_RUN_DESCRIPTION *
                                             glyphRunDescription,
                                         IUnknown * clientDrawingEffect)
{
    ID2D1Brush * foregroundBrush = m_defaultBrush.Get();
    ID2D1Brush * backgroundBrush = nullptr;

    // Get foreground and background brushes
    CharacterFormatSpecifier * specifier =
        (CharacterFormatSpecifier *)clientDrawingEffect;

    if (specifier != nullptr)
    {
        backgroundBrush = specifier->GetBackgroundBrush();

        ID2D1Brush * brush = specifier->GetForegroundBrush();

        if (brush != nullptr)
        {
            foregroundBrush = brush;
        }
    }

    if (m_renderPass == RenderPass::Initial && backgroundBrush != nullptr)
    {
        // Get width of text
        float totalWidth = 0;

        for (UINT32 index = 0; index < glyphRun->glyphCount; index++)
        {
            totalWidth += glyphRun->glyphAdvances[index];
        }

        // Get height of text
        DWRITE_FONT_METRICS fontMetrics;
        glyphRun->fontFace->GetMetrics(&fontMetrics);
        float adjust = glyphRun->fontEmSize / fontMetrics.designUnitsPerEm;
        float ascent = adjust * fontMetrics.ascent;
        float descent = adjust * (fontMetrics.descent + fontMetrics.lineGap);
        D2D1_RECT_F rect = RectF(baselineOriginX,
                                 baselineOriginY - ascent,
                                 baselineOriginX + totalWidth,
                                 baselineOriginY + descent);

        // Fill Rectangle
        m_renderTarget->FillRectangle(rect, backgroundBrush);
    }

    else if (m_renderPass == RenderPass::Main)
    {
        m_renderTarget->DrawGlyphRun(Point2F(baselineOriginX, baselineOriginY),
                                     glyphRun, foregroundBrush, measuringMode);
    }
    return S_OK;
}

And we can now see the entire "f" characters:

Let's add a new argument to the SetBackgroundBrush method in CharacterFormatSpecifier based on the following enumeration:

enum class BackgroundMode
{
    TextHeight,
    TextHeightWithLineGap,
    LineHeight
};

Now let's add some code to the Draw method of CharacterFormatter to obtain the line metrics of the IDWriteTextLayout object. Three more private fields must be added to the header file:

    std::vector<DWRITE_LINE_METRICS> m_lineMetrics;
    int                              m_lineIndex;
    int                              m_charIndex;

Here's the new Draw method. Notice that the m_lineIndex and m_charIndex fields are initialized to zero prior to each call of the Draw method of textLayout:

HRESULT CharacterFormatter::Draw(ID2D1RenderTarget * renderTarget,
                                 IDWriteTextLayout * textLayout,
                                 D2D1_POINT_2F origin,
                                 ID2D1Brush * defaultBrush)
{
    // Get the line metrics of the IDWriteTextLayout
    HRESULT hr;
    UINT32 actualLineCount;

    if (E_NOT_SUFFICIENT_BUFFER != 
        (hr = textLayout->GetLineMetrics(nullptr, 
                                         0, 
                                         &actualLineCount)))
    {
        return hr;
    }

    m_lineMetrics = std::vector<DWRITE_LINE_METRICS>(actualLineCount);

    if (S_OK != (hr = textLayout->GetLineMetrics(m_lineMetrics.data(),
                                                 m_lineMetrics.size(),
                                                 &actualLineCount)))
    {
        return hr;
    }

    m_renderTarget = renderTarget;
    m_defaultBrush = defaultBrush;

    for (m_renderPass = RenderPass::Initial;
         m_renderPass <= RenderPass::Final;
         m_renderPass = (RenderPass)((int)m_renderPass + 1))
    {
        m_lineIndex = 0;
        m_charIndex = 0;
        HRESULT hr = textLayout->Draw(nullptr, this, origin.x, origin.y);

        if (hr != S_OK)
        {
            return hr;
        }
    }
    return S_OK;
}

Here's the new DrawGlyphRun method. It keeps the m_charIndex and m_lineIndex variables updated in synchronization with the character lengths of the glyph runs coming through, and it uses the BackgroundMode setting to determine the background brush rectangle height:

HRESULT CharacterFormatter::DrawGlyphRun(void * clientDrawingContext,
                                         FLOAT baselineOriginX,
                                         FLOAT baselineOriginY,
                                         DWRITE_MEASURING_MODE measuringMode,
                                         _In_ const DWRITE_GLYPH_RUN * glyphRun,
                                         _In_ const DWRITE_GLYPH_RUN_DESCRIPTION *
                                             glyphRunDescription,
                                         IUnknown * clientDrawingEffect)
{
    ID2D1Brush * foregroundBrush = m_defaultBrush.Get();

    BackgroundMode backgroundMode = BackgroundMode::TextHeight;
    ID2D1Brush * backgroundBrush = nullptr;

    // Get foreground and background brushes
    CharacterFormatSpecifier * specifier =
        (CharacterFormatSpecifier *)clientDrawingEffect;

    if (specifier != nullptr)
    {
        specifier->GetBackgroundBrush(&backgroundMode, &backgroundBrush);

        ID2D1Brush * brush = specifier->GetForegroundBrush();

        if (brush != nullptr)
        {
            foregroundBrush = brush;
        }
    }

    if (m_renderPass == RenderPass::Initial)
    {
        bool renderBackground = backgroundBrush != nullptr;

        // Check if we're at the trailing white space
        DWRITE_LINE_METRICS lineMetrics = m_lineMetrics.at(m_lineIndex);
        UINT32 length = lineMetrics.length;

        if (length - m_charIndex == lineMetrics.trailingWhitespaceLength)
        {
            renderBackground = false;
        }

        if (renderBackground)
        {
            // Get width of text
            float totalWidth = 0;

            for (UINT32 index = 0; index < glyphRun->glyphCount; index++)
            {
                totalWidth += glyphRun->glyphAdvances[index];
            }

            // Get height of text
            float ascent;
            float descent;

            if (backgroundMode == BackgroundMode::LineHeight)
            {
                ascent = lineMetrics.baseline;
                descent = lineMetrics.height - ascent;
            }
            else
            {
                DWRITE_FONT_METRICS fontMetrics;
                glyphRun->fontFace->GetMetrics(&fontMetrics);
                float adjust = glyphRun->fontEmSize / fontMetrics.designUnitsPerEm;
                ascent = adjust * fontMetrics.ascent;
                descent = adjust * fontMetrics.descent;
                
                if (backgroundMode == BackgroundMode::TextHeightWithLineGap)
                {
                    descent += adjust * fontMetrics.lineGap;
                }
            }

            // Fill Rectangle
            D2D1_RECT_F rect = RectF(baselineOriginX,
                                     baselineOriginY - ascent,
                                     baselineOriginX + totalWidth,
                                     baselineOriginY + descent);

            m_renderTarget->FillRectangle(rect, backgroundBrush);
        }

        // Now the indices increment for this glyph run
        m_charIndex += glyphRunDescription->stringLength;

        if (m_charIndex == lineMetrics.length)
        {
            m_lineIndex++;
            m_charIndex = 0;
        }
    }

    else if (m_renderPass == RenderPass::Main)
    {
        m_renderTarget->DrawGlyphRun(Point2F(baselineOriginX, baselineOriginY),
                                     glyphRun, foregroundBrush, measuringMode);
    }
    return S_OK;
}

As you can see, it suppresses the background for trailing whitespace:

For that screen shot, the background mode is set to BackgroundMode::TextHeight. If a run has a larger font size, the background is based on the text height of the particular run:

I've restricted the large font size formatting to just the word "and" rather than including the two surrounding spaces so it doesn't affect the line spacing of the previous line.

Change the background mode to BackgroundMode::LineHeight to use the line height from the DWRITE_LINE_METRICS structure to form the background rectangle:

This mode also works well when you've set the line spacing to DWRITE_LINE_SPACING_UNIFORM:

Highlighting the Text

I've defined the RenderPass enumeration with three values: Initial, Main, and Final. In the DrawGlyphRun method the Initial pass is for drawing the background brush and the Main pass is for drawing the glyph runs on top of the background.

I anticipated that I would want another pass to draw something on top of the glyph runs, and I can think of two possibilities right off: a kind of up-and-down strikethrough (which would need to be based on the height of the text and hence couldn't be drawn during the DrawStrikethrough method) and a highlighter. The highlighter brush differs from the background brush because it's semi-transparent and goes on top of the text rather than behind it.

I've restricted myself to highlighting for now. I added SetHighlight and GetHighlight methods to CharacterFormatSpecifier, and the sole variable is a brush. Presumably the user will know that the brush should have an alpha channel less than 1.

Here's how the highlight formatting is set in the CustomFormattingDemoRenderer class:

    ComPtr<ID2D1SolidColorBrush> highlightBrush;

    DX::ThrowIfFailed(
        context->CreateSolidColorBrush(ColorF(1.0f, 1.0f, 0, 0.5f),
        &highlightBrush)
        );

    strFind = L"the SetDrawingEffect method";
    textRange.startPosition = m_text.find(strFind.data());
    textRange.length = strFind.length();

    DX::ThrowIfFailed(
        CharacterFormatSpecifier::SetHighlight(m_textLayout.Get(),
                                               highlightBrush.Get(),
                                               textRange)
        );

And I revamped the DrawGlyphRun method once more. Because the check for trailing white space now needs to be performed for both the initial and final passes, it's done regardless of the pass. I've switched to using a switch and case statement for the three passes, and the initial and final passes share some code implemented in a GetRectangle method:

HRESULT CharacterFormatter::DrawGlyphRun(void * clientDrawingContext,
                                         FLOAT baselineOriginX,
                                         FLOAT baselineOriginY,
                                         DWRITE_MEASURING_MODE measuringMode,
                                         _In_ const DWRITE_GLYPH_RUN * glyphRun,
                                         _In_ const DWRITE_GLYPH_RUN_DESCRIPTION *
                                             glyphRunDescription,
                                         IUnknown * clientDrawingEffect)
{
    ID2D1Brush * foregroundBrush = m_defaultBrush.Get();

    BackgroundMode backgroundMode = BackgroundMode::TextHeight;
    ID2D1Brush * backgroundBrush = nullptr;
    ID2D1Brush * highlightBrush = nullptr;

    // Get foreground, background, highlight brushes
    CharacterFormatSpecifier * specifier =
        (CharacterFormatSpecifier *)clientDrawingEffect;

    if (specifier != nullptr)
    {
        specifier->GetBackgroundBrush(&backgroundMode, &backgroundBrush);

        ID2D1Brush * brush = specifier->GetForegroundBrush();

        if (brush != nullptr)
        {
            foregroundBrush = brush;
        }

        highlightBrush = specifier->GetHighlight();
    }

    // Set variable indicating trailing white space
    bool isTrailingWhiteSpace = false;

    DWRITE_LINE_METRICS lineMetrics = m_lineMetrics.at(m_lineIndex);
    UINT32 length = lineMetrics.length;

    if (length - m_charIndex == lineMetrics.trailingWhitespaceLength)
    {
        isTrailingWhiteSpace = true;
    }

    switch (m_renderPass)
    {
        case RenderPass::Initial:
        {
            if (backgroundBrush != nullptr && !isTrailingWhiteSpace)
            {
                D2D1_RECT_F rect = GetRectangle(glyphRun, 
                                                &lineMetrics,
                                                baselineOriginX, 
                                                baselineOriginY,
                                                backgroundMode);

                m_renderTarget->FillRectangle(rect, backgroundBrush);
            }
            break;
        }

        case RenderPass::Main:
        {
            m_renderTarget->DrawGlyphRun(Point2F(baselineOriginX, 
                                                 baselineOriginY),
                                         glyphRun, 
                                         foregroundBrush, 
                                         measuringMode);
            break;
        }

        case RenderPass::Final:
        {
            if (highlightBrush != nullptr && !isTrailingWhiteSpace)
            {
                D2D1_RECT_F rect = GetRectangle(glyphRun,
                                                &lineMetrics,
                                                baselineOriginX,
                                                baselineOriginY,
                                                BackgroundMode::TextHeight);

                m_renderTarget->FillRectangle(rect, highlightBrush);
            }
            break;
        }
    }

    // Increment the indices for this glyph run
    m_charIndex += glyphRunDescription->stringLength;

    if (m_charIndex == lineMetrics.length)
    {
        m_lineIndex++;
        m_charIndex = 0;
    }

    return S_OK;
}

D2D1_RECT_F CharacterFormatter::GetRectangle(const DWRITE_GLYPH_RUN * glyphRun,
                                             const DWRITE_LINE_METRICS * lineMetrics,
                                             FLOAT baselineOriginX,
                                             FLOAT baselineOriginY,
                                             BackgroundMode backgroundMode)
{
    // Get width of text
    float totalWidth = 0;

    for (UINT32 index = 0; index < glyphRun->glyphCount; index++)
    {
        totalWidth += glyphRun->glyphAdvances[index];
    }

    // Get height of text
    float ascent;
    float descent;

    if (backgroundMode == BackgroundMode::LineHeight)
    {
        ascent = lineMetrics->baseline;
        descent = lineMetrics->height - ascent;
    }
    else
    {
        DWRITE_FONT_METRICS fontMetrics;
        glyphRun->fontFace->GetMetrics(&fontMetrics);
        float adjust = glyphRun->fontEmSize / fontMetrics.designUnitsPerEm;
        ascent = adjust * fontMetrics.ascent;
        descent = adjust * fontMetrics.descent;

        if (backgroundMode == BackgroundMode::TextHeightWithLineGap)
        {
            descent += adjust * fontMetrics.lineGap;
        }
    }

    // Create rectangle
    return RectF(baselineOriginX,
                 baselineOriginY - ascent,
                 baselineOriginX + totalWidth,
                 baselineOriginY + descent);
}

And here it is:

Notice that the highlight is qualitatively different from the background because the highlight dims out the text itself. The text is now being viewed through the semi-transparent highlight brush.

It occurred to me to add some randomness to the beginning and end of the highlight stroke to better mimic a real highlighter, but perhaps that feature is best reserved for interactive highlighting with a pen or finger.

And perhaps it should wait for another short blog entry.


Comments:

Holy cow. All this IDWTR love, and no comments? Perhaps if you'd titled it "Everything (and I Mean Everything!) You Ever Wanted to Know About IDWriteTextRenderer but Were Afraid to Ask" it may have had more sex-appeal?

Literally everything I ever wanted to know (on this subject, at any rate). Thanks.

— Philip the Duck, Tue, 25 Feb 2014 17:58:58 -0500


Recent Entries
< PreviousBrowse the ArchivesNext >
Subscribe to the RSS Feed

(c) Copyright Charles Petzold
www.charlespetzold.com