terminal/src/renderer/base/FontResource.cpp
James Holderness 90ff261c35
Add support for downloadable soft fonts (#10011)
This PR adds conhost support for downloadable soft fonts - also known as
dynamically redefinable character sets (DRCS) - using the `DECDLD`
escape sequence.

These fonts are typically designed to work on a specific terminal model,
and each model tends to have a different character cell size. So in
order to support as many models as possible, the code attempts to detect
the original target size of the font, and then scale the glyphs to fit
our current cell size.

Once a font has been downloaded to the terminal, it can be designated in
the same way you would a standard character set, using an `SCS` escape
sequence. The identification string for the set is defined by the
`DECDLD` sequence. Internally we map the characters in this set to code
points `U+EF20` to `U+EF7F` in the Unicode private use are (PUA).

Then in the renderer, any characters in that range are split off into
separate runs, which get painted with a special font. The font itself is
dynamically generated as an in-memory resource, constructed from the
downloaded character bitmaps which have been scaled to the appropriate
size.

If no soft fonts are in use, then no mapping of the PUA code points will
take place, so this shouldn't interfere with anyone using those code
points for something else, as along as they aren't also trying to use
soft fonts. I also tried to pick a PUA range that hadn't already been
snatched up by Nerd Fonts, but if we do receive reports of a conflict,
it's easy enough to change.

## Validation Steps Performed

I added an adapter test that runs through a bunch of parameter
variations for the `DECDLD` sequence, to make sure we're correctly
detecting the font sizes for most of the known DEC terminal models.

I've also tested manually on a wide range of existing fonts, of varying
dimensions, and from multiple sources, and made sure they all worked
reasonably well.

Closes #9164
2021-08-06 20:41:02 +00:00

286 lines
12 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "../inc/FontResource.hpp"
using namespace Microsoft::Console::Render;
namespace
{
// The structures below are based on the Windows 3.0 font file format, which
// was documented in Microsoft Knowledge Base article Q65123. Although no
// longer hosted by Microsoft, it can still be found at the following URL:
// https://web.archive.org/web/20140820153410/http://support.microsoft.com/kb/65123
// For now we're only using fixed pitch single color fonts, but the rest
// of the flags are included here for completeness.
static constexpr DWORD DFF_FIXED = 0x0001;
static constexpr DWORD DFF_PROPORTIONAL = 0x0002;
static constexpr DWORD DFF_1COLOR = 0x0010;
static constexpr DWORD DFF_16COLOR = 0x0020;
static constexpr DWORD DFF_256COLOR = 0x0040;
static constexpr DWORD DFF_RGBCOLOR = 0x0080;
// DRCS soft fonts only require 96 characters at most.
static constexpr size_t CHAR_COUNT = 96;
#pragma pack(push, 1)
struct GLYPHENTRY
{
WORD geWidth;
DWORD geOffset;
};
struct FONTINFO
{
WORD dfVersion;
DWORD dfSize;
CHAR dfCopyright[60];
WORD dfType;
WORD dfPoints;
WORD dfVertRes;
WORD dfHorizRes;
WORD dfAscent;
WORD dfInternalLeading;
WORD dfExternalLeading;
BYTE dfItalic;
BYTE dfUnderline;
BYTE dfStrikeOut;
WORD dfWeight;
BYTE dfCharSet;
WORD dfPixWidth;
WORD dfPixHeight;
BYTE dfPitchAndFamily;
WORD dfAvgWidth;
WORD dfMaxWidth;
BYTE dfFirstChar;
BYTE dfLastChar;
BYTE dfDefaultChar;
BYTE dfBreakChar;
WORD dfWidthBytes;
DWORD dfDevice;
DWORD dfFace;
DWORD dfBitsPointer;
DWORD dfBitsOffset;
BYTE dfReserved;
DWORD dfFlags;
WORD dfAspace;
WORD dfBspace;
WORD dfCspace;
DWORD dfColorPointer;
DWORD dfReserved1[4];
GLYPHENTRY dfCharTable[CHAR_COUNT];
CHAR szFaceName[LF_FACESIZE];
};
#pragma pack(pop)
}
FontResource::FontResource(const gsl::span<const uint16_t> bitPattern,
const til::size sourceSize,
const til::size targetSize,
const size_t centeringHint) :
_bitPattern{ bitPattern.begin(), bitPattern.end() },
_sourceSize{ sourceSize },
_targetSize{ targetSize },
_centeringHint{ centeringHint }
{
}
void FontResource::SetTargetSize(const til::size targetSize)
{
if (_targetSize != targetSize)
{
_targetSize = targetSize;
_fontHandle = nullptr;
}
}
FontResource::operator HFONT()
{
if (!_fontHandle && !_bitPattern.empty())
{
_regenerateFont();
}
return _fontHandle.get();
}
void FontResource::_regenerateFont()
{
const auto targetWidth = _targetSize.width<WORD>();
const auto targetHeight = _targetSize.height<WORD>();
const auto charSizeInBytes = (targetWidth + 7) / 8 * targetHeight;
const DWORD fontBitmapSize = charSizeInBytes * CHAR_COUNT;
const DWORD fontResourceSize = sizeof(FONTINFO) + fontBitmapSize;
auto fontResourceBuffer = std::vector<byte>(fontResourceSize);
void* fontResourceBufferPointer = fontResourceBuffer.data();
auto& fontResource = *static_cast<FONTINFO*>(fontResourceBufferPointer);
fontResource.dfVersion = 0x300;
fontResource.dfSize = fontResourceSize;
fontResource.dfWeight = FW_NORMAL;
fontResource.dfCharSet = OEM_CHARSET;
fontResource.dfPixWidth = targetWidth;
fontResource.dfPixHeight = targetHeight;
fontResource.dfPitchAndFamily = FIXED_PITCH | FF_DONTCARE;
fontResource.dfAvgWidth = targetWidth;
fontResource.dfMaxWidth = targetWidth;
fontResource.dfFirstChar = L' ';
fontResource.dfLastChar = fontResource.dfFirstChar + CHAR_COUNT - 1;
fontResource.dfFace = offsetof(FONTINFO, szFaceName);
fontResource.dfBitsOffset = sizeof(FONTINFO);
fontResource.dfFlags = DFF_FIXED | DFF_1COLOR;
// We use an atomic counter to create a locally-unique name for the font.
static std::atomic<uint64_t> faceNameCounter;
sprintf_s(fontResource.szFaceName, "WTSOFTFONT%016llX", faceNameCounter++);
// Each character has a fixed size and position in the font bitmap, but we
// still need to fill in the header table with that information.
for (auto i = 0u; i < std::size(fontResource.dfCharTable); i++)
{
const auto charOffset = fontResource.dfBitsOffset + charSizeInBytes * i;
fontResource.dfCharTable[i].geOffset = charOffset;
fontResource.dfCharTable[i].geWidth = targetWidth;
}
// Raster fonts aren't generally scalable, so we need to resize the bit
// patterns for the character glyphs to the requested target size, and
// copy the results into the resource structure.
auto fontResourceSpan = gsl::span<byte>(fontResourceBuffer);
_resizeBitPattern(fontResourceSpan.subspan(fontResource.dfBitsOffset));
DWORD fontCount = 0;
_resourceHandle.reset(AddFontMemResourceEx(&fontResource, fontResourceSize, nullptr, &fontCount));
LOG_HR_IF_NULL(E_FAIL, _resourceHandle.get());
// Once the resource has been registered, we should be able to create the
// font by using the same name and attributes as were set in the resource.
LOGFONTA logFont = {};
logFont.lfHeight = fontResource.dfPixHeight;
logFont.lfWidth = fontResource.dfPixWidth;
logFont.lfCharSet = fontResource.dfCharSet;
logFont.lfOutPrecision = OUT_RASTER_PRECIS;
logFont.lfPitchAndFamily = fontResource.dfPitchAndFamily;
strcpy_s(logFont.lfFaceName, fontResource.szFaceName);
_fontHandle.reset(CreateFontIndirectA(&logFont));
LOG_HR_IF_NULL(E_FAIL, _fontHandle.get());
}
void FontResource::_resizeBitPattern(gsl::span<byte> targetBuffer)
{
auto sourceWidth = _sourceSize.width<int>();
auto targetWidth = _targetSize.width<int>();
const auto sourceHeight = _sourceSize.height<int>();
const auto targetHeight = _targetSize.height<int>();
// If the text in the font is not perfectly centered, the _centeringHint
// gives us the offset needed to correct that misalignment. So to ensure
// that any inserted or deleted columns are evenly spaced around the center
// point of the glyphs, we need to adjust the source and target widths by
// that amount (proportionally) before calculating the scaling increments.
targetWidth -= std::lround((double)_centeringHint * targetWidth / sourceWidth);
sourceWidth -= gsl::narrow_cast<int>(_centeringHint);
// The way the scaling works is by iterating over the target range, and
// calculating the source offsets that correspond to each target position.
// We achieve that by incrementing the source offset every iteration by an
// integer value that is the quotient of the source and target dimensions.
// Because this is an integer division, we're going to be off by a certain
// fraction on each iteration, so we need to keep track of that accumulated
// error using the modulus of the division. Once the error total exceeds
// the target dimension (more or less), we add another pixel to compensate
// for the error, and reset the error total.
const auto createIncrementFunction = [](const auto sourceDimension, const auto targetDimension) {
const auto increment = sourceDimension / targetDimension;
const auto errorIncrement = sourceDimension % targetDimension * 2;
const auto errorThreshold = targetDimension * 2 - std::min(sourceDimension, targetDimension);
const auto errorReset = targetDimension * 2;
return [=](auto& errorTotal) {
errorTotal += errorIncrement;
if (errorTotal > errorThreshold)
{
errorTotal -= errorReset;
return increment + 1;
}
return increment;
};
};
const auto columnIncrement = createIncrementFunction(sourceWidth, targetWidth);
const auto lineIncrement = createIncrementFunction(sourceHeight, targetHeight);
// Once we've calculated the scaling increments, taking the centering hint
// into account, we reset the target width back to its original value.
targetWidth = _targetSize.width<int>();
auto targetBufferPointer = targetBuffer.begin();
for (auto ch = 0; ch < CHAR_COUNT; ch++)
{
// Bits are read from the source from left to right - MSB to LSB. The source
// column is a single bit representing the 1-based position. The reason for
// this will become clear in the mask calculation below.
auto sourceColumn = 1 << 16;
auto sourceColumnError = 0;
// The target format expects the character bitmaps to be laid out in columns
// of 8 bits. So we generate 8 bits from each scanline until we've covered
// the full target height. Then we start again from the top with the next 8
// bits of the line, until we've covered the full target width.
for (auto targetX = 0; targetX < targetWidth; targetX += 8)
{
auto sourceLine = std::next(_bitPattern.begin(), ch * sourceHeight);
auto sourceLineError = 0;
// Since we're going to be reading from the same horizontal offset for each
// target line, we save the state here so we can reset it every iteration.
const auto initialSourceColumn = sourceColumn;
const auto initialSourceColumnError = sourceColumnError;
for (auto targetY = 0; targetY < targetHeight; targetY++)
{
sourceColumn = initialSourceColumn;
sourceColumnError = initialSourceColumnError;
// For a particular target line, we calculate the span of source lines from
// which it is derived, then OR those values together. We don't want the
// source value to be zero, though, so we must read at least one line.
const auto lineSpan = lineIncrement(sourceLineError);
auto sourceValue = 0;
for (auto i = 0; i < std::max(lineSpan, 1); i++)
{
sourceValue |= sourceLine[i];
}
std::advance(sourceLine, lineSpan);
// From the combined value of the source lines, we now need to extract eight
// bits to make up the next byte in the target at the current X offset.
byte targetValue = 0;
for (auto targetBit = 0; targetBit < 8; targetBit++)
{
targetValue <<= 1;
if (targetX + targetBit < targetWidth)
{
// As with the line iteration, we first need to calculate the span of source
// columns from which the target bit is derived. We shift our source column
// position right by that amount to determine the next column position, then
// subtract those two values to obtain a mask. For example, if we're reading
// from columns 6 to 3 (exclusively), the initial column position is 1<<6,
// the next column position is 1<<3, so the mask is 64-8=56, or 00111000.
// Again we don't want this mask to be zero, so if the span is zero, we need
// to shift an additional bit to make sure we cover at least one column.
const auto columnSpan = columnIncrement(sourceColumnError);
const auto nextSourceColumn = sourceColumn >> columnSpan;
const auto sourceMask = sourceColumn - (nextSourceColumn >> (columnSpan ? 0 : 1));
sourceColumn = nextSourceColumn;
targetValue |= (sourceValue & sourceMask) ? 1 : 0;
}
}
*(targetBufferPointer++) = targetValue;
}
}
}
}