// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "DxFontRenderData.h" #include "unicode.hpp" #include static constexpr float POINTS_PER_INCH = 72.0f; static constexpr const wchar_t* FALLBACK_FONT_FACES[] = { L"Consolas", L"Lucida Console", L"Courier New" }; static constexpr wchar_t FALLBACK_LOCALE[]{ L"en-us" }; using namespace Microsoft::Console::Render; DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwriteFactory) noexcept : _dwriteFactory(dwriteFactory), _fontSize{}, _lineMetrics{}, _lineSpacing{} { } [[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::Analyzer() { if (!_dwriteTextAnalyzer) { Microsoft::WRL::ComPtr analyzer; THROW_IF_FAILED(_dwriteFactory->CreateTextAnalyzer(&analyzer)); THROW_IF_FAILED(analyzer.As(&_dwriteTextAnalyzer)); } return _dwriteTextAnalyzer; } [[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::SystemFontFallback() { if (!_systemFontFallback) { ::Microsoft::WRL::ComPtr factory2; THROW_IF_FAILED(_dwriteFactory.As(&factory2)); factory2->GetSystemFontFallback(&_systemFontFallback); } return _systemFontFallback; } void DxFontRenderData::_RefreshUserLocaleName() { std::array buffer; const wchar_t* localeName = buffer.data(); auto length = GetUserDefaultLocaleName(buffer.data(), gsl::narrow_cast(buffer.size())); if (length <= 0) { localeName = &FALLBACK_LOCALE[0]; length = sizeof(FALLBACK_LOCALE); } // length is including the trailing null byte. // See GetUserDefaultLocaleName()'s docs. _userLocaleName = wil::make_process_heap_string_nothrow(localeName, length - 1); } [[nodiscard]] til::size DxFontRenderData::GlyphCell() const noexcept { return _glyphCell; } [[nodiscard]] DxFontRenderData::LineMetrics DxFontRenderData::GetLineMetrics() const noexcept { return _lineMetrics; } [[nodiscard]] DWRITE_FONT_WEIGHT DxFontRenderData::DefaultFontWeight() const noexcept { return _userFontWeight; } [[nodiscard]] DWRITE_FONT_STYLE DxFontRenderData::DefaultFontStyle() noexcept { return DWRITE_FONT_STYLE_NORMAL; } [[nodiscard]] DWRITE_FONT_STRETCH DxFontRenderData::DefaultFontStretch() noexcept { return DWRITE_FONT_STRETCH_NORMAL; } [[nodiscard]] const std::vector& DxFontRenderData::DefaultFontFeatures() const noexcept { return _featureVector; } [[nodiscard]] IDWriteTextFormat* DxFontRenderData::DefaultTextFormat() const noexcept { return _textFormats[0][0].Get(); } [[nodiscard]] IDWriteFontFace1* DxFontRenderData::DefaultFontFace() const noexcept { return _fontFaces[0][0].Get(); } [[nodiscard]] IBoxDrawingEffect* DxFontRenderData::DefaultBoxDrawingEffect() { if (!_boxDrawingEffect) { // 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. THROW_IF_FAILED(s_CalculateBoxEffect(DefaultTextFormat(), _glyphCell.width(), DefaultFontFace(), 1.0f, &_boxDrawingEffect)); } return _boxDrawingEffect.Get(); } [[nodiscard]] IDWriteTextFormat* DxFontRenderData::TextFormatWithAttribute(DWRITE_FONT_WEIGHT weight, DWRITE_FONT_STYLE style) const noexcept { return _textFormats[style != DWRITE_FONT_STYLE_NORMAL][weight != _userFontWeight].Get(); } [[nodiscard]] IDWriteFontFace1* DxFontRenderData::FontFaceWithAttribute(DWRITE_FONT_WEIGHT weight, DWRITE_FONT_STYLE style) const noexcept { return _fontFaces[style != DWRITE_FONT_STYLE_NORMAL][weight != _userFontWeight].Get(); } const std::vector& DxFontRenderData::GetAxisVector(DWRITE_FONT_WEIGHT weight, DWRITE_FONT_STYLE style) const noexcept { return _textFormatAxes[style != DWRITE_FONT_STYLE_NORMAL][weight != _userFontWeight]; } // Routine Description: // - Updates the font used for drawing // Arguments: // - desired - Information specifying the font that is requested // - actual - Filled with the nearest font actually chosen for drawing // - dpi - The DPI of the screen // Return Value: // - S_OK or relevant DirectX error [[nodiscard]] HRESULT DxFontRenderData::UpdateFont(const FontInfoDesired& desired, FontInfo& actual, const int dpi, const std::unordered_map& features, const std::unordered_map& axes) noexcept try { std::vector fontFeatures; if (!features.empty()) { fontFeatures.reserve(features.size() + 3); // All of these features are enabled by default by DirectWrite. // If you want to (and can) peek into the source of DirectWrite // you can look for the "GenericDefaultGsubFeatures" and "GenericDefaultGposFeatures" arrays. // Gsub is for GetGlyphs() and Gpos for GetGlyphPlacements(). // // GH#10774: Apparently specifying all of the features is just redundant. fontFeatures.emplace_back(DWRITE_FONT_FEATURE{ DWRITE_FONT_FEATURE_TAG_STANDARD_LIGATURES, 1 }); fontFeatures.emplace_back(DWRITE_FONT_FEATURE{ DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_LIGATURES, 1 }); fontFeatures.emplace_back(DWRITE_FONT_FEATURE{ DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_ALTERNATES, 1 }); for (const auto& p : features) { if (p.first.size() == 4) { const auto s = p.first.data(); switch (const auto tag = DWRITE_MAKE_FONT_FEATURE_TAG(s[0], s[1], s[2], s[3])) { case DWRITE_FONT_FEATURE_TAG_STANDARD_LIGATURES: fontFeatures[0].parameter = p.second; break; case DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_LIGATURES: fontFeatures[1].parameter = p.second; break; case DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_ALTERNATES: fontFeatures[2].parameter = p.second; break; default: fontFeatures.emplace_back(DWRITE_FONT_FEATURE{ tag, p.second }); break; } } } } std::vector fontAxisValues; if (!axes.empty()) { fontAxisValues.reserve(axes.size() + 3); // Just a few lines below we configure IDWriteTextFormat3 instances with these font axes. // We need at least all 3 of these axes below, in order to ensure that if we get a request for an italic // text format, we can supply an italic one, no matter what font axes the user set (or didn't set). fontAxisValues.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_WEIGHT, -1.0f }); fontAxisValues.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_ITALIC, -1.0f }); fontAxisValues.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_SLANT, -1.0f }); for (const auto& p : axes) { if (p.first.size() == 4) { const auto s = p.first.data(); switch (const auto tag = DWRITE_MAKE_FONT_AXIS_TAG(s[0], s[1], s[2], s[3])) { case DWRITE_FONT_AXIS_TAG_WEIGHT: fontAxisValues[0].value = p.second; break; case DWRITE_FONT_AXIS_TAG_ITALIC: fontAxisValues[1].value = p.second; break; case DWRITE_FONT_AXIS_TAG_SLANT: fontAxisValues[2].value = p.second; break; default: fontAxisValues.emplace_back(DWRITE_FONT_AXIS_VALUE{ tag, p.second }); break; } } } } _RefreshUserLocaleName(); _BuildFontRenderData(desired, actual, dpi); _featureVector = std::move(fontFeatures); _userFontWeight = static_cast(actual.GetWeight()); _boxDrawingEffect.Reset(); { // Just a few lines above we hardcode indices 0/1/2 in fontAxisValues to the weight/italic/slant axes. // If they're -1.0f they haven't been set by the user and must be filled by us. // When we call SetFontAxisValues() we basically override (disable) DirectWrite's internal font axes, // and if either of the 3 aren't set we'd make it impossible for the user to see bold/italic text. #pragma warning(suppress : 26494) // Variable 'standardAxes' is uninitialized. Always initialize an object (type.5). std::array standardAxes; if (!fontAxisValues.empty()) { Expects(fontAxisValues.size() >= standardAxes.size()); memcpy(standardAxes.data(), fontAxisValues.data(), sizeof(standardAxes)); } for (auto italic = 0; italic < 2; ++italic) { for (auto bold = 0; bold < 2; ++bold) { const auto fontWeight = bold ? DWRITE_FONT_WEIGHT_BOLD : _userFontWeight; const auto fontStyle = italic ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL; auto& textFormat = _textFormats[italic][bold]; // If the given face name couldn't be found this will call _NearbyCollection at some point. // Thanks to that we can use _nearbyFontCollection.Get() below to get the font collection for our textFormat as well. const auto font = _ResolveFontWithFallback(actual.GetFaceName(), fontWeight, fontStyle); Microsoft::WRL::ComPtr face; THROW_IF_FAILED(font->CreateFontFace(&face)); THROW_IF_FAILED(face.As(&_fontFaces[italic][bold])); THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(actual.GetFaceName().c_str(), _nearbyFontCollection.Get(), font->GetWeight(), font->GetStyle(), DWRITE_FONT_STRETCH_NORMAL, _fontSize, _userLocaleName.get(), &textFormat)); THROW_IF_FAILED(textFormat->SetLineSpacing(_lineSpacing.method, _lineSpacing.height, _lineSpacing.baseline)); THROW_IF_FAILED(textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR)); THROW_IF_FAILED(textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP)); if (!fontAxisValues.empty()) { Microsoft::WRL::ComPtr textFormat3; if (SUCCEEDED(textFormat.As(&textFormat3))) { // The wght axis defaults to the font weight. fontAxisValues[0].value = bold || standardAxes[0].value == -1.0f ? static_cast(fontWeight) : standardAxes[0].value; // The ital axis defaults to 1 if this is italic and 0 otherwise. fontAxisValues[1].value = italic ? 1.0f : (standardAxes[1].value == -1.0f ? 0.0f : standardAxes[1].value); // The slnt axis defaults to -12 if this is italic and 0 otherwise. fontAxisValues[2].value = italic ? -12.0f : (standardAxes[2].value == -1.0f ? 0.0f : standardAxes[2].value); THROW_IF_FAILED(textFormat3->SetFontAxisValues(fontAxisValues.data(), gsl::narrow_cast(fontAxisValues.size()))); _textFormatAxes[italic][bold] = fontAxisValues; } } } } } return S_OK; } CATCH_RETURN() // Routine Description: // - Calculates the box drawing scale/translate matrix values to fit a box glyph into the cell as perfectly as possible. // Arguments: // - format - Text format used to determine line spacing (height including ascent & descent) as calculated from the base font. // - widthPixels - The pixel width of the available cell. // - face - The font face that is currently being used, may differ from the base font from the layout. // - fontScale - if the given font face is going to be scaled versus the format, we need to know so we can compensate for that. pass 1.0f for no scaling. // - effect - Receives the effect to apply to box drawing characters. If no effect is received, special treatment isn't required. // Return Value: // - S_OK, GSL/WIL errors, DirectWrite errors, or math errors. [[nodiscard]] HRESULT DxFontRenderData::s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept try { // Check for bad in parameters. RETURN_HR_IF(E_INVALIDARG, !format); RETURN_HR_IF(E_INVALIDARG, !face); // Check the out parameter and fill it up with null. RETURN_HR_IF(E_INVALIDARG, !effect); *effect = nullptr; // The format is based around the main font that was specified by the user. // We need to know its size as well as the final spacing that was calculated around // it when it was first selected to get an idea of how large the bounding box is. const auto fontSize = format->GetFontSize(); DWRITE_LINE_SPACING_METHOD spacingMethod; float lineSpacing; // total height of the cells float baseline; // vertical position counted down from the top where the characters "sit" RETURN_IF_FAILED(format->GetLineSpacing(&spacingMethod, &lineSpacing, &baseline)); const float ascentPixels = baseline; const float descentPixels = lineSpacing - baseline; // We need this for the designUnitsPerEm which will be required to move back and forth between // Design Units and Pixels. I'll elaborate below. DWRITE_FONT_METRICS1 fontMetrics; face->GetMetrics(&fontMetrics); // If we had font fallback occur, the size of the font given to us (IDWriteFontFace1) can be different // than the font size used for the original format (IDWriteTextFormat). const auto scaledFontSize = fontScale * fontSize; // This is Unicode FULL BLOCK U+2588. // We presume that FULL BLOCK should be filling its entire cell in all directions so it should provide a good basis // in knowing exactly where to touch every single edge. // We're also presuming that the other box/line drawing glyphs were authored in this font to perfectly inscribe // inside of FULL BLOCK, with the same left/top/right/bottom bearings so they would look great when drawn adjacent. const UINT32 blockCodepoint = L'\x2588'; // Get the index of the block out of the font. UINT16 glyphIndex; RETURN_IF_FAILED(face->GetGlyphIndicesW(&blockCodepoint, 1, &glyphIndex)); // If it was 0, it wasn't found in the font. We're going to try again with // Unicode BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL U+253C which should be touching // all the edges of the possible rectangle, much like a full block should. if (glyphIndex == 0) { const UINT32 alternateCp = L'\x253C'; RETURN_IF_FAILED(face->GetGlyphIndicesW(&alternateCp, 1, &glyphIndex)); } // If we still didn't find the glyph index, we haven't implemented any further logic to figure out the box dimensions. // So we're just going to leave successfully as is and apply no scaling factor. It might look not-right, but it won't // stop the rendering pipeline. RETURN_HR_IF(S_FALSE, glyphIndex == 0); // Get the metrics of the given glyph, which we're going to treat as the outline box in which all line/block drawing // glyphs will be inscribed within, perfectly touching each edge as to align when two cells meet. DWRITE_GLYPH_METRICS boxMetrics = { 0 }; RETURN_IF_FAILED(face->GetDesignGlyphMetrics(&glyphIndex, 1, &boxMetrics)); // NOTE: All metrics we receive from DWRITE are going to be in "design units" which are a somewhat agnostic // way of describing proportions. // Converting back and forth between real pixels and design units is possible using // any font's specific fontSize and the designUnitsPerEm FONT_METRIC value. // // Here's what to know about the boxMetrics: // // // // topLeft --> +--------------------------------+ --- // | ^ | | // | | topSide | | // | | Bearing | | // | v | | // | +-----------------+ | | // | | | | | // | | | | | a // | | | | | d // | | | | | v // +<---->+ | | | a // | | | | | n // | left | | | | c // | Side | | | | e // | Bea- | | | | H // | ring | | right | | e // vertical | | | Side | | i // OriginY --> x | | Bea- | | g // | | | ring | | h // | | | | | t // | | +<----->+ | // | +-----------------+ | | // | ^ | | // | bottomSide | | | // | Bearing | | | // | v | | // +--------------------------------+ --- // // // | | // +--------------------------------+ // | advanceWidth | // // // NOTE: The bearings can be negative, in which case it is specifying that the glyphs overhang the box // as defined by the advanceHeight/width. // See also: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ns-dwrite-dwrite_glyph_metrics // The scale is a multiplier and the translation is addition. So *1 and +0 will mean nothing happens. const float defaultBoxVerticalScaleFactor = 1.0f; float boxVerticalScaleFactor = defaultBoxVerticalScaleFactor; const float defaultBoxVerticalTranslation = 0.0f; float boxVerticalTranslation = defaultBoxVerticalTranslation; { // First, find the dimensions of the glyph representing our fully filled box. // Ascent is how far up from the baseline we'll draw. // verticalOriginY is the measure from the topLeft corner of the bounding box down to where // the glyph's version of the baseline is. // topSideBearing is how much "gap space" is left between that topLeft and where the glyph // starts drawing. Subtract the gap space to find how far is drawn upward from baseline. const auto boxAscentDesignUnits = boxMetrics.verticalOriginY - boxMetrics.topSideBearing; // Descent is how far down from the baseline we'll draw. // advanceHeight is the total height of the drawn bounding box. // verticalOriginY is how much was given to the ascent, so subtract that out. // What remains is then the descent value. Remove the // bottomSideBearing as the "gap space" on the bottom to find how far is drawn downward from baseline. const auto boxDescentDesignUnits = boxMetrics.advanceHeight - boxMetrics.verticalOriginY - boxMetrics.bottomSideBearing; // The height, then, of the entire box is just the sum of the ascent above the baseline and the descent below. const auto boxHeightDesignUnits = boxAscentDesignUnits + boxDescentDesignUnits; // Second, find the dimensions of the cell we're going to attempt to fit within. // We know about the exact ascent/descent units in pixels as calculated when we chose a font and // adjusted the ascent/descent for a nice perfect baseline and integer total height. // All we need to do is adapt it into Design Units so it meshes nicely with the Design Units above. // Use the formula: Pixels * Design Units Per Em / Font Size = Design Units const auto cellAscentDesignUnits = ascentPixels * fontMetrics.designUnitsPerEm / scaledFontSize; const auto cellDescentDesignUnits = descentPixels * fontMetrics.designUnitsPerEm / scaledFontSize; const auto cellHeightDesignUnits = cellAscentDesignUnits + cellDescentDesignUnits; // OK, now do a few checks. If the drawn box touches the top and bottom of the cell // and the box is overall tall enough, then we'll not bother adjusting. // We will presume the font author has set things as they wish them to be. const auto boxTouchesCellTop = boxAscentDesignUnits >= cellAscentDesignUnits; const auto boxTouchesCellBottom = boxDescentDesignUnits >= cellDescentDesignUnits; const auto boxIsTallEnoughForCell = boxHeightDesignUnits >= cellHeightDesignUnits; // If not... if (!(boxTouchesCellTop && boxTouchesCellBottom && boxIsTallEnoughForCell)) { // Find a scaling factor that will make the total height drawn of this box // perfectly fit the same number of design units as the cell. // Since scale factor is a multiplier, it doesn't matter that this is design units. // The fraction between the two heights in pixels should be exactly the same // (which is what will matter when we go to actually render it... the pixels that is.) // Don't scale below 1.0. If it'd shrink, just center it at the prescribed scale. boxVerticalScaleFactor = std::max(cellHeightDesignUnits / boxHeightDesignUnits, 1.0f); // The box as scaled might be hanging over the top or bottom of the cell (or both). // We find out the amount of overhang/underhang on both the top and the bottom. const auto extraAscent = boxAscentDesignUnits * boxVerticalScaleFactor - cellAscentDesignUnits; const auto extraDescent = boxDescentDesignUnits * boxVerticalScaleFactor - cellDescentDesignUnits; // This took a bit of time and effort and it's difficult to put into words, but here goes. // We want the average of the two magnitudes to find out how much to "take" from one and "give" // to the other such that both are equal. We presume the glyphs are designed to be drawn // centered in their box vertically to look good. // The ordering around subtraction is required to ensure that the direction is correct with a negative // translation moving up (taking excess descent and adding to ascent) and positive is the opposite. const auto boxVerticalTranslationDesignUnits = (extraAscent - extraDescent) / 2; // The translation is just a raw movement of pixels up or down. Since we were working in Design Units, // we need to run the opposite algorithm shown above to go from Design Units to Pixels. boxVerticalTranslation = boxVerticalTranslationDesignUnits * scaledFontSize / fontMetrics.designUnitsPerEm; } } // The horizontal adjustments follow the exact same logic as the vertical ones. const float defaultBoxHorizontalScaleFactor = 1.0f; float boxHorizontalScaleFactor = defaultBoxHorizontalScaleFactor; const float defaultBoxHorizontalTranslation = 0.0f; float boxHorizontalTranslation = defaultBoxHorizontalTranslation; { // This is the only difference. We don't have a horizontalOriginX from the metrics. // However, https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ns-dwrite-dwrite_glyph_metrics says // the X coordinate is specified by half the advanceWidth to the right of the horizontalOrigin. // So we'll use that as the "center" and apply it the role that verticalOriginY had above. const auto boxCenterDesignUnits = boxMetrics.advanceWidth / 2; const auto boxLeftDesignUnits = boxCenterDesignUnits - boxMetrics.leftSideBearing; const auto boxRightDesignUnits = boxMetrics.advanceWidth - boxMetrics.rightSideBearing - boxCenterDesignUnits; const auto boxWidthDesignUnits = boxLeftDesignUnits + boxRightDesignUnits; const auto cellWidthDesignUnits = widthPixels * fontMetrics.designUnitsPerEm / scaledFontSize; const auto cellLeftDesignUnits = cellWidthDesignUnits / 2; const auto cellRightDesignUnits = cellLeftDesignUnits; const auto boxTouchesCellLeft = boxLeftDesignUnits >= cellLeftDesignUnits; const auto boxTouchesCellRight = boxRightDesignUnits >= cellRightDesignUnits; const auto boxIsWideEnoughForCell = boxWidthDesignUnits >= cellWidthDesignUnits; if (!(boxTouchesCellLeft && boxTouchesCellRight && boxIsWideEnoughForCell)) { boxHorizontalScaleFactor = std::max(cellWidthDesignUnits / boxWidthDesignUnits, 1.0f); const auto extraLeft = boxLeftDesignUnits * boxHorizontalScaleFactor - cellLeftDesignUnits; const auto extraRight = boxRightDesignUnits * boxHorizontalScaleFactor - cellRightDesignUnits; const auto boxHorizontalTranslationDesignUnits = (extraLeft - extraRight) / 2; boxHorizontalTranslation = boxHorizontalTranslationDesignUnits * scaledFontSize / fontMetrics.designUnitsPerEm; } } // If we set anything, make a drawing effect. Otherwise, there isn't one. if (defaultBoxVerticalScaleFactor != boxVerticalScaleFactor || defaultBoxVerticalTranslation != boxVerticalTranslation || defaultBoxHorizontalScaleFactor != boxHorizontalScaleFactor || defaultBoxHorizontalTranslation != boxHorizontalTranslation) { // OK, make the object that will represent our effect, stuff the metrics into it, and return it. RETURN_IF_FAILED(WRL::MakeAndInitialize(effect, boxVerticalScaleFactor, boxVerticalTranslation, boxHorizontalScaleFactor, boxHorizontalTranslation)); } return S_OK; } CATCH_RETURN() // Routine Description: // - Returns whether the user set or updated any of the font features to be applied bool DxFontRenderData::DidUserSetFeatures() const noexcept { return !_featureVector.empty(); } // Routine Description: // - Returns whether the user set or updated any of the font axes to be applied bool DxFontRenderData::DidUserSetAxes() const noexcept { return !_textFormatAxes[0][0].empty(); } // Routine Description: // - Build the needed data for rendering according to the font used // Arguments: // - desired - Information specifying the font that is requested // - actual - Filled with the nearest font actually chosen for drawing // - dpi - The DPI of the screen // Return Value: // - None void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi) { // This is the first attempt to resolve font face after `UpdateFont`. // Note that the following line may cause property changes _inside_ `_defaultFontInfo` because the desired font may not exist. // See the implementation of `ResolveFontFaceWithFallback` for details. const auto font = _ResolveFontWithFallback(desired.GetFaceName(), static_cast(desired.GetWeight()), DWRITE_FONT_STYLE_NORMAL); Microsoft::WRL::ComPtr face; THROW_IF_FAILED(font->CreateFontFace(&face)); DWRITE_FONT_METRICS1 fontMetrics; face->GetMetrics(&fontMetrics); const UINT32 spaceCodePoint = L'M'; UINT16 spaceGlyphIndex; THROW_IF_FAILED(face->GetGlyphIndicesW(&spaceCodePoint, 1, &spaceGlyphIndex)); DWRITE_GLYPH_METRICS spaceMetrics = { 0 }; THROW_IF_FAILED(face->GetDesignGlyphMetrics(&spaceGlyphIndex, 1, &spaceMetrics)); // The math here is actually: // Requested Size in Points * DPI scaling factor * Points to Pixels scaling factor. // - DPI = dots per inch // - PPI = points per inch or "points" as usually seen when choosing a font size // - The DPI scaling factor is the current monitor DPI divided by 96, the default DPI. // - The Points to Pixels factor is based on the typography definition of 72 points per inch. // As such, converting requires taking the 96 pixel per inch default and dividing by the 72 points per inch // to get a factor of 1 and 1/3. // This turns into something like: // - 12 ppi font * (96 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 16 pixels tall font for 100% display (96 dpi is 100%) // - 12 ppi font * (144 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 24 pixels tall font for 150% display (144 dpi is 150%) // - 12 ppi font * (192 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 32 pixels tall font for 200% display (192 dpi is 200%) float heightDesired = static_cast(desired.GetEngineSize().Y) * static_cast(USER_DEFAULT_SCREEN_DPI) / POINTS_PER_INCH; // The advance is the number of pixels left-to-right (X dimension) for the given font. // We're finding a proportional factor here with the design units in "ems", not an actual pixel measurement. // Now we play trickery with the font size. Scale by the DPI to get the height we expect. heightDesired *= (static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI)); const float widthAdvance = static_cast(spaceMetrics.advanceWidth) / fontMetrics.designUnitsPerEm; // Use the real pixel height desired by the "em" factor for the width to get the number of pixels // we will need per character in width. This will almost certainly result in fractional X-dimension pixels. const float widthApprox = heightDesired * widthAdvance; // Since we can't deal with columns of the presentation grid being fractional pixels in width, round to the nearest whole pixel. const float widthExact = round(widthApprox); // Now reverse the "em" factor from above to turn the exact pixel width into a (probably) fractional // height in pixels of each character. It's easier for us to pad out height and align vertically // than it is horizontally. const auto fontSize = widthExact / widthAdvance; _fontSize = fontSize; // Now figure out the basic properties of the character height which include ascent and descent // for this specific font size. const float ascent = (fontSize * fontMetrics.ascent) / fontMetrics.designUnitsPerEm; const float descent = (fontSize * fontMetrics.descent) / fontMetrics.designUnitsPerEm; // Get the gap. const float gap = (fontSize * fontMetrics.lineGap) / fontMetrics.designUnitsPerEm; const float halfGap = gap / 2; // We're going to build a line spacing object here to track all of this data in our format. DWRITE_LINE_SPACING lineSpacing = {}; lineSpacing.method = DWRITE_LINE_SPACING_METHOD_UNIFORM; // We need to make sure the baseline falls on a round pixel (not a fractional pixel). // If the baseline is fractional, the text appears blurry, especially at small scales. // Since we also need to make sure the bounding box as a whole is round pixels // (because the entire console system maths in full cell units), // we're just going to ceiling up the ascent and descent to make a full pixel amount // and set the baseline to the full round pixel ascent value. // // For reference, for the letters "ag": // ... // gggggg bottom of previous line // // ----------------- <===========================================| // | topSideBearing | 1/2 lineGap | // aaaaaa ggggggg <-------------------------|-------------| | // a g g | | | // aaaaa ggggg |<-ascent | | // a a g | | |---- lineHeight // aaaaa a gggggg <----baseline, verticalOriginY----------|---| // g g |<-descent | | // gggggg <-------------------------|-------------| | // | bottomSideBearing | 1/2 lineGap | // ----------------- <===========================================| // // aaaaaa ggggggg top of next line // ... // // Also note... // We're going to add half the line gap to the ascent and half the line gap to the descent // to ensure that the spacing is balanced vertically. // Generally speaking, the line gap is added to the ascent by DirectWrite itself for // horizontally drawn text which can place the baseline and glyphs "lower" in the drawing // box than would be desired for proper alignment of things like line and box characters // which will try to sit centered in the area and touch perfectly with their neighbors. const auto fullPixelAscent = ceil(ascent + halfGap); const auto fullPixelDescent = ceil(descent + halfGap); lineSpacing.height = fullPixelAscent + fullPixelDescent; lineSpacing.baseline = fullPixelAscent; // According to MSDN (https://docs.microsoft.com/en-us/windows/win32/api/dwrite_3/ne-dwrite_3-dwrite_font_line_gap_usage) // Setting "ENABLED" means we've included the line gapping in the spacing numbers given. lineSpacing.fontLineGapUsage = DWRITE_FONT_LINE_GAP_USAGE_ENABLED; _lineSpacing = lineSpacing; // The scaled size needs to represent the pixel box that each character will fit within for the purposes // of hit testing math and other such multiplication/division. COORD coordSize = { 0 }; coordSize.X = gsl::narrow(widthExact); coordSize.Y = gsl::narrow_cast(lineSpacing.height); // Unscaled is for the purposes of re-communicating this font back to the renderer again later. // As such, we need to give the same original size parameter back here without padding // or rounding or scaling manipulation. const COORD unscaled = desired.GetEngineSize(); const COORD scaled = coordSize; actual.SetFromEngine(desired.GetFaceName().c_str(), desired.GetFamily(), desired.GetWeight(), false, scaled, unscaled); LineMetrics lineMetrics; // There is no font metric for the grid line width, so we use a small // multiple of the font size, which typically rounds to a pixel. lineMetrics.gridlineWidth = std::round(fontSize * 0.025f); // All other line metrics are in design units, so to get a pixel value, // we scale by the font size divided by the design-units-per-em. const auto scale = fontSize / fontMetrics.designUnitsPerEm; lineMetrics.underlineOffset = std::round(fontMetrics.underlinePosition * scale); lineMetrics.underlineWidth = std::round(fontMetrics.underlineThickness * scale); lineMetrics.strikethroughOffset = std::round(fontMetrics.strikethroughPosition * scale); lineMetrics.strikethroughWidth = std::round(fontMetrics.strikethroughThickness * scale); // We always want the lines to be visible, so if a stroke width ends up // at zero after rounding, we need to make it at least 1 pixel. lineMetrics.gridlineWidth = std::max(lineMetrics.gridlineWidth, 1.0f); lineMetrics.underlineWidth = std::max(lineMetrics.underlineWidth, 1.0f); lineMetrics.strikethroughWidth = std::max(lineMetrics.strikethroughWidth, 1.0f); // Offsets are relative to the base line of the font, so we subtract // from the ascent to get an offset relative to the top of the cell. lineMetrics.underlineOffset = fullPixelAscent - lineMetrics.underlineOffset; lineMetrics.strikethroughOffset = fullPixelAscent - lineMetrics.strikethroughOffset; // For double underlines we need a second offset, just below the first, // but with a bit of a gap (about double the grid line width). lineMetrics.underlineOffset2 = lineMetrics.underlineOffset + lineMetrics.underlineWidth + std::round(fontSize * 0.05f); // However, we don't want the underline to extend past the bottom of the // cell, so we clamp the offset to fit just inside. const auto maxUnderlineOffset = lineSpacing.height - lineMetrics.underlineWidth; lineMetrics.underlineOffset2 = std::min(lineMetrics.underlineOffset2, maxUnderlineOffset); // But if the resulting gap isn't big enough even to register as a thicker // line, it's better to place the second line slightly above the first. if (lineMetrics.underlineOffset2 < lineMetrics.underlineOffset + lineMetrics.gridlineWidth) { lineMetrics.underlineOffset2 = lineMetrics.underlineOffset - lineMetrics.gridlineWidth; } // We also add half the stroke width to the offsets, since the line // coordinates designate the center of the line. lineMetrics.underlineOffset += lineMetrics.underlineWidth / 2.0f; lineMetrics.underlineOffset2 += lineMetrics.underlineWidth / 2.0f; lineMetrics.strikethroughOffset += lineMetrics.strikethroughWidth / 2.0f; _lineMetrics = lineMetrics; _glyphCell = actual.GetSize(); } // Routine Description: // - Attempts to locate the font given, but then begins falling back if we cannot find it. // - We'll try to fall back to Consolas with the given weight/stretch/style first, // then try Consolas again with normal weight/stretch/style, // and if nothing works, then we'll throw an error. // Arguments: // - dwriteFactory - The DWrite factory to use // - localeName - Locale to search for appropriate fonts // Return Value: // - Smart pointer holding interface reference for queryable font data. [[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::_ResolveFontWithFallback(std::wstring familyName, DWRITE_FONT_WEIGHT weight, DWRITE_FONT_STYLE style) { if (familyName.empty()) { familyName = L"Consolas"; } if (!weight) { weight = DWRITE_FONT_WEIGHT_NORMAL; } if (!style) { style = DWRITE_FONT_STYLE_NORMAL; } // First attempt to find exactly what the user asked for. Microsoft::WRL::ComPtr font; // GH#10211 - wrap this all up in a try/catch. If the nearby fonts are // corrupted, then we don't want to throw out of this top half of this // method. We still want to fall back to a font that's reasonable, below. try { font = _FindFont(familyName.c_str(), weight, style); if (!font) { // If we missed, try looking a little more by trimming the last word off the requested family name a few times. // Quite often, folks are specifying weights or something in the familyName and it causes failed resolution and // an unexpected error dialog. We theoretically could detect the weight words and convert them, but this // is the quick fix for the majority scenario. // The long/full fix is backlogged to GH#9744 // Also this doesn't count as a fallback because we don't want to annoy folks with the warning dialog over // this resolution. while (!font && !familyName.empty()) { const auto lastSpace = familyName.find_last_of(UNICODE_SPACE); // value is unsigned and npos will be greater than size. // if we didn't find anything to trim, leave. if (lastSpace >= familyName.size()) { break; } // trim string down to just before the found space // (space found at 6... trim from 0 for 6 length will give us 0-5 as the new string) familyName = familyName.substr(0, lastSpace); // Try to find it with the shortened family name font = _FindFont(familyName.c_str(), weight, style); } } } CATCH_LOG(); // Alright, if our quick shot at trimming didn't work either... // move onto looking up a font from our hardcoded list of fonts // that should really always be available. if (!font) { for (const auto fallbackFont : FALLBACK_FONT_FACES) { // With these fonts, don't attempt the nearby lookup. We're looking // for system fonts only. If one of the nearby fonts is causing us // problems (like in GH#10211), then we don't want to go anywhere // near it in this part. font = _FindFont(fallbackFont, weight, style); if (font) { break; } font = _FindFont(fallbackFont, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL); if (font) { break; } } } THROW_HR_IF_NULL(E_FAIL, font); return font; } // Routine Description: // - Locates a suitable font face from the given information // Arguments: // - dwriteFactory - The DWrite factory to use // - localeName - Locale to search for appropriate fonts // Return Value: // - Smart pointer holding interface reference for queryable font data. [[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::_FindFont(const wchar_t* familyName, DWRITE_FONT_WEIGHT weight, DWRITE_FONT_STYLE style) { Microsoft::WRL::ComPtr font; Microsoft::WRL::ComPtr fontCollection; THROW_IF_FAILED(_dwriteFactory->GetSystemFontCollection(&fontCollection, false)); UINT32 familyIndex; BOOL familyExists; THROW_IF_FAILED(fontCollection->FindFamilyName(familyName, &familyIndex, &familyExists)); // If the system collection missed, try the files sitting next to our binary. if (!familyExists) { // May be null on OS below Windows 10. If null, just skip the attempt. if (const auto nearbyCollection = _NearbyCollection()) { THROW_IF_FAILED(nearbyCollection->FindFamilyName(familyName, &familyIndex, &familyExists)); fontCollection = nearbyCollection; } } if (familyExists) { Microsoft::WRL::ComPtr fontFamily; THROW_IF_FAILED(fontCollection->GetFontFamily(familyIndex, &fontFamily)); THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(weight, DWRITE_FONT_STRETCH_NORMAL, style, &font)); } return font; } // Routine Description: // - Creates a DirectWrite font collection of font files that are sitting next to the running // binary (in the same directory as the EXE). // Arguments: // - dwriteFactory - The DWrite factory to use // Return Value: // - DirectWrite font collection. May be null if one cannot be created. [[nodiscard]] IDWriteFontCollection* DxFontRenderData::_NearbyCollection() { if (_nearbyFontCollection) { return _nearbyFontCollection.Get(); } // The convenience interfaces for loading fonts from files // are only available on Windows 10+. ::Microsoft::WRL::ComPtr factory6; if (FAILED(_dwriteFactory->QueryInterface(&factory6))) { return nullptr; } ::Microsoft::WRL::ComPtr systemFontCollection; THROW_IF_FAILED(factory6->GetSystemFontCollection(false, &systemFontCollection, 0)); ::Microsoft::WRL::ComPtr systemFontSet; THROW_IF_FAILED(systemFontCollection->GetFontSet(&systemFontSet)); ::Microsoft::WRL::ComPtr fontSetBuilder2; THROW_IF_FAILED(factory6->CreateFontSetBuilder(&fontSetBuilder2)); THROW_IF_FAILED(fontSetBuilder2->AddFontSet(systemFontSet.Get())); // Magic static so we only attempt to grovel the hard disk once no matter how many instances // of the font collection itself we require. static const auto knownPaths = s_GetNearbyFonts(); for (auto& p : knownPaths) { fontSetBuilder2->AddFontFile(p.c_str()); } ::Microsoft::WRL::ComPtr fontSet; THROW_IF_FAILED(fontSetBuilder2->CreateFontSet(&fontSet)); ::Microsoft::WRL::ComPtr fontCollection; THROW_IF_FAILED(factory6->CreateFontCollectionFromFontSet(fontSet.Get(), &fontCollection)); _nearbyFontCollection = fontCollection; return _nearbyFontCollection.Get(); } // Routine Description: // - Digs through the directory that the current executable is running within to find // any TTF files sitting next to it. // Arguments: // - // Return Value: // - Iterable collection of filesystem paths, one per font file that was found [[nodiscard]] std::vector DxFontRenderData::s_GetNearbyFonts() { std::vector paths; // Find the directory we're running from then enumerate all the TTF files // sitting next to us. const std::filesystem::path module{ wil::GetModuleFileNameW(nullptr) }; const auto folder{ module.parent_path() }; for (const auto& p : std::filesystem::directory_iterator(folder)) { if (til::ends_with_insensitive_ascii(p.path().native(), L".ttf")) { paths.push_back(p.path()); } } return paths; }