Skip to content

Commit

Permalink
Add support for the "italic" graphic rendition attribute (#8580)
Browse files Browse the repository at this point in the history
This PR adds support for the ANSI _italic_ graphic rendition attribute,
which is enabled by the `SGR 3` escape sequence.

For the GDI renderer, I've just created an additional italic variant of
the font, and then the `UpdateDrawingBrushes` method selects the
appropriate font variant into the device context based on the requested
text attributes.

It's a bit more complicated in the DX renderer, because we need both an
italic variant of the font, and a variant of the text format object. The
`CustomTextLayout` class also had to be updated to hold the two font and
format instances, and decide which of the variants to use based on a
`useItalicFont` property in the drawing context, initially set in the
`UpdateDrawingBrushes` method.

## Validation Steps Performed
I've created some test content using a range of different character sets
(e.g. CJK, block characters, emoji, etc.), then applied the italic
attribute mixed with various other SGR attributes to see how they
interact. The output isn't always perfect, but I think it seems
reasonable given the constraints of a cell-based terminal renderer.

Closes #5461
  • Loading branch information
j4james authored Dec 18, 2020
1 parent 8276b54 commit fc7b052
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 22 deletions.
34 changes: 25 additions & 9 deletions src/renderer/dx/CustomTextLayout.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "precomp.h"

#include "CustomTextLayout.h"
#include "CustomTextRenderer.h"

#include <wrl.h>
#include <wrl/client.h>
Expand All @@ -19,19 +20,27 @@ using namespace Microsoft::Console::Render;
// - factory - DirectWrite factory reference in case we need other DirectWrite objects for our layout
// - analyzer - DirectWrite text analyzer from the factory that has been cached at a level above this layout (expensive to create)
// - format - The DirectWrite format object representing the size and other text properties to be applied (by default) to a layout
// - formatItalic - The italic variant of the format object representing the size and other text properties for italic text
// - font - The DirectWrite font face to use while calculating layout (by default, will fallback if necessary)
// - fontItalic - The italic variant of the font face to use while calculating layout for italic text
// - width - The count of pixels available per column (the expected pixel width of every column)
// - boxEffect - Box drawing scaling effects that are cached for the base font across layouts.
CustomTextLayout::CustomTextLayout(gsl::not_null<IDWriteFactory1*> const factory,
gsl::not_null<IDWriteTextAnalyzer1*> const analyzer,
gsl::not_null<IDWriteTextFormat*> const format,
gsl::not_null<IDWriteTextFormat*> const formatItalic,
gsl::not_null<IDWriteFontFace1*> const font,
gsl::not_null<IDWriteFontFace1*> const fontItalic,
size_t const width,
IBoxDrawingEffect* const boxEffect) :
_factory{ factory.get() },
_analyzer{ analyzer.get() },
_format{ format.get() },
_formatItalic{ formatItalic.get() },
_formatInUse{ format.get() },
_font{ font.get() },
_fontItalic{ fontItalic.get() },
_fontInUse{ font.get() },
_boxDrawingEffect{ boxEffect },
_localeName{},
_numberSubstitution{},
Expand Down Expand Up @@ -113,6 +122,9 @@ CATCH_RETURN()
RETURN_HR_IF_NULL(E_INVALIDARG, columns);
*columns = 0;

_formatInUse = _format.Get();
_fontInUse = _font.Get();

RETURN_IF_FAILED(_AnalyzeTextComplexity());
RETURN_IF_FAILED(_AnalyzeRuns());
RETURN_IF_FAILED(_ShapeGlyphRuns());
Expand Down Expand Up @@ -144,6 +156,10 @@ CATCH_RETURN()
FLOAT originX,
FLOAT originY) noexcept
{
const auto drawingContext = static_cast<const DrawingContext*>(clientDrawingContext);
_formatInUse = drawingContext->useItalicFont ? _formatItalic.Get() : _format.Get();
_fontInUse = drawingContext->useItalicFont ? _fontItalic.Get() : _font.Get();

RETURN_IF_FAILED(_AnalyzeTextComplexity());
RETURN_IF_FAILED(_AnalyzeRuns());
RETURN_IF_FAILED(_ShapeGlyphRuns());
Expand Down Expand Up @@ -183,7 +199,7 @@ CATCH_RETURN()
const HRESULT hr = _analyzer->GetTextComplexity(
_text.c_str(),
textLength,
_font.Get(),
_fontInUse,
&isTextSimple,
&uiLengthRead,
&_glyphIndices.at(glyphStart));
Expand Down Expand Up @@ -240,7 +256,7 @@ CATCH_RETURN()
{
if (!run.fontFace)
{
run.fontFace = _font;
run.fontFace = _fontInUse;
}
}

Expand Down Expand Up @@ -360,15 +376,15 @@ CATCH_RETURN()

USHORT designUnitsPerEm = metrics.designUnitsPerEm;

RETURN_IF_FAILED(_font->GetDesignGlyphAdvances(
RETURN_IF_FAILED(_fontInUse->GetDesignGlyphAdvances(
textLength,
&_glyphIndices.at(glyphStart),
&_glyphDesignUnitAdvances.at(glyphStart),
run.isSideways));

for (size_t i = glyphStart; i < _glyphAdvances.size(); i++)
{
_glyphAdvances.at(i) = (float)_glyphDesignUnitAdvances.at(i) / designUnitsPerEm * _format->GetFontSize() * run.fontScale;
_glyphAdvances.at(i) = (float)_glyphDesignUnitAdvances.at(i) / designUnitsPerEm * _formatInUse->GetFontSize() * run.fontScale;
}

// Set all the clusters as sequential. In a simple run, we're going 1 to 1.
Expand Down Expand Up @@ -433,7 +449,7 @@ CATCH_RETURN()
_glyphAdvances.resize(std::max(gsl::narrow_cast<size_t>(glyphStart) + gsl::narrow_cast<size_t>(actualGlyphCount), _glyphAdvances.size()));
_glyphOffsets.resize(std::max(gsl::narrow_cast<size_t>(glyphStart) + gsl::narrow_cast<size_t>(actualGlyphCount), _glyphOffsets.size()));

const auto fontSizeFormat = _format->GetFontSize();
const auto fontSizeFormat = _formatInUse->GetFontSize();
const auto fontSize = fontSizeFormat * run.fontScale;

hr = _analyzer->GetGlyphPlacements(
Expand Down Expand Up @@ -913,7 +929,7 @@ CATCH_RETURN();
// internal storage representation into something that matches DWrite's structures.
DWRITE_GLYPH_RUN glyphRun;
glyphRun.bidiLevel = run.bidiLevel;
glyphRun.fontEmSize = _format->GetFontSize() * run.fontScale;
glyphRun.fontEmSize = _formatInUse->GetFontSize() * run.fontScale;
glyphRun.fontFace = run.fontFace.Get();
glyphRun.glyphAdvances = &_glyphAdvances.at(run.glyphStart);
glyphRun.glyphCount = run.glyphCount;
Expand Down Expand Up @@ -1226,7 +1242,7 @@ CATCH_RETURN();
{
// Get the font fallback first
::Microsoft::WRL::ComPtr<IDWriteTextFormat1> format1;
if (FAILED(_format.As(&format1)))
if (FAILED(_formatInUse->QueryInterface(IID_PPV_ARGS(&format1))))
{
// If IDWriteTextFormat1 does not exist, return directly as this OS version doesn't have font fallback.
return S_FALSE;
Expand Down Expand Up @@ -1318,7 +1334,7 @@ CATCH_RETURN();
}
else
{
run.fontFace = _font;
run.fontFace = _fontInUse;
}

// Store the font scale as well.
Expand Down Expand Up @@ -1458,7 +1474,7 @@ try
else
{
::Microsoft::WRL::ComPtr<IBoxDrawingEffect> eff;
RETURN_IF_FAILED(s_CalculateBoxEffect(_format.Get(), _width, run.fontFace.Get(), run.fontScale, &eff));
RETURN_IF_FAILED(s_CalculateBoxEffect(_formatInUse, _width, run.fontFace.Get(), run.fontScale, &eff));

// store data in the run
run.drawingEffect = std::move(eff);
Expand Down
14 changes: 10 additions & 4 deletions src/renderer/dx/CustomTextLayout.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ namespace Microsoft::Console::Render

CustomTextLayout(gsl::not_null<IDWriteFactory1*> const factory,
gsl::not_null<IDWriteTextAnalyzer1*> const analyzer,
gsl::not_null<IDWriteTextFormat*> const format,
gsl::not_null<IDWriteFontFace1*> const font,
gsl::not_null<IDWriteTextFormat*> const normalFormat,
gsl::not_null<IDWriteTextFormat*> const italicFormat,
gsl::not_null<IDWriteFontFace1*> const normalFont,
gsl::not_null<IDWriteFontFace1*> const italicFont,
size_t const width,
IBoxDrawingEffect* const boxEffect);

Expand Down Expand Up @@ -160,11 +162,15 @@ namespace Microsoft::Console::Render
// DirectWrite analyzer
const ::Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> _analyzer;

// DirectWrite text format
// DirectWrite text formats
const ::Microsoft::WRL::ComPtr<IDWriteTextFormat> _format;
const ::Microsoft::WRL::ComPtr<IDWriteTextFormat> _formatItalic;
IDWriteTextFormat* _formatInUse;

// DirectWrite font face
// DirectWrite font faces
const ::Microsoft::WRL::ComPtr<IDWriteFontFace1> _font;
const ::Microsoft::WRL::ComPtr<IDWriteFontFace1> _fontItalic;
IDWriteFontFace1* _fontInUse;

// Box drawing effect
const ::Microsoft::WRL::ComPtr<IBoxDrawingEffect> _boxDrawingEffect;
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/dx/CustomTextRenderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ namespace Microsoft::Console::Render
renderTarget(renderTarget),
foregroundBrush(foregroundBrush),
backgroundBrush(backgroundBrush),
useItalicFont(false),
forceGrayscaleAA(forceGrayscaleAA),
dwriteFactory(dwriteFactory),
spacing(spacing),
Expand All @@ -37,6 +38,7 @@ namespace Microsoft::Console::Render
ID2D1RenderTarget* renderTarget;
ID2D1SolidColorBrush* foregroundBrush;
ID2D1SolidColorBrush* backgroundBrush;
bool useItalicFont;
bool forceGrayscaleAA;
IDWriteFactory* dwriteFactory;
DWRITE_LINE_SPACING spacing;
Expand Down
43 changes: 41 additions & 2 deletions src/renderer/dx/DxRenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1909,13 +1909,15 @@ CATCH_RETURN()

// If we have a drawing context, it may be choosing its antialiasing based
// on the colors. Update it if it exists.
// Also record whether we need to render the text with an italic font.
// We only need to do this here because this is called all the time on painting frames
// and will update it in a timely fashion. Changing the AA mode or opacity do affect
// it, but we will always hit updating the drawing brushes so we don't
// need to update this in those locations.
if (_drawingContext)
{
_drawingContext->forceGrayscaleAA = _ShouldForceGrayscaleAA();
_drawingContext->useItalicFont = textAttributes.IsItalic();
}

if (textAttributes.IsHyperlink())
Expand Down Expand Up @@ -1943,17 +1945,26 @@ try
fiFontInfo,
_dpi,
_dwriteTextFormat,
_dwriteTextFormatItalic,
_dwriteTextAnalyzer,
_dwriteFontFace,
_dwriteFontFaceItalic,
_lineMetrics));

_glyphCell = fiFontInfo.GetSize();

// Calculate and cache the box effect for the base font. Scale is 1.0f because the base font is exactly the scale we want already.
RETURN_IF_FAILED(CustomTextLayout::s_CalculateBoxEffect(_dwriteTextFormat.Get(), _glyphCell.width(), _dwriteFontFace.Get(), 1.0f, &_boxDrawingEffect));

// Prepare the text layout
_customLayout = WRL::Make<CustomTextLayout>(_dwriteFactory.Get(), _dwriteTextAnalyzer.Get(), _dwriteTextFormat.Get(), _dwriteFontFace.Get(), _glyphCell.width(), _boxDrawingEffect.Get());
// Prepare the text layout.
_customLayout = WRL::Make<CustomTextLayout>(_dwriteFactory.Get(),
_dwriteTextAnalyzer.Get(),
_dwriteTextFormat.Get(),
_dwriteTextFormatItalic.Get(),
_dwriteFontFace.Get(),
_dwriteFontFaceItalic.Get(),
_glyphCell.width(),
_boxDrawingEffect.Get());

return S_OK;
}
Expand Down Expand Up @@ -2033,16 +2044,20 @@ float DxEngine::GetScaling() const noexcept
int const iDpi) noexcept
{
Microsoft::WRL::ComPtr<IDWriteTextFormat> format;
Microsoft::WRL::ComPtr<IDWriteTextFormat> formatItalic;
Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> analyzer;
Microsoft::WRL::ComPtr<IDWriteFontFace1> face;
Microsoft::WRL::ComPtr<IDWriteFontFace1> faceItalic;
LineMetrics lineMetrics;

return _GetProposedFont(pfiFontInfoDesired,
pfiFontInfo,
iDpi,
format,
formatItalic,
analyzer,
face,
faceItalic,
lineMetrics);
}

Expand Down Expand Up @@ -2311,8 +2326,10 @@ CATCH_RETURN();
FontInfo& actual,
const int dpi,
Microsoft::WRL::ComPtr<IDWriteTextFormat>& textFormat,
Microsoft::WRL::ComPtr<IDWriteTextFormat>& textFormatItalic,
Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1>& textAnalyzer,
Microsoft::WRL::ComPtr<IDWriteFontFace1>& fontFace,
Microsoft::WRL::ComPtr<IDWriteFontFace1>& fontFaceItalic,
LineMetrics& lineMetrics) const noexcept
{
try
Expand Down Expand Up @@ -2447,11 +2464,33 @@ CATCH_RETURN();

THROW_IF_FAILED(format.As(&textFormat));

// We also need to create an italic variant of the font face and text
// format, based on the same parameters, but using an italic style.
std::wstring fontNameItalic = fontName;
DWRITE_FONT_WEIGHT weightItalic = weight;
DWRITE_FONT_STYLE styleItalic = DWRITE_FONT_STYLE_ITALIC;
DWRITE_FONT_STRETCH stretchItalic = stretch;

const auto faceItalic = _ResolveFontFaceWithFallback(fontNameItalic, weightItalic, stretchItalic, styleItalic, fontLocaleName);

Microsoft::WRL::ComPtr<IDWriteTextFormat> formatItalic;
THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontNameItalic.data(),
nullptr,
weightItalic,
styleItalic,
stretchItalic,
fontSize,
localeName.data(),
&formatItalic));

