From 6e04fd983f0684facb881d4748f3dd2d69fb3c58 Mon Sep 17 00:00:00 2001 From: Birunthan Mohanathas Date: Sat, 10 Aug 2013 13:54:53 +0300 Subject: [PATCH] Gfx: Make AccurateText=0 more consistent with GDI+ for D2D The height is now based on a formula that should be exactly equal to that provided by GDI+. The padding should now be equal on the left and right sides now. There may be some discrepancies in overall width, but it should always be reasonably to close to GDI+. In addition, this makes D2D behavior match GDI+ when a newline character is the last character of the text. --- Common/Gfx/CanvasD2D.cpp | 72 +++++------------- Common/Gfx/TextFormatD2D.cpp | 118 +++++++++++++++++++++++++++++- Common/Gfx/TextFormatD2D.h | 9 ++- Common/Gfx/Util/DWriteHelpers.cpp | 25 ------- Common/Gfx/Util/DWriteHelpers.h | 5 -- 5 files changed, 140 insertions(+), 89 deletions(-) diff --git a/Common/Gfx/CanvasD2D.cpp b/Common/Gfx/CanvasD2D.cpp index c8ca8057..3b0276bf 100644 --- a/Common/Gfx/CanvasD2D.cpp +++ b/Common/Gfx/CanvasD2D.cpp @@ -313,13 +313,13 @@ void CanvasD2D::DrawTextW(const WCHAR* str, UINT strLen, const TextFormat& forma { TextFormatD2D& formatD2D = (TextFormatD2D&)format; formatD2D.CreateLayout( - str, strLen, rect.Width, rect.Height); + str, strLen, rect.Width, rect.Height, !m_AccurateText && m_TextAntiAliasing); - const float xOffset = formatD2D.m_TextFormat->GetFontSize() / 6.0f; const float xPos = [&]() { if (!m_AccurateText) { + const float xOffset = formatD2D.m_TextFormat->GetFontSize() / 6.0f; switch (formatD2D.GetHorizontalAlignment()) { case HorizontalAlignment::Left: return rect.X + xOffset; @@ -330,22 +330,11 @@ void CanvasD2D::DrawTextW(const WCHAR* str, UINT strLen, const TextFormat& forma return rect.X; } (); - if (!m_AccurateText && m_TextAntiAliasing) - { - const float emOffset = xOffset / 25.0f; - - DWRITE_TEXT_RANGE range = {0, strLen}; - Microsoft::WRL::ComPtr textLayout; - formatD2D.m_TextLayout.As(&textLayout); - - textLayout->SetCharacterSpacing(emOffset, emOffset, 0.0f, range); - } - // TODO: Check for transformation. m_Target->PushAxisAlignedClip(ToRectF(rect), D2D1_ANTIALIAS_MODE_ALIASED); m_Target->DrawTextLayout( - D2D1::Point2F(xPos, rect.Y - 1.0f), + D2D1::Point2F(xPos, rect.Y - formatD2D.m_LineGap), formatD2D.m_TextLayout.Get(), solidBrush.Get()); @@ -355,52 +344,27 @@ void CanvasD2D::DrawTextW(const WCHAR* str, UINT strLen, const TextFormat& forma bool CanvasD2D::MeasureTextW(const WCHAR* str, UINT strLen, const TextFormat& format, Gdiplus::RectF& rect) { - Microsoft::WRL::ComPtr textLayout; - HRESULT hr = c_DWFactory->CreateTextLayout( - str, - strLen, - ((TextFormatD2D&)format).m_TextFormat.Get(), - 10000, - 10000, - textLayout.GetAddressOf()); - if (SUCCEEDED(hr)) - { - const DWRITE_TEXT_METRICS metrics = - Util::GetAdjustedDWriteTextLayoutMetrics(textLayout.Get(), !m_AccurateText); - rect.Width = metrics.width; - rect.Height = metrics.height; - return true; - } - - return false; + TextFormatD2D& formatD2D = (TextFormatD2D&)format; + const DWRITE_TEXT_METRICS metrics = formatD2D.GetMetrics(str, strLen, !m_AccurateText); + rect.Width = metrics.width; + rect.Height = metrics.height; + return true; } bool CanvasD2D::MeasureTextLinesW(const WCHAR* str, UINT strLen, const TextFormat& format, Gdiplus::RectF& rect, UINT& lines) { - ((TextFormatD2D&)format).m_TextFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_WRAP); + TextFormatD2D& formatD2D = (TextFormatD2D&)format; + formatD2D.m_TextFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_WRAP); - Microsoft::WRL::ComPtr textLayout; - HRESULT hr = c_DWFactory->CreateTextLayout( - str, - strLen, - ((TextFormatD2D&)format).m_TextFormat.Get(), - rect.Width, - 10000, - textLayout.GetAddressOf()); - if (SUCCEEDED(hr)) - { - const DWRITE_TEXT_METRICS metrics = - Util::GetAdjustedDWriteTextLayoutMetrics(textLayout.Get(), !m_AccurateText); - rect.Width = metrics.width; - lines = metrics.lineCount; + const DWRITE_TEXT_METRICS metrics = formatD2D.GetMetrics(str, strLen, !m_AccurateText, rect.Width); + rect.Width = metrics.width; + rect.Height = metrics.height; + lines = metrics.lineCount; - // GDI+ draws multi-line text even though the last line may be clipped slightly at the bottom. - // This is a workaround to emulate that behaviour. - rect.Height = metrics.height + 1.0f; - return true; - } - - return false; + // GDI+ draws multi-line text even though the last line may be clipped slightly at the bottom. + // This is a workaround to emulate that behaviour. + rect.Height += 1.0f; + return true; } void CanvasD2D::DrawBitmap(Gdiplus::Bitmap* bitmap, const Gdiplus::Rect& dstRect, const Gdiplus::Rect& srcRect) diff --git a/Common/Gfx/TextFormatD2D.cpp b/Common/Gfx/TextFormatD2D.cpp index 1f367a70..015138a5 100644 --- a/Common/Gfx/TextFormatD2D.cpp +++ b/Common/Gfx/TextFormatD2D.cpp @@ -22,7 +22,9 @@ namespace Gfx { -TextFormatD2D::TextFormatD2D() +TextFormatD2D::TextFormatD2D() : + m_ExtraHeight(), + m_LineGap() { } @@ -37,7 +39,8 @@ void TextFormatD2D::Dispose() m_InlineEllipsis.Reset(); } -void TextFormatD2D::CreateLayout(const WCHAR* str, UINT strLen, float maxW, float maxH) +void TextFormatD2D::CreateLayout( + const WCHAR* str, UINT strLen, float maxW, float maxH, bool gdiEmulation) { bool strChanged = false; if (strLen != m_LastString.length() || @@ -63,6 +66,17 @@ void TextFormatD2D::CreateLayout(const WCHAR* str, UINT strLen, float maxW, floa { CanvasD2D::c_DWFactory->CreateTextLayout( str, strLen, m_TextFormat.Get(), maxW, maxH, m_TextLayout.ReleaseAndGetAddressOf()); + + if (gdiEmulation && m_TextLayout) + { + Microsoft::WRL::ComPtr textLayout1; + m_TextLayout.As(&textLayout1); + + const float xOffset = m_TextFormat->GetFontSize() / 6.0f; + const float emOffset = xOffset / 24.0f; + const DWRITE_TEXT_RANGE range = {0, strLen}; + textLayout1->SetCharacterSpacing(emOffset, emOffset, 0.0f, range); + } } } @@ -80,6 +94,7 @@ void TextFormatD2D::SetProperties( DWRITE_FONT_STYLE dwriteFontStyle = italic ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL; DWRITE_FONT_STRETCH dwriteFontStretch = DWRITE_FONT_STRETCH_NORMAL; + const float dwriteFontSize = size * (4.0f / 3.0f); // |fontFamily| uses the GDI/GDI+ font naming convention so try to create DirectWrite font // using the GDI family name and then create a text format using the DirectWrite family name @@ -95,7 +110,7 @@ void TextFormatD2D::SetProperties( dwriteFontWeight, dwriteFontStyle, dwriteFontStretch, - size * (4.0f / 3.0f), + dwriteFontSize, L"", &m_TextFormat); } @@ -135,7 +150,7 @@ void TextFormatD2D::SetProperties( dwriteFontWeight, dwriteFontStyle, dwriteFontStretch, - size * (4.0f / 3.0f), + dwriteFontSize, L"", &m_TextFormat); } @@ -144,6 +159,36 @@ void TextFormatD2D::SetProperties( { SetHorizontalAlignment(GetHorizontalAlignment()); SetVerticalAlignment(GetVerticalAlignment()); + + // TODO: Clean this up and check for errors. + Microsoft::WRL::ComPtr collection; + m_TextFormat->GetFontCollection(collection.GetAddressOf()); + + UINT32 familyNameIndex; + BOOL exists; + collection->FindFamilyName(dwriteFamilyName, &familyNameIndex, &exists); + + Microsoft::WRL::ComPtr fontFamily; + collection->GetFontFamily(familyNameIndex, fontFamily.GetAddressOf()); + + Microsoft::WRL::ComPtr font; + fontFamily->GetFirstMatchingFont( + m_TextFormat->GetFontWeight(), + m_TextFormat->GetFontStretch(), + m_TextFormat->GetFontStyle(), + font.GetAddressOf()); + + DWRITE_FONT_METRICS fmetrics; + font->GetMetrics(&fmetrics); + + // GDI+ compatibility: GDI+ adds extra padding below the string when |m_AccurateText| is + // |false|. The bottom padding seems to be based on the font metrics so we can calculate it + // once and keep using it regardless of the actual string. In some cases, GDI+ also adds + // the line gap to the overall height so we will store it as well. + const float pixelsPerDesignUnit = dwriteFontSize / (float)fmetrics.designUnitsPerEm; + m_ExtraHeight = + (((float)fmetrics.designUnitsPerEm / 8.0f) - fmetrics.lineGap) * pixelsPerDesignUnit; + m_LineGap = fmetrics.lineGap * pixelsPerDesignUnit; } else { @@ -151,6 +196,71 @@ void TextFormatD2D::SetProperties( } } +DWRITE_TEXT_METRICS TextFormatD2D::GetMetrics( + const WCHAR* str, UINT strLen, bool gdiEmulation, float maxWidth) +{ + // GDI+ compatibility: If the last character is a newline, GDI+ measurements seem to ignore it. + bool strippedLastNewLine = false; + if (str[strLen - 1] == L'\n') + { + strippedLastNewLine = true; + --strLen; + + if (str[strLen - 1] == L'\r') + { + --strLen; + } + } + + DWRITE_TEXT_METRICS metrics = {0}; + Microsoft::WRL::ComPtr textLayout; + HRESULT hr = CanvasD2D::c_DWFactory->CreateTextLayout( + str, + strLen, + m_TextFormat.Get(), + maxWidth, + 10000, + textLayout.GetAddressOf()); + if (SUCCEEDED(hr)) + { + const float xOffset = m_TextFormat->GetFontSize() / 6.0f; + if (gdiEmulation) + { + Microsoft::WRL::ComPtr textLayout1; + textLayout.As(&textLayout1); + + const float emOffset = xOffset / 24.0f; + const DWRITE_TEXT_RANGE range = {0, strLen}; + textLayout1->SetCharacterSpacing(emOffset, emOffset, 0.0f, range); + } + + textLayout->GetMetrics(&metrics); + if (metrics.width > 0.0f) + { + if (gdiEmulation) + { + metrics.width += xOffset * 2; + metrics.height += m_ExtraHeight; + + // GDI+ compatibility: If the string contains a newline (even if it is the + // stripped last character), GDI+ adds the line gap to the overall height. + if (strippedLastNewLine || wmemchr(str, L'\n', strLen) != nullptr) + { + metrics.height += m_LineGap; + } + } + } + else + { + // GDI+ compatibility: Get rid of the height that DirectWrite assigns to zero-width + // strings. + metrics.height = 0.0f; + } + } + + return metrics; +} + void TextFormatD2D::SetTrimming(bool trim) { IDWriteInlineObject* inlineObject = nullptr; diff --git a/Common/Gfx/TextFormatD2D.h b/Common/Gfx/TextFormatD2D.h index d37e55b1..537853c2 100644 --- a/Common/Gfx/TextFormatD2D.h +++ b/Common/Gfx/TextFormatD2D.h @@ -54,13 +54,20 @@ private: // Creates a new DirectWrite text layout if |str| has changed since last call. Since creating // the layout is costly, it is more efficient to keep reusing the text layout until the text // changes. - void CreateLayout(const WCHAR* str, UINT strLen, float maxW, float maxH); + void CreateLayout(const WCHAR* str, UINT strLen, float maxW, float maxH, bool gdiEmulation); + + DWRITE_TEXT_METRICS GetMetrics( + const WCHAR* str, UINT strLen, bool gdiEmulation, float maxWidth = 10000.0f); Microsoft::WRL::ComPtr m_TextFormat; Microsoft::WRL::ComPtr m_TextLayout; Microsoft::WRL::ComPtr m_InlineEllipsis; std::wstring m_LastString; + + // Used to emulate GDI+ behaviour. + float m_ExtraHeight; + float m_LineGap; }; } // namespace Gfx diff --git a/Common/Gfx/Util/DWriteHelpers.cpp b/Common/Gfx/Util/DWriteHelpers.cpp index 1ab3c941..e4b1601c 100644 --- a/Common/Gfx/Util/DWriteHelpers.cpp +++ b/Common/Gfx/Util/DWriteHelpers.cpp @@ -23,31 +23,6 @@ namespace Gfx { namespace Util { -DWRITE_TEXT_METRICS GetAdjustedDWriteTextLayoutMetrics( - IDWriteTextLayout* textLayout, bool gdiEmulation) -{ - DWRITE_TEXT_METRICS metrics; - textLayout->GetMetrics(&metrics); - - if (metrics.width > 0.0f) - { - if (gdiEmulation) - { - float size = 0.0f; - textLayout->GetFontSize(0, &size); - metrics.width = floor(metrics.width + (size / 2.05f) + (metrics.width / 55.0f) - 0.5f); - metrics.height = floor(metrics.height + (size / 9.25f) + 0.3f); - } - } - else - { - // Get rid of the height that DirectWrite assigns to zero-width strings. - metrics.height = 0.0f; - } - - return metrics; -} - HRESULT GetDWritePropertiesFromGDIProperties( IDWriteFactory* factory, const WCHAR* gdiFamilyName, const bool gdiBold, const bool gdiItalic, DWRITE_FONT_WEIGHT& dwriteFontWeight, DWRITE_FONT_STYLE& dwriteFontStyle, diff --git a/Common/Gfx/Util/DWriteHelpers.h b/Common/Gfx/Util/DWriteHelpers.h index 278f0b36..1140e2a5 100644 --- a/Common/Gfx/Util/DWriteHelpers.h +++ b/Common/Gfx/Util/DWriteHelpers.h @@ -24,11 +24,6 @@ namespace Gfx { namespace Util { -// If |gdiEmulation| is true, the returns metrics have similar characteristics to those provided -// by GDI+. -DWRITE_TEXT_METRICS GetAdjustedDWriteTextLayoutMetrics( - IDWriteTextLayout* textLayout, bool gdiEmulation); - // Maps the GDI family name and italic/bold flags to the DirectWrite family name, weight, style, // and stretch. HRESULT GetDWritePropertiesFromGDIProperties(