THROW_IF_FAILED(formatItalic.As(&textFormatItalic));

Microsoft::WRL::ComPtr<IDWriteTextAnalyzer> analyzer;
THROW_IF_FAILED(_dwriteFactory->CreateTextAnalyzer(&analyzer));
THROW_IF_FAILED(analyzer.As(&textAnalyzer));

fontFace = face;
fontFaceItalic = faceItalic;

THROW_IF_FAILED(textFormat->SetLineSpacing(lineSpacing.method, lineSpacing.height, lineSpacing.baseline));
THROW_IF_FAILED(textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR));
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/dx/DxRenderer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,9 @@ namespace Microsoft::Console::Render

::Microsoft::WRL::ComPtr<IDWriteFactory1> _dwriteFactory;
::Microsoft::WRL::ComPtr<IDWriteTextFormat> _dwriteTextFormat;
::Microsoft::WRL::ComPtr<IDWriteTextFormat> _dwriteTextFormatItalic;
::Microsoft::WRL::ComPtr<IDWriteFontFace1> _dwriteFontFace;
::Microsoft::WRL::ComPtr<IDWriteFontFace1> _dwriteFontFaceItalic;
::Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> _dwriteTextAnalyzer;
::Microsoft::WRL::ComPtr<CustomTextLayout> _customLayout;
::Microsoft::WRL::ComPtr<CustomTextRenderer> _customRenderer;
Expand Down Expand Up @@ -317,8 +319,10 @@ namespace Microsoft::Console::Render
FontInfo& actual,
const int dpi,
::Microsoft::WRL::ComPtr<IDWriteTextFormat>& textFormat,
::Microsoft::WRL::ComPtr<IDWriteTextFormat>& textFormatItalic,
::Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1>& textAnalyzer,
::Microsoft::WRL::ComPtr<IDWriteFontFace1>& fontFace,
::Microsoft::WRL::ComPtr<IDWriteFontFace1>& fontFaceItalic,
LineMetrics& lineMetrics) const noexcept;

[[nodiscard]] til::size _GetClientSize() const;
Expand Down
5 changes: 4 additions & 1 deletion src/renderer/gdi/gdirenderer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ namespace Microsoft::Console::Render
bool _isTrueTypeFont;
UINT _fontCodepage;
HFONT _hfont;
HFONT _hfontItalic;
TEXTMETRICW _tmFontMetrics;

static const size_t s_cPolyTextCache = 80;
Expand Down Expand Up @@ -122,6 +123,7 @@ namespace Microsoft::Console::Render

COLORREF _lastFg;
COLORREF _lastBg;
bool _lastFontItalic;

[[nodiscard]] HRESULT _InvalidCombine(const RECT* const prc) noexcept;
[[nodiscard]] HRESULT _InvalidOffset(const POINT* const ppt) noexcept;
Expand Down Expand Up @@ -152,7 +154,8 @@ namespace Microsoft::Console::Render
[[nodiscard]] HRESULT _GetProposedFont(const FontInfoDesired& FontDesired,
_Out_ FontInfo& Font,
const int iDpi,
_Inout_ wil::unique_hfont& hFont) noexcept;
_Inout_ wil::unique_hfont& hFont,
_Inout_ wil::unique_hfont& hFontItalic) noexcept;

COORD _GetFontSize() const;
bool _IsMinimized() const;
Expand Down
Loading

0 comments on commit fc7b052

Please sign in to comment.