mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-10 00:48:23 -06:00
Add support for Sixel images in conhost (#17421)
## Summary of the Pull Request This PR introduces basic support for the Sixel graphics protocol in conhost, limited to the GDI renderer. ## References and Relevant Issues This is a first step towards supporting Sixel graphics in Windows Terminal (#448), but that will first require us to have some form of ConPTY passthrough (#1173). ## Detailed Description of the Pull Request / Additional comments There are three main parts to the architecture: * The `SixelParser` class takes care of parsing the incoming Sixel `DCS` sequence. * The resulting image content is stored in the text buffer in a series of `ImageSlice` objects, which represent per-row image content. * The renderer then takes care of painting those image slices for each affected row. The parser is designed to support multiple conformance levels so we can one day provide strict compatibility with the original DEC hardware. But for now the default behavior is intended to work with more modern Sixel applications. This is essentially the equivalent of a VT340 with 256 colors, so it should still work reasonably well as a VT340 emulator too. ## Validation Steps Performed Thanks to the work of @hackerb9, who has done extensive testing on a real VT340, we now have a fairly good understanding of how the original Sixel hardware terminals worked, and I've tried to make sure that our implementation matches that behavior as closely as possible. I've also done some testing with modern Sixel libraries like notcurses and jexer, but those typically rely on the terminal implementing certain proprietary Xterm query sequences which I haven't included in this PR. --------- Co-authored-by: Dustin L. Howett <dustin@howett.net>
This commit is contained in:
parent
6589957d4d
commit
236c0030f1
12
.github/actions/spelling/expect/expect.txt
vendored
12
.github/actions/spelling/expect/expect.txt
vendored
@ -116,6 +116,8 @@ binplaced
|
||||
binskim
|
||||
bitcoin
|
||||
bitcrazed
|
||||
BITMAPINFO
|
||||
BITMAPINFOHEADER
|
||||
bitmasks
|
||||
BITOPERATION
|
||||
BKCOLOR
|
||||
@ -123,6 +125,7 @@ BKGND
|
||||
Bksp
|
||||
Blt
|
||||
BLUESCROLL
|
||||
bmi
|
||||
BODGY
|
||||
BOLDFONT
|
||||
Borland
|
||||
@ -395,6 +398,11 @@ DECERA
|
||||
DECFI
|
||||
DECFNK
|
||||
DECFRA
|
||||
DECGCI
|
||||
DECGCR
|
||||
DECGNL
|
||||
DECGRA
|
||||
DECGRI
|
||||
DECIC
|
||||
DECID
|
||||
DECINVM
|
||||
@ -431,6 +439,7 @@ DECSCA
|
||||
DECSCNM
|
||||
DECSCPP
|
||||
DECSCUSR
|
||||
DECSDM
|
||||
DECSED
|
||||
DECSEL
|
||||
DECSERA
|
||||
@ -1514,6 +1523,7 @@ rfa
|
||||
rfid
|
||||
rftp
|
||||
rgbi
|
||||
RGBQUAD
|
||||
rgbs
|
||||
rgci
|
||||
rgfae
|
||||
@ -1678,9 +1688,11 @@ SOLIDBOX
|
||||
Solutiondir
|
||||
somefile
|
||||
sourced
|
||||
SRCAND
|
||||
SRCCODEPAGE
|
||||
SRCCOPY
|
||||
SRCINVERT
|
||||
SRCPAINT
|
||||
srcsrv
|
||||
SRCSRVTRG
|
||||
srctool
|
||||
|
||||
245
src/buffer/out/ImageSlice.cpp
Normal file
245
src/buffer/out/ImageSlice.cpp
Normal file
@ -0,0 +1,245 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "precomp.h"
|
||||
|
||||
#include "ImageSlice.hpp"
|
||||
#include "Row.hpp"
|
||||
#include "textBuffer.hpp"
|
||||
|
||||
ImageSlice::ImageSlice(const til::size cellSize) noexcept :
|
||||
_cellSize{ cellSize }
|
||||
{
|
||||
}
|
||||
|
||||
til::size ImageSlice::CellSize() const noexcept
|
||||
{
|
||||
return _cellSize;
|
||||
}
|
||||
|
||||
til::CoordType ImageSlice::ColumnOffset() const noexcept
|
||||
{
|
||||
return _columnBegin;
|
||||
}
|
||||
|
||||
til::CoordType ImageSlice::PixelWidth() const noexcept
|
||||
{
|
||||
return _pixelWidth;
|
||||
}
|
||||
|
||||
std::span<const RGBQUAD> ImageSlice::Pixels() const noexcept
|
||||
{
|
||||
return _pixelBuffer;
|
||||
}
|
||||
|
||||
const RGBQUAD* ImageSlice::Pixels(const til::CoordType columnBegin) const noexcept
|
||||
{
|
||||
const auto pixelOffset = (columnBegin - _columnBegin) * _cellSize.width;
|
||||
return &til::at(_pixelBuffer, pixelOffset);
|
||||
}
|
||||
|
||||
RGBQUAD* ImageSlice::MutablePixels(const til::CoordType columnBegin, const til::CoordType columnEnd)
|
||||
{
|
||||
// IF the buffer is empty or isn't large enough for the requested range, we'll need to resize it.
|
||||
if (_pixelBuffer.empty() || columnBegin < _columnBegin || columnEnd > _columnEnd)
|
||||
{
|
||||
const auto oldColumnBegin = _columnBegin;
|
||||
const auto oldPixelWidth = _pixelWidth;
|
||||
const auto existingData = !_pixelBuffer.empty();
|
||||
_columnBegin = existingData ? std::min(_columnBegin, columnBegin) : columnBegin;
|
||||
_columnEnd = existingData ? std::max(_columnEnd, columnEnd) : columnEnd;
|
||||
_pixelWidth = (_columnEnd - _columnBegin) * _cellSize.width;
|
||||
_pixelWidth = (_pixelWidth + 3) & ~3; // Renderer needs this as a multiple of 4
|
||||
const auto bufferSize = _pixelWidth * _cellSize.height;
|
||||
if (existingData)
|
||||
{
|
||||
// If there is existing data in the buffer, we need to copy it
|
||||
// across to the appropriate position in the new buffer.
|
||||
auto newPixelBuffer = std::vector<RGBQUAD>(bufferSize);
|
||||
const auto newPixelOffset = (oldColumnBegin - _columnBegin) * _cellSize.width;
|
||||
auto newIterator = std::next(newPixelBuffer.data(), newPixelOffset);
|
||||
auto oldIterator = _pixelBuffer.data();
|
||||
// Because widths are rounded up to multiples of 4, it's possible
|
||||
// that the old width will extend past the right border of the new
|
||||
// buffer, so the range that we copy must be clamped to fit.
|
||||
const auto newPixelRange = std::min(oldPixelWidth, _pixelWidth - newPixelOffset);
|
||||
for (auto i = 0; i < _cellSize.height; i++)
|
||||
{
|
||||
std::memcpy(newIterator, oldIterator, newPixelRange * sizeof(RGBQUAD));
|
||||
std::advance(oldIterator, oldPixelWidth);
|
||||
std::advance(newIterator, _pixelWidth);
|
||||
}
|
||||
_pixelBuffer = std::move(newPixelBuffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise we just initialize the buffer to the correct size.
|
||||
_pixelBuffer.resize(bufferSize);
|
||||
}
|
||||
}
|
||||
const auto pixelOffset = (columnBegin - _columnBegin) * _cellSize.width;
|
||||
return &til::at(_pixelBuffer, pixelOffset);
|
||||
}
|
||||
|
||||
void ImageSlice::CopyBlock(const TextBuffer& srcBuffer, const til::rect srcRect, TextBuffer& dstBuffer, const til::rect dstRect)
|
||||
{
|
||||
// If the top of the source is less than the top of the destination, we copy
|
||||
// the rows from the bottom upwards, to avoid the possibility of the source
|
||||
// being overwritten if it were to overlap the destination range.
|
||||
if (srcRect.top < dstRect.top)
|
||||
{
|
||||
for (auto y = srcRect.height(); y-- > 0;)
|
||||
{
|
||||
const auto& srcRow = srcBuffer.GetRowByOffset(srcRect.top + y);
|
||||
auto& dstRow = dstBuffer.GetMutableRowByOffset(dstRect.top + y);
|
||||
CopyCells(srcRow, srcRect.left, dstRow, dstRect.left, dstRect.right);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto y = 0; y < srcRect.height(); y++)
|
||||
{
|
||||
const auto& srcRow = srcBuffer.GetRowByOffset(srcRect.top + y);
|
||||
auto& dstRow = dstBuffer.GetMutableRowByOffset(dstRect.top + y);
|
||||
CopyCells(srcRow, srcRect.left, dstRow, dstRect.left, dstRect.right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ImageSlice::CopyRow(const ROW& srcRow, ROW& dstRow)
|
||||
{
|
||||
const auto& srcSlice = srcRow.GetImageSlice();
|
||||
auto& dstSlice = dstRow.GetMutableImageSlice();
|
||||
dstSlice = srcSlice ? std::make_unique<ImageSlice>(*srcSlice) : nullptr;
|
||||
}
|
||||
|
||||
void ImageSlice::CopyCells(const ROW& srcRow, const til::CoordType srcColumn, ROW& dstRow, const til::CoordType dstColumnBegin, const til::CoordType dstColumnEnd)
|
||||
{
|
||||
// If there's no image content in the source row, we're essentially copying
|
||||
// a blank image into the destination, which is the same thing as an erase.
|
||||
// Also if the line renditions are different, there's no meaningful way to
|
||||
// copy the image content, so we also just treat that as an erase.
|
||||
const auto& srcSlice = srcRow.GetImageSlice();
|
||||
if (!srcSlice || srcRow.GetLineRendition() != dstRow.GetLineRendition()) [[likely]]
|
||||
{
|
||||
ImageSlice::EraseCells(dstRow, dstColumnBegin, dstColumnEnd);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto& dstSlice = dstRow.GetMutableImageSlice();
|
||||
if (!dstSlice)
|
||||
{
|
||||
dstSlice = std::make_unique<ImageSlice>(srcSlice->CellSize());
|
||||
}
|
||||
const auto scale = srcRow.GetLineRendition() != LineRendition::SingleWidth ? 1 : 0;
|
||||
if (dstSlice->_copyCells(*srcSlice, srcColumn << scale, dstColumnBegin << scale, dstColumnEnd << scale))
|
||||
{
|
||||
// If _copyCells returns true, that means the destination was
|
||||
// completely erased, so we can delete this slice.
|
||||
dstSlice = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ImageSlice::_copyCells(const ImageSlice& srcSlice, const til::CoordType srcColumn, const til::CoordType dstColumnBegin, const til::CoordType dstColumnEnd)
|
||||
{
|
||||
const auto srcColumnEnd = srcColumn + dstColumnEnd - dstColumnBegin;
|
||||
|
||||
// First we determine the portions of the copy range that are currently in use.
|
||||
const auto srcUsedBegin = std::max(srcColumn, srcSlice._columnBegin);
|
||||
const auto srcUsedEnd = std::max(std::min(srcColumnEnd, srcSlice._columnEnd), srcUsedBegin);
|
||||
const auto dstUsedBegin = std::max(dstColumnBegin, _columnBegin);
|
||||
const auto dstUsedEnd = std::max(std::min(dstColumnEnd, _columnEnd), dstUsedBegin);
|
||||
|
||||
// The used source projected into the destination is the range we must overwrite.
|
||||
const auto projectedOffset = dstColumnBegin - srcColumn;
|
||||
const auto dstWriteBegin = srcUsedBegin + projectedOffset;
|
||||
const auto dstWriteEnd = srcUsedEnd + projectedOffset;
|
||||
|
||||
if (dstWriteBegin < dstWriteEnd)
|
||||
{
|
||||
auto dstIterator = MutablePixels(dstWriteBegin, dstWriteEnd);
|
||||
auto srcIterator = srcSlice.Pixels(srcUsedBegin);
|
||||
const auto writeCellCount = dstWriteEnd - dstWriteBegin;
|
||||
const auto writeByteCount = sizeof(RGBQUAD) * writeCellCount * _cellSize.width;
|
||||
for (auto y = 0; y < _cellSize.height; y++)
|
||||
{
|
||||
std::memmove(dstIterator, srcIterator, writeByteCount);
|
||||
std::advance(srcIterator, srcSlice._pixelWidth);
|
||||
std::advance(dstIterator, _pixelWidth);
|
||||
}
|
||||
}
|
||||
|
||||
// The used destination before and after the written area must be erased.
|
||||
if (dstUsedBegin < dstWriteBegin)
|
||||
{
|
||||
_eraseCells(dstUsedBegin, dstWriteBegin);
|
||||
}
|
||||
if (dstUsedEnd > dstWriteEnd)
|
||||
{
|
||||
_eraseCells(dstWriteEnd, dstUsedEnd);
|
||||
}
|
||||
|
||||
// If the beginning column is now not less than the end, that means the
|
||||
// content has been entirely erased, so we return true to let the caller
|
||||
// know that the slice should be deleted.
|
||||
return _columnBegin >= _columnEnd;
|
||||
}
|
||||
|
||||
void ImageSlice::EraseBlock(TextBuffer& buffer, const til::rect rect)
|
||||
{
|
||||
for (auto y = rect.top; y < rect.bottom; y++)
|
||||
{
|
||||
auto& row = buffer.GetMutableRowByOffset(y);
|
||||
EraseCells(row, rect.left, rect.right);
|
||||
}
|
||||
}
|
||||
|
||||
void ImageSlice::EraseCells(TextBuffer& buffer, const til::point at, const size_t distance)
|
||||
{
|
||||
auto& row = buffer.GetMutableRowByOffset(at.y);
|
||||
EraseCells(row, at.x, gsl::narrow_cast<til::CoordType>(at.x + distance));
|
||||
}
|
||||
|
||||
void ImageSlice::EraseCells(ROW& row, const til::CoordType columnBegin, const til::CoordType columnEnd)
|
||||
{
|
||||
auto& imageSlice = row.GetMutableImageSlice();
|
||||
if (imageSlice) [[unlikely]]
|
||||
{
|
||||
const auto scale = row.GetLineRendition() != LineRendition::SingleWidth ? 1 : 0;
|
||||
if (imageSlice->_eraseCells(columnBegin << scale, columnEnd << scale))
|
||||
{
|
||||
// If _eraseCells returns true, that means the image was
|
||||
// completely erased, so we can delete this slice.
|
||||
imageSlice = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ImageSlice::_eraseCells(const til::CoordType columnBegin, const til::CoordType columnEnd)
|
||||
{
|
||||
if (columnBegin <= _columnBegin && columnEnd >= _columnEnd)
|
||||
{
|
||||
// If we're erasing the entire range that's in use, we return true to
|
||||
// indicate that there is now nothing left. We don't bother altering
|
||||
// the buffer because the caller is now expected to delete this slice.
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto eraseBegin = std::max(columnBegin, _columnBegin);
|
||||
const auto eraseEnd = std::min(columnEnd, _columnEnd);
|
||||
if (eraseBegin < eraseEnd)
|
||||
{
|
||||
const auto eraseOffset = (eraseBegin - _columnBegin) * _cellSize.width;
|
||||
const auto eraseLength = (eraseEnd - eraseBegin) * _cellSize.width;
|
||||
auto eraseIterator = std::next(_pixelBuffer.data(), eraseOffset);
|
||||
for (auto y = 0; y < _cellSize.height; y++)
|
||||
{
|
||||
std::memset(eraseIterator, 0, eraseLength * sizeof(RGBQUAD));
|
||||
std::advance(eraseIterator, _pixelWidth);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
53
src/buffer/out/ImageSlice.hpp
Normal file
53
src/buffer/out/ImageSlice.hpp
Normal file
@ -0,0 +1,53 @@
|
||||
/*++
|
||||
Copyright (c) Microsoft Corporation
|
||||
Licensed under the MIT license.
|
||||
|
||||
Module Name:
|
||||
- ImageSlice.hpp
|
||||
|
||||
Abstract:
|
||||
- This serves as a structure to represent a slice of an image covering one textbuffer row.
|
||||
--*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "til.h"
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
class ROW;
|
||||
class TextBuffer;
|
||||
|
||||
class ImageSlice
|
||||
{
|
||||
public:
|
||||
using Pointer = std::unique_ptr<ImageSlice>;
|
||||
|
||||
ImageSlice(const ImageSlice& rhs) = default;
|
||||
ImageSlice(const til::size cellSize) noexcept;
|
||||
|
||||
til::size CellSize() const noexcept;
|
||||
til::CoordType ColumnOffset() const noexcept;
|
||||
til::CoordType PixelWidth() const noexcept;
|
||||
|
||||
std::span<const RGBQUAD> Pixels() const noexcept;
|
||||
const RGBQUAD* Pixels(const til::CoordType columnBegin) const noexcept;
|
||||
RGBQUAD* MutablePixels(const til::CoordType columnBegin, const til::CoordType columnEnd);
|
||||
|
||||
static void CopyBlock(const TextBuffer& srcBuffer, const til::rect srcRect, TextBuffer& dstBuffer, const til::rect dstRect);
|
||||
static void CopyRow(const ROW& srcRow, ROW& dstRow);
|
||||
static void CopyCells(const ROW& srcRow, const til::CoordType srcColumn, ROW& dstRow, const til::CoordType dstColumnBegin, const til::CoordType dstColumnEnd);
|
||||
static void EraseBlock(TextBuffer& buffer, const til::rect rect);
|
||||
static void EraseCells(TextBuffer& buffer, const til::point at, const size_t distance);
|
||||
static void EraseCells(ROW& row, const til::CoordType columnBegin, const til::CoordType columnEnd);
|
||||
|
||||
private:
|
||||
bool _copyCells(const ImageSlice& srcSlice, const til::CoordType srcColumn, const til::CoordType dstColumnBegin, const til::CoordType dstColumnEnd);
|
||||
bool _eraseCells(const til::CoordType columnBegin, const til::CoordType columnEnd);
|
||||
|
||||
til::size _cellSize;
|
||||
std::vector<RGBQUAD> _pixelBuffer;
|
||||
til::CoordType _columnBegin = 0;
|
||||
til::CoordType _columnEnd = 0;
|
||||
til::CoordType _pixelWidth = 0;
|
||||
};
|
||||
@ -225,6 +225,7 @@ void ROW::Reset(const TextAttribute& attr) noexcept
|
||||
// Constructing and then moving objects into place isn't free.
|
||||
// Modifying the existing object is _much_ faster.
|
||||
*_attr.runs().unsafe_shrink_to_size(1) = til::rle_pair{ attr, _columnCount };
|
||||
_imageSlice = nullptr;
|
||||
_lineRendition = LineRendition::SingleWidth;
|
||||
_wrapForced = false;
|
||||
_doubleBytePadded = false;
|
||||
@ -964,6 +965,16 @@ std::vector<uint16_t> ROW::GetHyperlinks() const
|
||||
return ids;
|
||||
}
|
||||
|
||||
const ImageSlice::Pointer& ROW::GetImageSlice() const noexcept
|
||||
{
|
||||
return _imageSlice;
|
||||
}
|
||||
|
||||
ImageSlice::Pointer& ROW::GetMutableImageSlice() noexcept
|
||||
{
|
||||
return _imageSlice;
|
||||
}
|
||||
|
||||
uint16_t ROW::size() const noexcept
|
||||
{
|
||||
return _columnCount;
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
|
||||
#include <til/rle.h>
|
||||
|
||||
#include "ImageSlice.hpp"
|
||||
#include "LineRendition.hpp"
|
||||
#include "OutputCell.hpp"
|
||||
#include "OutputCellIterator.hpp"
|
||||
@ -151,6 +152,8 @@ public:
|
||||
const til::small_rle<TextAttribute, uint16_t, 1>& Attributes() const noexcept;
|
||||
TextAttribute GetAttrByColumn(til::CoordType column) const;
|
||||
std::vector<uint16_t> GetHyperlinks() const;
|
||||
const ImageSlice::Pointer& GetImageSlice() const noexcept;
|
||||
ImageSlice::Pointer& GetMutableImageSlice() noexcept;
|
||||
uint16_t size() const noexcept;
|
||||
til::CoordType GetLastNonSpaceColumn() const noexcept;
|
||||
til::CoordType MeasureLeft() const noexcept;
|
||||
@ -296,6 +299,8 @@ private:
|
||||
til::small_rle<TextAttribute, uint16_t, 1> _attr;
|
||||
// The width of the row in visual columns.
|
||||
uint16_t _columnCount = 0;
|
||||
// Stores any image content covering the row.
|
||||
ImageSlice::Pointer _imageSlice;
|
||||
// Stores double-width/height (DECSWL/DECDWL/DECDHL) attributes.
|
||||
LineRendition _lineRendition = LineRendition::SingleWidth;
|
||||
// Occurs when the user runs out of text in a given row and we're forced to wrap the cursor to the next line
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
<Import Project="$(SolutionDir)src\common.nugetversions.props" />
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\cursor.cpp" />
|
||||
<ClCompile Include="..\ImageSlice.cpp" />
|
||||
<ClCompile Include="..\OutputCell.cpp" />
|
||||
<ClCompile Include="..\OutputCellIterator.cpp" />
|
||||
<ClCompile Include="..\OutputCellRect.cpp" />
|
||||
@ -32,6 +33,7 @@
|
||||
<ClInclude Include="..\cursor.h" />
|
||||
<ClInclude Include="..\DbcsAttribute.hpp" />
|
||||
<ClInclude Include="..\ICharRow.hpp" />
|
||||
<ClInclude Include="..\ImageSlice.hpp" />
|
||||
<ClInclude Include="..\LineRendition.hpp" />
|
||||
<ClInclude Include="..\OutputCell.hpp" />
|
||||
<ClInclude Include="..\OutputCellIterator.hpp" />
|
||||
|
||||
@ -30,6 +30,7 @@ PRECOMPILED_INCLUDE = ..\precomp.h
|
||||
|
||||
SOURCES= \
|
||||
..\cursor.cpp \
|
||||
..\ImageSlice.cpp \
|
||||
..\OutputCell.cpp \
|
||||
..\OutputCellIterator.cpp \
|
||||
..\OutputCellRect.cpp \
|
||||
|
||||
@ -529,6 +529,7 @@ void TextBuffer::Replace(til::CoordType row, const TextAttribute& attributes, Ro
|
||||
auto& r = GetMutableRowByOffset(row);
|
||||
r.ReplaceText(state);
|
||||
r.ReplaceAttributes(state.columnBegin, state.columnEnd, attributes);
|
||||
ImageSlice::EraseCells(r, state.columnBegin, state.columnEnd);
|
||||
TriggerRedraw(Viewport::FromExclusive({ state.columnBeginDirty, row, state.columnEndDirty, row + 1 }));
|
||||
}
|
||||
|
||||
@ -557,7 +558,11 @@ void TextBuffer::Insert(til::CoordType row, const TextAttribute& attributes, Row
|
||||
const auto& scratchAttr = scratch.Attributes();
|
||||
const auto restoreAttr = scratchAttr.slice(gsl::narrow<uint16_t>(state.columnBegin), gsl::narrow<uint16_t>(state.columnBegin + copyAmount));
|
||||
rowAttr.replace(gsl::narrow<uint16_t>(restoreState.columnBegin), gsl::narrow<uint16_t>(restoreState.columnEnd), restoreAttr);
|
||||
// If there is any image content, that needs to be copied too.
|
||||
ImageSlice::CopyCells(r, state.columnBegin, r, restoreState.columnBegin, restoreState.columnEnd);
|
||||
}
|
||||
// Image content at the insert position needs to be erased.
|
||||
ImageSlice::EraseCells(r, state.columnBegin, restoreState.columnBegin);
|
||||
|
||||
TriggerRedraw(Viewport::FromExclusive({ state.columnBeginDirty, row, restoreState.columnEndDirty, row + 1 }));
|
||||
}
|
||||
@ -612,6 +617,7 @@ void TextBuffer::FillRect(const til::rect& rect, const std::wstring_view& fill,
|
||||
auto& r = GetMutableRowByOffset(y);
|
||||
r.CopyTextFrom(state);
|
||||
r.ReplaceAttributes(rect.left, rect.right, attributes);
|
||||
ImageSlice::EraseCells(r, rect.left, rect.right);
|
||||
TriggerRedraw(Viewport::FromExclusive({ state.columnBeginDirty, y, state.columnEndDirty, y + 1 }));
|
||||
}
|
||||
}
|
||||
@ -858,10 +864,18 @@ void TextBuffer::ScrollRows(const til::CoordType firstRow, til::CoordType size,
|
||||
|
||||
for (; y != end; y += step)
|
||||
{
|
||||
GetMutableRowByOffset(y + delta).CopyFrom(GetRowByOffset(y));
|
||||
CopyRow(y, y + delta, *this);
|
||||
}
|
||||
}
|
||||
|
||||
void TextBuffer::CopyRow(const til::CoordType srcRowIndex, const til::CoordType dstRowIndex, TextBuffer& dstBuffer) const
|
||||
{
|
||||
auto& dstRow = dstBuffer.GetMutableRowByOffset(dstRowIndex);
|
||||
const auto& srcRow = GetRowByOffset(srcRowIndex);
|
||||
dstRow.CopyFrom(srcRow);
|
||||
ImageSlice::CopyRow(srcRow, dstRow);
|
||||
}
|
||||
|
||||
Cursor& TextBuffer::GetCursor() noexcept
|
||||
{
|
||||
return _cursor;
|
||||
@ -902,6 +916,8 @@ void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition, cons
|
||||
row.SetLineRendition(lineRendition);
|
||||
// If the line rendition has changed, the row can no longer be wrapped.
|
||||
row.SetWrapForced(false);
|
||||
// And all image content on the row is removed.
|
||||
row.GetMutableImageSlice().reset();
|
||||
// And if it's no longer single width, the right half of the row should be erased.
|
||||
if (lineRendition != LineRendition::SingleWidth)
|
||||
{
|
||||
@ -1029,7 +1045,7 @@ void TextBuffer::ResizeTraditional(til::size newSize)
|
||||
|
||||
for (; dstRow < copyableRows; ++dstRow, ++srcRow)
|
||||
{
|
||||
newBuffer.GetMutableRowByOffset(dstRow).CopyFrom(GetRowByOffset(srcRow));
|
||||
CopyRow(srcRow, dstRow, newBuffer);
|
||||
}
|
||||
|
||||
// NOTE: Keep this in sync with _reserve().
|
||||
@ -2790,6 +2806,12 @@ void TextBuffer::Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer, const View
|
||||
};
|
||||
newRow.CopyTextFrom(state);
|
||||
|
||||
// If we're at the start of the old row, copy its image content.
|
||||
if (oldX == 0)
|
||||
{
|
||||
ImageSlice::CopyRow(oldRow, newRow);
|
||||
}
|
||||
|
||||
const auto& oldAttr = oldRow.Attributes();
|
||||
auto& newAttr = newRow.Attributes();
|
||||
const auto attributes = oldAttr.slice(gsl::narrow_cast<uint16_t>(oldX), oldAttr.size());
|
||||
|
||||
@ -135,6 +135,7 @@ public:
|
||||
const Microsoft::Console::Types::Viewport GetSize() const noexcept;
|
||||
|
||||
void ScrollRows(const til::CoordType firstRow, const til::CoordType size, const til::CoordType delta);
|
||||
void CopyRow(const til::CoordType srcRow, const til::CoordType dstRow, TextBuffer& dstBuffer) const;
|
||||
|
||||
til::CoordType TotalRowCount() const noexcept;
|
||||
|
||||
|
||||
@ -130,6 +130,8 @@ void WriteToScreen(SCREEN_INFORMATION& screenInfo, const Viewport& region)
|
||||
OutputCellIterator it(chars);
|
||||
const auto finished = screenInfo.Write(it, target);
|
||||
used = finished.GetInputDistance(it);
|
||||
// If we've overwritten image content, it needs to be erased.
|
||||
ImageSlice::EraseCells(screenInfo.GetTextBuffer(), target, used);
|
||||
}
|
||||
CATCH_RETURN();
|
||||
|
||||
@ -283,6 +285,9 @@ void WriteToScreen(SCREEN_INFORMATION& screenInfo, const Viewport& region)
|
||||
|
||||
cellsModified = cellsModifiedCoord;
|
||||
|
||||
// If we've overwritten image content, it needs to be erased.
|
||||
ImageSlice::EraseCells(screenInfo.GetTextBuffer(), startingCoordinate, cellsModified);
|
||||
|
||||
// Notify accessibility
|
||||
if (screenInfo.HasAccessibilityEventing())
|
||||
{
|
||||
|
||||
@ -691,6 +691,9 @@ CATCH_RETURN();
|
||||
storageBuffer.Write(it, target);
|
||||
}
|
||||
|
||||
// If we've overwritten image content, it needs to be erased.
|
||||
ImageSlice::EraseBlock(storageBuffer.GetTextBuffer(), writeRectangle.ToExclusive());
|
||||
|
||||
// Since we've managed to write part of the request, return the clamped part that we actually used.
|
||||
writtenRectangle = writeRectangle;
|
||||
|
||||
|
||||
@ -113,6 +113,9 @@ static void _CopyRectangle(SCREEN_INFORMATION& screenInfo,
|
||||
next = OutputCell(*screenInfo.GetCellDataAt(sourcePos));
|
||||
screenInfo.GetTextBuffer().WriteLine(OutputCellIterator({ ¤t, 1 }), targetPos);
|
||||
} while (target.WalkInBounds(targetPos, walkDirection));
|
||||
|
||||
auto& textBuffer = screenInfo.GetTextBuffer();
|
||||
ImageSlice::CopyBlock(textBuffer, source.ToExclusive(), textBuffer, target.ToExclusive());
|
||||
}
|
||||
}
|
||||
|
||||
@ -426,6 +429,9 @@ void ScrollRegion(SCREEN_INFORMATION& screenInfo,
|
||||
const auto& view = remaining.at(i);
|
||||
screenInfo.WriteRect(fillData, view);
|
||||
|
||||
// If the region has image content it needs to be erased.
|
||||
ImageSlice::EraseBlock(screenInfo.GetTextBuffer(), view.ToExclusive());
|
||||
|
||||
// If we're scrolling an area that encompasses the full buffer width,
|
||||
// then the filled rows should also have their line rendition reset.
|
||||
if (view.Width() == buffer.Width() && destinationOriginGiven.x == 0)
|
||||
|
||||
@ -520,6 +520,11 @@ try
|
||||
}
|
||||
CATCH_RETURN()
|
||||
|
||||
[[nodiscard]] HRESULT AtlasEngine::PaintImageSlice(const ImageSlice& /*imageSlice*/, const til::CoordType /*targetRow*/, const til::CoordType /*viewportLeft*/) noexcept
|
||||
{
|
||||
return S_FALSE;
|
||||
}
|
||||
|
||||
[[nodiscard]] HRESULT AtlasEngine::PaintSelection(const til::rect& rect) noexcept
|
||||
try
|
||||
{
|
||||
|
||||
@ -45,6 +45,7 @@ namespace Microsoft::Console::Render::Atlas
|
||||
[[nodiscard]] HRESULT PaintBackground() noexcept override;
|
||||
[[nodiscard]] HRESULT PaintBufferLine(std::span<const Cluster> clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept override;
|
||||
[[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override;
|
||||
[[nodiscard]] HRESULT PaintImageSlice(const ImageSlice& imageSlice, til::CoordType targetRow, til::CoordType viewportLeft) noexcept override;
|
||||
[[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override;
|
||||
[[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override;
|
||||
[[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const RenderSettings& renderSettings, gsl::not_null<IRenderData*> pData, bool usingSoftFont, bool isSettingDefaultBrushes) noexcept override;
|
||||
|
||||
@ -64,6 +64,13 @@ HRESULT RenderEngineBase::PrepareLineTransform(const LineRendition /*lineRenditi
|
||||
return S_FALSE;
|
||||
}
|
||||
|
||||
HRESULT RenderEngineBase::PaintImageSlice(const ImageSlice& /*imageSlice*/,
|
||||
const til::CoordType /*targetRow*/,
|
||||
const til::CoordType /*viewportLeft*/) noexcept
|
||||
{
|
||||
return S_FALSE;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - By default, no one should need continuous redraw. It ruins performance
|
||||
// in terms of CPU, memory, and battery life to just paint forever.
|
||||
|
||||
@ -835,6 +835,13 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine)
|
||||
|
||||
// Ask the helper to paint through this specific line.
|
||||
_PaintBufferOutputHelper(pEngine, it, screenPosition, lineWrapped);
|
||||
|
||||
// Paint any image content on top of the text.
|
||||
const auto& imageSlice = buffer.GetRowByOffset(row).GetImageSlice();
|
||||
if (imageSlice) [[unlikely]]
|
||||
{
|
||||
LOG_IF_FAILED(pEngine->PaintImageSlice(*imageSlice, screenPosition.y, view.Left()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,6 +56,9 @@ namespace Microsoft::Console::Render
|
||||
const COLORREF underlineColor,
|
||||
const size_t cchLine,
|
||||
const til::point coordTarget) noexcept override;
|
||||
[[nodiscard]] HRESULT PaintImageSlice(const ImageSlice& imageSlice,
|
||||
const til::CoordType targetRow,
|
||||
const til::CoordType viewportLeft) noexcept override;
|
||||
[[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override;
|
||||
|
||||
[[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override;
|
||||
@ -165,6 +168,8 @@ namespace Microsoft::Console::Render
|
||||
std::pmr::vector<std::pmr::wstring> _polyStrings;
|
||||
std::pmr::vector<std::pmr::basic_string<int>> _polyWidths;
|
||||
|
||||
std::vector<DWORD> _imageMask;
|
||||
|
||||
[[nodiscard]] HRESULT _InvalidCombine(const til::rect* const prc) noexcept;
|
||||
[[nodiscard]] HRESULT _InvalidOffset(const til::point* const ppt) noexcept;
|
||||
[[nodiscard]] HRESULT _InvalidRestrict() noexcept;
|
||||
|
||||
@ -666,6 +666,64 @@ try
|
||||
}
|
||||
CATCH_RETURN();
|
||||
|
||||
[[nodiscard]] HRESULT GdiEngine::PaintImageSlice(const ImageSlice& imageSlice,
|
||||
const til::CoordType targetRow,
|
||||
const til::CoordType viewportLeft) noexcept
|
||||
try
|
||||
{
|
||||
LOG_IF_FAILED(_FlushBufferLines());
|
||||
LOG_IF_FAILED(ResetLineTransform());
|
||||
|
||||
const auto& imagePixels = imageSlice.Pixels();
|
||||
if (_imageMask.size() < imagePixels.size())
|
||||
{
|
||||
_imageMask.resize(imagePixels.size());
|
||||
}
|
||||
|
||||
const auto srcCellSize = imageSlice.CellSize();
|
||||
const auto dstCellSize = _GetFontSize();
|
||||
const auto srcWidth = imageSlice.PixelWidth();
|
||||
const auto srcHeight = srcCellSize.height;
|
||||
const auto dstWidth = srcWidth * dstCellSize.width / srcCellSize.width;
|
||||
const auto dstHeight = srcHeight * dstCellSize.height / srcCellSize.height;
|
||||
const auto x = (imageSlice.ColumnOffset() - viewportLeft) * dstCellSize.width;
|
||||
const auto y = targetRow * dstCellSize.height;
|
||||
|
||||
auto bitmapInfo = BITMAPINFO{
|
||||
.bmiHeader = {
|
||||
.biSize = sizeof(BITMAPINFOHEADER),
|
||||
.biWidth = srcWidth,
|
||||
.biHeight = -srcHeight,
|
||||
.biPlanes = 1,
|
||||
.biBitCount = 32,
|
||||
.biCompression = BI_RGB,
|
||||
}
|
||||
};
|
||||
|
||||
auto allOpaque = true;
|
||||
auto allTransparent = true;
|
||||
for (size_t i = 0; i < imagePixels.size(); i++)
|
||||
{
|
||||
const auto opaque = til::at(imagePixels, i).rgbReserved != 0;
|
||||
allOpaque &= opaque;
|
||||
allTransparent &= !opaque;
|
||||
til::at(_imageMask, i) = (opaque ? 0 : 0xFFFFFF);
|
||||
}
|
||||
|
||||
if (allOpaque)
|
||||
{
|
||||
StretchDIBits(_hdcMemoryContext, x, y, dstWidth, dstHeight, 0, 0, srcWidth, srcHeight, imagePixels.data(), &bitmapInfo, DIB_RGB_COLORS, SRCCOPY);
|
||||
}
|
||||
else if (!allTransparent)
|
||||
{
|
||||
StretchDIBits(_hdcMemoryContext, x, y, dstWidth, dstHeight, 0, 0, srcWidth, srcHeight, _imageMask.data(), &bitmapInfo, DIB_RGB_COLORS, SRCAND);
|
||||
StretchDIBits(_hdcMemoryContext, x, y, dstWidth, dstHeight, 0, 0, srcWidth, srcHeight, imagePixels.data(), &bitmapInfo, DIB_RGB_COLORS, SRCPAINT);
|
||||
}
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
CATCH_RETURN();
|
||||
|
||||
// Routine Description:
|
||||
// - Draws the cursor on the screen
|
||||
// Arguments:
|
||||
|
||||
@ -22,6 +22,7 @@ Author(s):
|
||||
#include "IRenderData.hpp"
|
||||
#include "RenderSettings.hpp"
|
||||
#include "../../buffer/out/LineRendition.hpp"
|
||||
#include "../../buffer/out/ImageSlice.hpp"
|
||||
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 4100) // '...': unreferenced formal parameter
|
||||
@ -79,6 +80,7 @@ namespace Microsoft::Console::Render
|
||||
[[nodiscard]] virtual HRESULT PaintBackground() noexcept = 0;
|
||||
[[nodiscard]] virtual HRESULT PaintBufferLine(std::span<const Cluster> clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept = 0;
|
||||
[[nodiscard]] virtual HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF gridlineColor, COLORREF underlineColor, size_t cchLine, til::point coordTarget) noexcept = 0;
|
||||
[[nodiscard]] virtual HRESULT PaintImageSlice(const ImageSlice& imageSlice, til::CoordType targetRow, til::CoordType viewportLeft) noexcept = 0;
|
||||
[[nodiscard]] virtual HRESULT PaintSelection(const til::rect& rect) noexcept = 0;
|
||||
[[nodiscard]] virtual HRESULT PaintCursor(const CursorOptions& options) noexcept = 0;
|
||||
[[nodiscard]] virtual HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const RenderSettings& renderSettings, gsl::not_null<IRenderData*> pData, bool usingSoftFont, bool isSettingDefaultBrushes) noexcept = 0;
|
||||
|
||||
@ -42,6 +42,10 @@ namespace Microsoft::Console::Render
|
||||
const til::CoordType targetRow,
|
||||
const til::CoordType viewportLeft) noexcept override;
|
||||
|
||||
[[nodiscard]] HRESULT PaintImageSlice(const ImageSlice& imageSlice,
|
||||
const til::CoordType targetRow,
|
||||
const til::CoordType viewportLeft) noexcept override;
|
||||
|
||||
[[nodiscard]] bool RequiresContinuousRedraw() noexcept override;
|
||||
|
||||
[[nodiscard]] HRESULT InvalidateFlush(_In_ const bool circled, _Out_ bool* const pForcePaint) noexcept override;
|
||||
|
||||
@ -535,6 +535,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
|
||||
DECNKM_NumericKeypadMode = DECPrivateMode(66),
|
||||
DECBKM_BackarrowKeyMode = DECPrivateMode(67),
|
||||
DECLRMM_LeftRightMarginMode = DECPrivateMode(69),
|
||||
DECSDM_SixelDisplayMode = DECPrivateMode(80),
|
||||
DECECM_EraseColorMode = DECPrivateMode(117),
|
||||
VT200_MOUSE_MODE = DECPrivateMode(1000),
|
||||
BUTTON_EVENT_MOUSE_MODE = DECPrivateMode(1002),
|
||||
@ -615,6 +616,13 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
|
||||
DependsOnMode
|
||||
};
|
||||
|
||||
enum class SixelBackground : VTInt
|
||||
{
|
||||
Default = 0,
|
||||
Transparent = 1,
|
||||
Opaque = 2
|
||||
};
|
||||
|
||||
enum class DrcsEraseControl : VTInt
|
||||
{
|
||||
AllChars = 0,
|
||||
|
||||
@ -149,6 +149,10 @@ public:
|
||||
|
||||
virtual bool DoWTAction(const std::wstring_view string) = 0;
|
||||
|
||||
virtual StringHandler DefineSixelImage(const VTInt macroParameter,
|
||||
const DispatchTypes::SixelBackground backgroundSelect,
|
||||
const VTParameter backgroundColor) = 0; // SIXEL
|
||||
|
||||
virtual StringHandler DownloadDRCS(const VTInt fontNumber,
|
||||
const VTParameter startChar,
|
||||
const DispatchTypes::DrcsEraseControl eraseControl,
|
||||
|
||||
@ -175,11 +175,11 @@ void PageManager::MoveTo(const til::CoordType pageNumber, const bool makeVisible
|
||||
auto& saveBuffer = _getBuffer(_visiblePageNumber, pageSize);
|
||||
for (auto i = 0; i < pageSize.height; i++)
|
||||
{
|
||||
saveBuffer.GetMutableRowByOffset(i).CopyFrom(visibleBuffer.GetRowByOffset(visibleTop + i));
|
||||
visibleBuffer.CopyRow(visibleTop + i, i, saveBuffer);
|
||||
}
|
||||
for (auto i = 0; i < pageSize.height; i++)
|
||||
{
|
||||
visibleBuffer.GetMutableRowByOffset(visibleTop + i).CopyFrom(newBuffer.GetRowByOffset(i));
|
||||
newBuffer.CopyRow(i, visibleTop + i, visibleBuffer);
|
||||
}
|
||||
_visiblePageNumber = newPageNumber;
|
||||
redrawRequired = true;
|
||||
|
||||
840
src/terminal/adapter/SixelParser.cpp
Normal file
840
src/terminal/adapter/SixelParser.cpp
Normal file
@ -0,0 +1,840 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "precomp.h"
|
||||
|
||||
#include "SixelParser.hpp"
|
||||
#include "adaptDispatch.hpp"
|
||||
#include "../buffer/out/ImageSlice.hpp"
|
||||
#include "../parser/ascii.hpp"
|
||||
#include "../renderer/base/renderer.hpp"
|
||||
#include "../types/inc/colorTable.hpp"
|
||||
#include "../types/inc/utils.hpp"
|
||||
|
||||
using namespace Microsoft::Console::Types;
|
||||
using namespace Microsoft::Console::Utils;
|
||||
using namespace Microsoft::Console::VirtualTerminal;
|
||||
using namespace std::chrono;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
til::size SixelParser::CellSizeForLevel(const VTInt conformanceLevel) noexcept
|
||||
{
|
||||
switch (conformanceLevel)
|
||||
{
|
||||
case 1: // Compatible with the VT125
|
||||
return { 9, 20 };
|
||||
default: // Compatible with the VT240 and VT340
|
||||
return { 10, 20 };
|
||||
}
|
||||
}
|
||||
|
||||
size_t SixelParser::MaxColorsForLevel(const VTInt conformanceLevel) noexcept
|
||||
{
|
||||
switch (conformanceLevel)
|
||||
{
|
||||
case 1:
|
||||
case 2: // Compatible with the 4-color VT125 and VT240
|
||||
return 4;
|
||||
case 3: // Compatible with the 16-color VT340
|
||||
return 16;
|
||||
default: // Modern sixel apps often require 256 colors.
|
||||
return MAX_COLORS;
|
||||
}
|
||||
}
|
||||
|
||||
SixelParser::SixelParser(AdaptDispatch& dispatcher, const StateMachine& stateMachine, const VTInt conformanceLevel) noexcept :
|
||||
_dispatcher{ dispatcher },
|
||||
_stateMachine{ stateMachine },
|
||||
_conformanceLevel{ conformanceLevel },
|
||||
_cellSize{ CellSizeForLevel(conformanceLevel) },
|
||||
_maxColors{ MaxColorsForLevel(conformanceLevel) }
|
||||
{
|
||||
// We initialize the first 16 color entries with the VT340 palette, which is
|
||||
// also compatible with the 4-color VT125 and VT240. The remaining entries
|
||||
// are initialized with the XTerm extended colors.
|
||||
Microsoft::Console::Utils::InitializeVT340ColorTable(_colorTable);
|
||||
Microsoft::Console::Utils::InitializeExtendedColorTable(_colorTable);
|
||||
}
|
||||
|
||||
void SixelParser::SoftReset()
|
||||
{
|
||||
// The VT240 is the only terminal known to reset colors with DECSTR.
|
||||
// We only reset the first 16, since it only needs 4 of them anyway.
|
||||
if (_conformanceLevel == 2)
|
||||
{
|
||||
Microsoft::Console::Utils::InitializeVT340ColorTable(_colorTable);
|
||||
_updateTextColors();
|
||||
}
|
||||
}
|
||||
|
||||
void SixelParser::SetDisplayMode(const bool enabled) noexcept
|
||||
{
|
||||
// The display mode determines whether images are clamped at the bottom of
|
||||
// the screen (the set state), or scroll when they reach the bottom of the
|
||||
// margin area (the reset state). Clamping was the only mode of operation
|
||||
// supported prior to the VT340, so we don't allow the mode to be reset on
|
||||
// levels 1 and 2.
|
||||
if (_conformanceLevel >= 3)
|
||||
{
|
||||
_displayMode = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
std::function<bool(wchar_t)> SixelParser::DefineImage(const VTInt macroParameter, const DispatchTypes::SixelBackground backgroundSelect, const VTParameter backgroundColor)
|
||||
{
|
||||
if (_initTextBufferBoundaries())
|
||||
{
|
||||
_initRasterAttributes(macroParameter, backgroundSelect);
|
||||
_initColorMap(backgroundColor);
|
||||
_initImageBuffer();
|
||||
_state = States::Normal;
|
||||
_parameters.clear();
|
||||
return [&](const auto ch) {
|
||||
_parseCommandChar(ch);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void SixelParser::_parseCommandChar(const wchar_t ch)
|
||||
{
|
||||
// Characters in the range `?` to `~` encode a sixel value, which is a group
|
||||
// of six vertical pixels. After subtracting `?` from the character, you've
|
||||
// got a six bit binary value which represents the six pixels.
|
||||
if (ch >= '?' && ch <= '~') [[likely]]
|
||||
{
|
||||
// When preceded by a repeat command, the repeat parameter value denotes
|
||||
// the number of times that the following sixel should be repeated.
|
||||
const auto repeatCount = _applyPendingCommand();
|
||||
_writeToImageBuffer(ch - L'?', repeatCount);
|
||||
}
|
||||
// Characters `0` to `9` and `;` are used to represent parameter values for
|
||||
// commands that require them.
|
||||
else if ((ch >= '0' && ch <= '9') || ch == ';')
|
||||
{
|
||||
_parseParameterChar(ch);
|
||||
}
|
||||
// The remaining characters represent commands, some of which will execute
|
||||
// immediately, but some requiring additional parameter values. In the
|
||||
// latter case, the command will only be applied once the next command
|
||||
// character is received.
|
||||
else
|
||||
{
|
||||
switch (ch)
|
||||
{
|
||||
case '#': // DECGCI - Color Introducer
|
||||
_applyPendingCommand();
|
||||
_state = States::Color;
|
||||
_parameters.clear();
|
||||
break;
|
||||
case '!': // DECGRI - Repeat Introducer
|
||||
_applyPendingCommand();
|
||||
_state = States::Repeat;
|
||||
_parameters.clear();
|
||||
break;
|
||||
case '$': // DECGCR - Graphics Carriage Return
|
||||
_applyPendingCommand();
|
||||
_executeCarriageReturn();
|
||||
break;
|
||||
case '-': // DECGNL - Graphics Next Line
|
||||
_applyPendingCommand();
|
||||
_executeNextLine();
|
||||
break;
|
||||
case '+': // Undocumented home command (VT240 only)
|
||||
if (_conformanceLevel == 2)
|
||||
{
|
||||
_applyPendingCommand();
|
||||
_executeMoveToHome();
|
||||
}
|
||||
break;
|
||||
case '"': // DECGRA - Set Raster Attributes
|
||||
if (_conformanceLevel >= 3)
|
||||
{
|
||||
_applyPendingCommand();
|
||||
_state = States::Attributes;
|
||||
_parameters.clear();
|
||||
}
|
||||
break;
|
||||
case AsciiChars::ESC: // End of image sequence
|
||||
// At this point we only care about pending color changes. Raster
|
||||
// attributes have no effect at the end of a sequence, and a repeat
|
||||
// command is only applicable when followed by a sixel value.
|
||||
if (_state == States::Color)
|
||||
{
|
||||
_applyPendingCommand();
|
||||
}
|
||||
_fillImageBackground();
|
||||
_executeCarriageReturn();
|
||||
_maybeFlushImageBuffer(true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SixelParser::_parseParameterChar(const wchar_t ch)
|
||||
{
|
||||
// The most any command requires is 5 parameters (for the color command),
|
||||
// so anything after that can be ignored.
|
||||
if (_parameters.size() <= 5)
|
||||
{
|
||||
if (_parameters.empty())
|
||||
{
|
||||
_parameters.push_back({});
|
||||
}
|
||||
|
||||
if (ch == ';')
|
||||
{
|
||||
_parameters.push_back({});
|
||||
}
|
||||
else
|
||||
{
|
||||
const VTInt digit = ch - L'0';
|
||||
auto currentValue = _parameters.back().value_or(0);
|
||||
currentValue = currentValue * 10 + digit;
|
||||
_parameters.back() = std::min(currentValue, MAX_PARAMETER_VALUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int SixelParser::_applyPendingCommand()
|
||||
{
|
||||
if (_state != States::Normal) [[unlikely]]
|
||||
{
|
||||
const auto previousState = _state;
|
||||
_state = States::Normal;
|
||||
switch (previousState)
|
||||
{
|
||||
case States::Color:
|
||||
_defineColor({ _parameters.data(), _parameters.size() });
|
||||
return 1;
|
||||
case States::Repeat:
|
||||
return VTParameters{ _parameters.data(), _parameters.size() }.at(0);
|
||||
case States::Attributes:
|
||||
_updateRasterAttributes({ _parameters.data(), _parameters.size() });
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void SixelParser::_executeCarriageReturn() noexcept
|
||||
{
|
||||
_imageWidth = std::max(_imageWidth, _imageCursor.x);
|
||||
_imageCursor.x = 0;
|
||||
}
|
||||
|
||||
void SixelParser::_executeNextLine()
|
||||
{
|
||||
_executeCarriageReturn();
|
||||
_imageLineCount++;
|
||||
_maybeFlushImageBuffer();
|
||||
_imageCursor.y += _sixelHeight;
|
||||
_availablePixelHeight -= _sixelHeight;
|
||||
_resizeImageBuffer(_sixelHeight);
|
||||
}
|
||||
|
||||
void SixelParser::_executeMoveToHome()
|
||||
{
|
||||
_executeCarriageReturn();
|
||||
_maybeFlushImageBuffer();
|
||||
_imageCursor.y = 0;
|
||||
_availablePixelHeight = _textMargins.height() * _cellSize.height;
|
||||
}
|
||||
|
||||
bool SixelParser::_initTextBufferBoundaries()
|
||||
{
|
||||
const auto page = _dispatcher._pages.ActivePage();
|
||||
auto validOrigin = true;
|
||||
if (_displayMode)
|
||||
{
|
||||
// When display mode is set, we can write to the full extent of the page
|
||||
// and the starting cursor position is the top left of the page.
|
||||
_textMargins = { 0, page.Top(), page.Width(), page.Bottom() };
|
||||
_textCursor = _textMargins.origin();
|
||||
_availablePixelWidth = page.Width() * _cellSize.width;
|
||||
_availablePixelHeight = page.Height() * _cellSize.height;
|
||||
}
|
||||
else
|
||||
{
|
||||
// When display mode is reset, we're constrained by the text margins,
|
||||
// and the starting position is the current cursor position. This must
|
||||
// be inside the horizontal margins and above the bottom margin, else
|
||||
// nothing will be rendered.
|
||||
const auto [topMargin, bottomMargin] = _dispatcher._GetVerticalMargins(page, true);
|
||||
const auto [leftMargin, rightMargin] = _dispatcher._GetHorizontalMargins(page.Width());
|
||||
_textMargins = til::rect{ leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 };
|
||||
_textCursor = page.Cursor().GetPosition();
|
||||
_availablePixelWidth = (_textMargins.right - _textCursor.x) * _cellSize.width;
|
||||
_availablePixelHeight = (_textMargins.bottom - _textCursor.y) * _cellSize.height;
|
||||
validOrigin = _textCursor.x >= leftMargin && _textCursor.x <= rightMargin && _textCursor.y <= bottomMargin;
|
||||
}
|
||||
_pendingTextScrollCount = 0;
|
||||
|
||||
// The pixel aspect ratio can't be so large that it would prevent a sixel
|
||||
// row from fitting within the margin height, so we need to have a limit.
|
||||
_maxPixelAspectRatio = _textMargins.height() * _cellSize.height / 6;
|
||||
|
||||
// If the cursor is visible, we need to hide it while the sixel data is
|
||||
// being processed. It will be made visible again when we're done.
|
||||
_textCursorWasVisible = page.Cursor().IsVisible();
|
||||
if (_textCursorWasVisible && validOrigin)
|
||||
{
|
||||
page.Cursor().SetIsVisible(false);
|
||||
}
|
||||
return validOrigin;
|
||||
}
|
||||
|
||||
void SixelParser::_initRasterAttributes(const VTInt macroParameter, const DispatchTypes::SixelBackground backgroundSelect) noexcept
|
||||
{
|
||||
if (_conformanceLevel < 3)
|
||||
{
|
||||
// Prior to the VT340, the pixel aspect ratio was fixed at 2:1.
|
||||
_pixelAspectRatio = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The macro parameter was originally used on printers to define the
|
||||
// pixel aspect ratio and the grid size (the distance between pixels).
|
||||
// On graphic terminals, though, it's only used for the aspect ratio,
|
||||
// and then only a limited set of ratios are supported.
|
||||
switch (macroParameter)
|
||||
{
|
||||
case 0:
|
||||
case 1:
|
||||
case 5:
|
||||
case 6:
|
||||
_pixelAspectRatio = 2;
|
||||
break;
|
||||
case 2:
|
||||
_pixelAspectRatio = 5;
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
_pixelAspectRatio = 3;
|
||||
break;
|
||||
case 7:
|
||||
case 8:
|
||||
case 9:
|
||||
default:
|
||||
// While the default aspect ratio is defined as 2:1, macro parameter
|
||||
// values outside the defined range of 0 to 9 should map to 1:1.
|
||||
_pixelAspectRatio = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The height of a sixel row is 6 virtual pixels, but if the aspect ratio is
|
||||
// greater than one, the height in device pixels is a multiple of that.
|
||||
_sixelHeight = 6 * _pixelAspectRatio;
|
||||
_segmentHeight = _sixelHeight;
|
||||
|
||||
// On the VT125, the background was always drawn, but for other terminals it
|
||||
// depends on the value of the background select parameter.
|
||||
const auto transparent = (backgroundSelect == DispatchTypes::SixelBackground::Transparent);
|
||||
_backgroundFillRequired = (_conformanceLevel == 1 || !transparent);
|
||||
|
||||
// By default, the filled area will cover the maximum extent allowed.
|
||||
_backgroundSize = { til::CoordTypeMax, til::CoordTypeMax };
|
||||
}
|
||||
|
||||
void SixelParser::_updateRasterAttributes(const VTParameters& rasterAttributes)
|
||||
{
|
||||
// The documentation says default values should be interpreted as 1, but
|
||||
// the original VT340 hardware interprets omitted parameters as 0, and if
|
||||
// the x aspect is 0 (implying division by zero), the update is ignored.
|
||||
const auto yAspect = rasterAttributes.at(0).value_or(0);
|
||||
const auto xAspect = rasterAttributes.at(1).value_or(0);
|
||||
if (xAspect > 0)
|
||||
{
|
||||
// The documentation suggests the aspect ratio is rounded to the nearest
|
||||
// integer, but on the original VT340 hardware it was rounded up.
|
||||
_pixelAspectRatio = std::clamp(static_cast<int>(std::ceil(yAspect * 1.0 / xAspect)), 1, _maxPixelAspectRatio);
|
||||
_sixelHeight = 6 * _pixelAspectRatio;
|
||||
// When the sixel height is changed multiple times in a row, the segment
|
||||
// height has to track the maximum of all the sixel heights used.
|
||||
_segmentHeight = std::max(_segmentHeight, _sixelHeight);
|
||||
_resizeImageBuffer(_sixelHeight);
|
||||
}
|
||||
|
||||
// Although it's not clear from the documentation, we know from testing on
|
||||
// a VT340 that the background dimensions are measured in device pixels, so
|
||||
// the given height does not need to be scaled by the pixel aspect ratio.
|
||||
const auto width = rasterAttributes.at(2).value_or(0);
|
||||
const auto height = rasterAttributes.at(3).value_or(0);
|
||||
|
||||
// If these values are omitted or 0, they default to what they were before,
|
||||
// which typically would mean filling the whole screen, but could also fall
|
||||
// back to the dimensions from an earlier raster attributes command.
|
||||
_backgroundSize.width = width > 0 ? width : _backgroundSize.width;
|
||||
_backgroundSize.height = height > 0 ? height : _backgroundSize.height;
|
||||
}
|
||||
|
||||
void SixelParser::_scrollTextBuffer(Page& page, const int scrollAmount)
|
||||
{
|
||||
// We scroll the text buffer by moving the cursor to the bottom of the
|
||||
// margin area and executing an appropriate number of line feeds.
|
||||
if (_textCursor.y != _textMargins.bottom - 1)
|
||||
{
|
||||
_textCursor = { _textCursor.x, _textMargins.bottom - 1 };
|
||||
page.Cursor().SetPosition(_textCursor);
|
||||
}
|
||||
auto panAmount = 0;
|
||||
for (auto i = 0; i < scrollAmount; i++)
|
||||
{
|
||||
if (_dispatcher._DoLineFeed(page, false, false))
|
||||
{
|
||||
page.MoveViewportDown();
|
||||
panAmount++;
|
||||
}
|
||||
}
|
||||
|
||||
// If the line feeds panned the viewport down, we need to adjust our margins
|
||||
// and text cursor coordinates to align with that movement.
|
||||
_textCursor.y += panAmount;
|
||||
_textMargins += til::point{ 0, panAmount };
|
||||
|
||||
// And if it wasn't all panning, we need to move the image origin up to
|
||||
// match the number of rows that were actually scrolled.
|
||||
if (scrollAmount > panAmount)
|
||||
{
|
||||
auto expectedMovement = scrollAmount - panAmount;
|
||||
// If constrained by margins, we can only move as far as the top margin.
|
||||
if (_textMargins.top > page.Top() || _textMargins.left > 0 || _textMargins.right < page.Width() - 1)
|
||||
{
|
||||
const auto availableSpace = std::max(_imageOriginCell.y - _textMargins.top, 0);
|
||||
if (expectedMovement > availableSpace)
|
||||
{
|
||||
// Anything more than that will need to be erased from the
|
||||
// image. And if the origin was already above the top margin,
|
||||
// this erased segment will be partway through the image.
|
||||
const auto eraseRowCount = expectedMovement - availableSpace;
|
||||
const auto eraseOffset = std::max(_textMargins.top - _imageOriginCell.y, 0);
|
||||
_eraseImageBufferRows(eraseRowCount, eraseOffset);
|
||||
// But if there was any available space, we still then need to
|
||||
// move the origin up as far as it can go.
|
||||
expectedMovement = availableSpace;
|
||||
}
|
||||
}
|
||||
_imageOriginCell.y -= expectedMovement;
|
||||
}
|
||||
}
|
||||
|
||||
void SixelParser::_updateTextCursor(Cursor& cursor) noexcept
|
||||
{
|
||||
// Unless the sixel display mode is set, we need to update the text cursor
|
||||
// position to align with the final image cursor position. This should be
|
||||
// the cell which is intersected by the top of the final sixel row.
|
||||
if (!_displayMode)
|
||||
{
|
||||
const auto finalRow = _imageOriginCell.y + _imageCursor.y / _cellSize.height;
|
||||
if (finalRow != _textCursor.y)
|
||||
{
|
||||
cursor.SetPosition({ _textCursor.x, finalRow });
|
||||
}
|
||||
}
|
||||
// And if the cursor was visible when we started, we need to restore it.
|
||||
if (_textCursorWasVisible)
|
||||
{
|
||||
cursor.SetIsVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
void SixelParser::_initColorMap(const VTParameter backgroundColor)
|
||||
{
|
||||
_colorsUsed = 0;
|
||||
_colorsAvailable = _maxColors;
|
||||
_colorTableChanged = false;
|
||||
|
||||
// The color numbers in a sixel image don't necessarily map directly to
|
||||
// entries in the color table. That mapping is determined by the order in
|
||||
// which the colors are defined. If they aren't defined, though, the default
|
||||
// mapping is just the color number modulo the color table size.
|
||||
for (size_t colorNumber = 0; colorNumber < _colorMap.size(); colorNumber++)
|
||||
{
|
||||
_colorMap.at(colorNumber) = gsl::narrow_cast<IndexType>(colorNumber % _maxColors);
|
||||
}
|
||||
|
||||
// The _colorMapUsed field keeps track of the color numbers that have been
|
||||
// explicitly mapped to a color table entry, since that locks in the mapping
|
||||
// for the duration of the image. Additional definitions for that color
|
||||
// number will update the existing mapped table entry - they won't generate
|
||||
// new mappings for the number.
|
||||
std::fill(_colorMapUsed.begin(), _colorMapUsed.end(), false);
|
||||
|
||||
// The VT240 has an extra feature, whereby the P3 parameter defines the
|
||||
// color number to be used for the background (i.e. it's preassigned to
|
||||
// table entry 0). If you specify a value larger than the maximum color
|
||||
// table index, the number of available colors is reduced by 1, which
|
||||
// effectively protects the background color from modification.
|
||||
if (_conformanceLevel == 2 && backgroundColor.has_value()) [[unlikely]]
|
||||
{
|
||||
const size_t colorNumber = backgroundColor.value();
|
||||
if (colorNumber < _maxColors)
|
||||
{
|
||||
til::at(_colorMap, colorNumber) = 0;
|
||||
til::at(_colorMapUsed, colorNumber) = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_colorsAvailable = _maxColors - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// On the original hardware terminals, the default color index would have
|
||||
// been the last entry in the color table. But on modern terminals, it is
|
||||
// typically capped at 15 for compatibility with the 16-color VT340. This
|
||||
// is the color used if no color commands are received.
|
||||
const auto defaultColorIndex = std::min<size_t>(_maxColors - 1, 15);
|
||||
_foregroundPixel = { .colorIndex = gsl::narrow_cast<IndexType>(defaultColorIndex) };
|
||||
}
|
||||
|
||||
void SixelParser::_defineColor(const VTParameters& colorParameters)
|
||||
{
|
||||
// The first parameter selects the color number to use. If it's greater than
|
||||
// the color map size, we just mod the value into range.
|
||||
const auto colorNumber = colorParameters.at(0).value_or(0) % _colorMap.size();
|
||||
|
||||
// If there are additional parameters, then this command will also redefine
|
||||
// the color palette associated with the selected color number. This is not
|
||||
// supported on the VT125 though.
|
||||
if (colorParameters.size() > 1 && _conformanceLevel > 1) [[unlikely]]
|
||||
{
|
||||
const auto model = DispatchTypes::ColorModel{ colorParameters.at(1) };
|
||||
const auto x = colorParameters.at(2).value_or(0);
|
||||
const auto y = colorParameters.at(3).value_or(0);
|
||||
const auto z = colorParameters.at(4).value_or(0);
|
||||
switch (model)
|
||||
{
|
||||
case DispatchTypes::ColorModel::HLS:
|
||||
_defineColor(colorNumber, Utils::ColorFromHLS(x, y, z));
|
||||
break;
|
||||
case DispatchTypes::ColorModel::RGB:
|
||||
_defineColor(colorNumber, Utils::ColorFromRGB100(x, y, z));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The actual color table index we use is derived from the color number via
|
||||
// the color map. This is initially defined in _initColorMap above, but may
|
||||
// be altered when colors are set in the _defineColor method below.
|
||||
const auto colorIndex = _colorMap.at(colorNumber);
|
||||
_foregroundPixel = { .colorIndex = colorIndex };
|
||||
}
|
||||
|
||||
void SixelParser::_defineColor(const size_t colorNumber, const COLORREF color)
|
||||
{
|
||||
if (til::at(_colorMapUsed, colorNumber))
|
||||
{
|
||||
// If the color is already assigned, we update the mapped table entry.
|
||||
const auto tableIndex = til::at(_colorMap, colorNumber);
|
||||
til::at(_colorTable, tableIndex) = color;
|
||||
_colorTableChanged = true;
|
||||
// If some image content has already been defined at this point, and
|
||||
// we're processing the last character in the packet, this is likely an
|
||||
// attempt to animate the palette, so we should flush the image.
|
||||
if (_imageWidth > 0 && _stateMachine.IsProcessingLastCharacter())
|
||||
{
|
||||
_maybeFlushImageBuffer();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise assign it to the next available color table entry.
|
||||
if (_colorsUsed < _colorsAvailable)
|
||||
{
|
||||
// Since table entry 0 is the background color, which you typically
|
||||
// want to leave unchanged, the original hardware terminals would
|
||||
// skip that and start with table entry 1, and only wrap back to 0
|
||||
// when all others had been used.
|
||||
const auto tableIndex = ++_colorsUsed % _maxColors;
|
||||
til::at(_colorMap, colorNumber) = gsl::narrow_cast<IndexType>(tableIndex);
|
||||
til::at(_colorTable, tableIndex) = color;
|
||||
_colorTableChanged = true;
|
||||
}
|
||||
else if (_conformanceLevel == 2)
|
||||
{
|
||||
// If we've used up all the available color table entries, we have
|
||||
// to assign this color number to one of the previously used ones.
|
||||
// The VT240 uses the closest match from the existing color entries,
|
||||
// but the VT340 just uses the default mapping assigned at the start
|
||||
// (i.e. the color number modulo the color table size).
|
||||
size_t tableIndex = 0;
|
||||
int bestDiff = std::numeric_limits<int>::max();
|
||||
for (size_t i = 0; i < _maxColors; i++)
|
||||
{
|
||||
const auto existingColor = til::at(_colorTable, i);
|
||||
const auto diff = [](const auto c1, const auto c2) noexcept {
|
||||
return static_cast<int>(c1) - static_cast<int>(c2);
|
||||
};
|
||||
const auto redDiff = diff(GetRValue(existingColor), GetRValue(color));
|
||||
const auto greenDiff = diff(GetGValue(existingColor), GetGValue(color));
|
||||
const auto blueDiff = diff(GetBValue(existingColor), GetBValue(color));
|
||||
const auto totalDiff = redDiff * redDiff + greenDiff * greenDiff + blueDiff * blueDiff;
|
||||
if (totalDiff <= bestDiff)
|
||||
{
|
||||
bestDiff = totalDiff;
|
||||
tableIndex = i;
|
||||
}
|
||||
}
|
||||
til::at(_colorMap, colorNumber) = gsl::narrow_cast<IndexType>(tableIndex);
|
||||
}
|
||||
til::at(_colorMapUsed, colorNumber) = true;
|
||||
}
|
||||
}
|
||||
|
||||
COLORREF SixelParser::_colorFromIndex(const IndexType tableIndex) const noexcept
|
||||
{
|
||||
return til::at(_colorTable, tableIndex);
|
||||
}
|
||||
|
||||
constexpr RGBQUAD SixelParser::_makeRGBQUAD(const COLORREF color) noexcept
|
||||
{
|
||||
return RGBQUAD{
|
||||
.rgbBlue = GetBValue(color),
|
||||
.rgbGreen = GetGValue(color),
|
||||
.rgbRed = GetRValue(color),
|
||||
.rgbReserved = 255
|
||||
};
|
||||
}
|
||||
|
||||
void SixelParser::_updateTextColors()
|
||||
{
|
||||
// On the original hardware terminals, text and images shared the same
|
||||
// color table, so palette changes made in an image would be reflected in
|
||||
// the text output as well.
|
||||
if (_conformanceLevel <= 3 && _maxColors > 2 && _colorTableChanged) [[unlikely]]
|
||||
{
|
||||
for (IndexType tableIndex = 0; tableIndex < _maxColors; tableIndex++)
|
||||
{
|
||||
_dispatcher.SetColorTableEntry(tableIndex, _colorFromIndex(tableIndex));
|
||||
}
|
||||
_colorTableChanged = false;
|
||||
}
|
||||
}
|
||||
|
||||
void SixelParser::_initImageBuffer()
|
||||
{
|
||||
_imageBuffer.clear();
|
||||
_imageOriginCell = _textCursor;
|
||||
_imageCursor = {};
|
||||
_imageWidth = 0;
|
||||
_imageMaxWidth = _availablePixelWidth;
|
||||
_imageLineCount = 0;
|
||||
_resizeImageBuffer(_sixelHeight);
|
||||
|
||||
_lastFlushLine = 0;
|
||||
_lastFlushTime = steady_clock::now();
|
||||
|
||||
// Prior to the VT340, the background was filled as soon as the sixel
|
||||
// definition was started, because the initial raster attributes could
|
||||
// not be altered.
|
||||
if (_conformanceLevel < 3)
|
||||
{
|
||||
_fillImageBackground();
|
||||
}
|
||||
}
|
||||
|
||||
void SixelParser::_resizeImageBuffer(const til::CoordType requiredHeight)
|
||||
{
|
||||
const auto requiredSize = (_imageCursor.y + requiredHeight) * _imageMaxWidth;
|
||||
if (static_cast<size_t>(requiredSize) > _imageBuffer.size())
|
||||
{
|
||||
static constexpr auto transparentPixel = IndexedPixel{ .transparent = true };
|
||||
_imageBuffer.resize(requiredSize, transparentPixel);
|
||||
}
|
||||
}
|
||||
|
||||
void SixelParser::_fillImageBackground()
|
||||
{
|
||||
if (_backgroundFillRequired) [[unlikely]]
|
||||
{
|
||||
_backgroundFillRequired = false;
|
||||
|
||||
const auto backgroundHeight = std::min(_backgroundSize.height, _availablePixelHeight);
|
||||
const auto backgroundWidth = std::min(_backgroundSize.width, _availablePixelWidth);
|
||||
_resizeImageBuffer(backgroundHeight);
|
||||
|
||||
// When a background fill is requested, we prefill the buffer with the 0
|
||||
// color index, up to the boundaries set by the raster attributes (or if
|
||||
// none were given, up to the page boundaries). The actual image output
|
||||
// isn't limited by the background dimensions though.
|
||||
static constexpr auto backgroundPixel = IndexedPixel{};
|
||||
const auto backgroundOffset = _imageCursor.y * _imageMaxWidth;
|
||||
auto dst = std::next(_imageBuffer.begin(), backgroundOffset);
|
||||
for (auto i = 0; i < backgroundHeight; i++)
|
||||
{
|
||||
std::fill_n(dst, backgroundWidth, backgroundPixel);
|
||||
std::advance(dst, _imageMaxWidth);
|
||||
}
|
||||
|
||||
_imageWidth = std::max(_imageWidth, backgroundWidth);
|
||||
}
|
||||
}
|
||||
|
||||
void SixelParser::_writeToImageBuffer(int sixelValue, int repeatCount)
|
||||
{
|
||||
// On terminals that support the raster attributes command (which sets the
|
||||
// background size), the background is only drawn when the first sixel value
|
||||
// is received. So if we haven't filled it yet, we need to do so now.
|
||||
_fillImageBackground();
|
||||
|
||||
// Then we need to render the 6 vertical pixels that are represented by the
|
||||
// bits in the sixel value. Although note that each of these sixel pixels
|
||||
// may cover more than one device pixel, depending on the aspect ratio.
|
||||
const auto targetOffset = _imageCursor.y * _imageMaxWidth + _imageCursor.x;
|
||||
auto imageBufferPtr = std::next(_imageBuffer.data(), targetOffset);
|
||||
repeatCount = std::min(repeatCount, _imageMaxWidth - _imageCursor.x);
|
||||
for (auto i = 0; i < 6; i++)
|
||||
{
|
||||
if (sixelValue & 1)
|
||||
{
|
||||
auto repeatAspectRatio = _pixelAspectRatio;
|
||||
do
|
||||
{
|
||||
std::fill_n(imageBufferPtr, repeatCount, _foregroundPixel);
|
||||
std::advance(imageBufferPtr, _imageMaxWidth);
|
||||
} while (--repeatAspectRatio > 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::advance(imageBufferPtr, _imageMaxWidth * _pixelAspectRatio);
|
||||
}
|
||||
sixelValue >>= 1;
|
||||
}
|
||||
_imageCursor.x += repeatCount;
|
||||
}
|
||||
|
||||
void SixelParser::_eraseImageBufferRows(const int rowCount, const til::CoordType rowOffset) noexcept
|
||||
{
|
||||
const auto pixelCount = rowCount * _cellSize.height;
|
||||
const auto bufferOffset = rowOffset * _cellSize.height * _imageMaxWidth;
|
||||
const auto bufferOffsetEnd = bufferOffset + pixelCount * _imageMaxWidth;
|
||||
_imageBuffer.erase(_imageBuffer.begin() + bufferOffset, _imageBuffer.begin() + bufferOffsetEnd);
|
||||
_imageCursor.y -= pixelCount;
|
||||
}
|
||||
|
||||
void SixelParser::_maybeFlushImageBuffer(const bool endOfSequence)
|
||||
{
|
||||
// Regardless of whether we flush the image or not, we always calculate how
|
||||
// much we need to scroll in advance. This algorithm is a bit odd. If there
|
||||
// isn't enough space for the current segment, it'll scroll until it can fit
|
||||
// the segment with a pixel to spare. So in the case that it's an exact fit,
|
||||
// it's expected that we'd scroll an additional line. Although this is not
|
||||
// common, since it only occurs for pixel aspect ratios of 4:1 or more. Also
|
||||
// note that we never scroll more than the margin height, since that would
|
||||
// result in the top of the segment being pushed offscreen.
|
||||
if (_segmentHeight > _availablePixelHeight && !_displayMode) [[unlikely]]
|
||||
{
|
||||
const auto marginPixelHeight = _textMargins.height() * _cellSize.height;
|
||||
while (_availablePixelHeight < marginPixelHeight && _segmentHeight >= _availablePixelHeight)
|
||||
{
|
||||
_pendingTextScrollCount += 1;
|
||||
_availablePixelHeight += _cellSize.height;
|
||||
}
|
||||
}
|
||||
|
||||
// Once we've calculated how much scrolling was necessary for the existing
|
||||
// segment height, we don't need to track that any longer. The next segment
|
||||
// will start with the active sixel height.
|
||||
_segmentHeight = _sixelHeight;
|
||||
|
||||
// This method is called after every newline (DECGNL), but we don't want to
|
||||
// render partial output for high speed image sequences like video, so we
|
||||
// only flush if it has been more than 500ms since the last flush, or it
|
||||
// appears that the output is intentionally streamed. If the current buffer
|
||||
// has ended with a newline, and we've received no more than one line since
|
||||
// the last flush, that suggest it's an intentional break in the stream.
|
||||
const auto currentTime = steady_clock::now();
|
||||
const auto timeSinceLastFlush = duration_cast<milliseconds>(currentTime - _lastFlushTime);
|
||||
const auto linesSinceLastFlush = _imageLineCount - _lastFlushLine;
|
||||
if (endOfSequence || timeSinceLastFlush > 500ms || (linesSinceLastFlush <= 1 && _stateMachine.IsProcessingLastCharacter()))
|
||||
{
|
||||
_lastFlushTime = currentTime;
|
||||
_lastFlushLine = _imageLineCount;
|
||||
|
||||
// Before we output anything, we need to scroll the text buffer to make
|
||||
// space for the image, using the precalculated scroll count from above.
|
||||
auto page = _dispatcher._pages.ActivePage();
|
||||
if (_pendingTextScrollCount > 0) [[unlikely]]
|
||||
{
|
||||
_scrollTextBuffer(page, _pendingTextScrollCount);
|
||||
_pendingTextScrollCount = 0;
|
||||
}
|
||||
|
||||
// If there's no image width, there's nothing to render at this point,
|
||||
// so the only visible change will be the scrolling.
|
||||
if (_imageWidth > 0)
|
||||
{
|
||||
const auto columnBegin = _imageOriginCell.x;
|
||||
const auto columnEnd = _imageOriginCell.x + (_imageWidth + _cellSize.width - 1) / _cellSize.width;
|
||||
auto rowOffset = _imageOriginCell.y;
|
||||
auto srcIterator = _imageBuffer.begin();
|
||||
while (srcIterator < _imageBuffer.end() && rowOffset < page.Bottom())
|
||||
{
|
||||
if (rowOffset >= 0)
|
||||
{
|
||||
auto& dstRow = page.Buffer().GetMutableRowByOffset(rowOffset);
|
||||
auto& dstSlice = dstRow.GetMutableImageSlice();
|
||||
if (!dstSlice)
|
||||
{
|
||||
dstSlice = std::make_unique<ImageSlice>(_cellSize);
|
||||
}
|
||||
auto dstIterator = dstSlice->MutablePixels(columnBegin, columnEnd);
|
||||
for (auto pixelRow = 0; pixelRow < _cellSize.height; pixelRow++)
|
||||
{
|
||||
for (auto pixelColumn = 0; pixelColumn < _imageWidth; pixelColumn++)
|
||||
{
|
||||
const auto srcPixel = til::at(srcIterator, pixelColumn);
|
||||
if (!srcPixel.transparent)
|
||||
{
|
||||
const auto srcColor = _colorFromIndex(srcPixel.colorIndex);
|
||||
til::at(dstIterator, pixelColumn) = _makeRGBQUAD(srcColor);
|
||||
}
|
||||
}
|
||||
std::advance(srcIterator, _imageMaxWidth);
|
||||
if (srcIterator >= _imageBuffer.end())
|
||||
{
|
||||
break;
|
||||
}
|
||||
std::advance(dstIterator, dstSlice->PixelWidth());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::advance(srcIterator, _imageMaxWidth * _cellSize.height);
|
||||
}
|
||||
rowOffset++;
|
||||
}
|
||||
|
||||
// Trigger a redraw of the affected rows in the renderer.
|
||||
const auto topRowOffset = std::max(_imageOriginCell.y, 0);
|
||||
const auto dirtyView = Viewport::FromExclusive({ 0, topRowOffset, page.Width(), rowOffset });
|
||||
page.Buffer().TriggerRedraw(dirtyView);
|
||||
|
||||
// If the start of the image is now above the top of the page, we
|
||||
// won't be making any further updates to that content, so we can
|
||||
// erase it from our local buffer
|
||||
if (_imageOriginCell.y < page.Top())
|
||||
{
|
||||
const auto rowsToDelete = page.Top() - _imageOriginCell.y;
|
||||
_eraseImageBufferRows(rowsToDelete);
|
||||
_imageOriginCell.y += rowsToDelete;
|
||||
}
|
||||
}
|
||||
|
||||
// On lower conformance levels, we also update the text colors.
|
||||
_updateTextColors();
|
||||
|
||||
// And at the end of the sequence, we update the text cursor position.
|
||||
if (endOfSequence)
|
||||
{
|
||||
_updateTextCursor(page.Cursor());
|
||||
}
|
||||
}
|
||||
}
|
||||
125
src/terminal/adapter/SixelParser.hpp
Normal file
125
src/terminal/adapter/SixelParser.hpp
Normal file
@ -0,0 +1,125 @@
|
||||
/*++
|
||||
Copyright (c) Microsoft Corporation
|
||||
Licensed under the MIT license.
|
||||
|
||||
Module Name:
|
||||
- SixelParser.hpp
|
||||
|
||||
Abstract:
|
||||
- This class handles the parsing of the Sixel image format.
|
||||
--*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "til.h"
|
||||
#include "DispatchTypes.hpp"
|
||||
|
||||
class Cursor;
|
||||
class TextBuffer;
|
||||
|
||||
namespace Microsoft::Console::VirtualTerminal
|
||||
{
|
||||
class AdaptDispatch;
|
||||
class Page;
|
||||
class StateMachine;
|
||||
|
||||
class SixelParser
|
||||
{
|
||||
public:
|
||||
static constexpr VTInt DefaultConformance = 9;
|
||||
|
||||
static til::size CellSizeForLevel(const VTInt conformanceLevel) noexcept;
|
||||
static size_t MaxColorsForLevel(const VTInt conformanceLevel) noexcept;
|
||||
|
||||
SixelParser(AdaptDispatch& dispatcher, const StateMachine& stateMachine, const VTInt conformanceLevel = DefaultConformance) noexcept;
|
||||
void SoftReset();
|
||||
void SetDisplayMode(const bool enabled) noexcept;
|
||||
std::function<bool(wchar_t)> DefineImage(const VTInt macroParameter, const DispatchTypes::SixelBackground backgroundSelect, const VTParameter backgroundColor);
|
||||
|
||||
private:
|
||||
// NB: If we want to support more than 256 colors, we'll also need to
|
||||
// change the IndexType to uint16_t, and use a bit field in IndexedPixel
|
||||
// to retain the 16-bit size.
|
||||
static constexpr size_t MAX_COLORS = 256;
|
||||
using IndexType = uint8_t;
|
||||
struct IndexedPixel
|
||||
{
|
||||
uint8_t transparent = false;
|
||||
IndexType colorIndex = 0;
|
||||
};
|
||||
|
||||
AdaptDispatch& _dispatcher;
|
||||
const StateMachine& _stateMachine;
|
||||
const VTInt _conformanceLevel;
|
||||
|
||||
void _parseCommandChar(const wchar_t ch);
|
||||
void _parseParameterChar(const wchar_t ch);
|
||||
int _applyPendingCommand();
|
||||
void _executeCarriageReturn() noexcept;
|
||||
void _executeNextLine();
|
||||
void _executeMoveToHome();
|
||||
|
||||
enum class States : size_t
|
||||
{
|
||||
Normal,
|
||||
Attributes,
|
||||
Color,
|
||||
Repeat
|
||||
};
|
||||
States _state = States::Normal;
|
||||
std::vector<VTParameter> _parameters;
|
||||
|
||||
bool _initTextBufferBoundaries();
|
||||
void _initRasterAttributes(const VTInt macroParameter, const DispatchTypes::SixelBackground backgroundSelect) noexcept;
|
||||
void _updateRasterAttributes(const VTParameters& rasterAttributes);
|
||||
void _scrollTextBuffer(Page& page, const int scrollAmount);
|
||||
void _updateTextCursor(Cursor& cursor) noexcept;
|
||||
|
||||
const til::size _cellSize;
|
||||
bool _displayMode = true;
|
||||
til::rect _textMargins;
|
||||
til::point _textCursor;
|
||||
bool _textCursorWasVisible;
|
||||
til::CoordType _availablePixelWidth;
|
||||
til::CoordType _availablePixelHeight;
|
||||
til::CoordType _maxPixelAspectRatio;
|
||||
til::CoordType _pixelAspectRatio;
|
||||
til::CoordType _sixelHeight;
|
||||
til::CoordType _segmentHeight;
|
||||
til::CoordType _pendingTextScrollCount;
|
||||
til::size _backgroundSize;
|
||||
bool _backgroundFillRequired;
|
||||
|
||||
void _initColorMap(const VTParameter backgroundColor);
|
||||
void _defineColor(const VTParameters& colorParameters);
|
||||
void _defineColor(const size_t colorNumber, const COLORREF color);
|
||||
COLORREF _colorFromIndex(const IndexType tableIndex) const noexcept;
|
||||
static constexpr RGBQUAD _makeRGBQUAD(const COLORREF color) noexcept;
|
||||
void _updateTextColors();
|
||||
|
||||
std::array<IndexType, MAX_COLORS> _colorMap = {};
|
||||
std::array<bool, MAX_COLORS> _colorMapUsed = {};
|
||||
std::array<COLORREF, MAX_COLORS> _colorTable = {};
|
||||
const size_t _maxColors;
|
||||
size_t _colorsUsed;
|
||||
size_t _colorsAvailable;
|
||||
bool _colorTableChanged;
|
||||
IndexedPixel _foregroundPixel;
|
||||
|
||||
void _initImageBuffer();
|
||||
void _resizeImageBuffer(const til::CoordType requiredHeight);
|
||||
void _fillImageBackground();
|
||||
void _writeToImageBuffer(const int sixelValue, const int repeatCount);
|
||||
void _eraseImageBufferRows(const int rowCount, const til::CoordType startRow = 0) noexcept;
|
||||
void _maybeFlushImageBuffer(const bool endOfSequence = false);
|
||||
|
||||
std::vector<IndexedPixel> _imageBuffer;
|
||||
til::point _imageOriginCell;
|
||||
til::point _imageCursor;
|
||||
til::CoordType _imageWidth;
|
||||
til::CoordType _imageMaxWidth;
|
||||
size_t _imageLineCount;
|
||||
size_t _lastFlushLine;
|
||||
std::chrono::steady_clock::time_point _lastFlushTime;
|
||||
};
|
||||
}
|
||||
@ -4,6 +4,7 @@
|
||||
#include "precomp.h"
|
||||
|
||||
#include "adaptDispatch.hpp"
|
||||
#include "SixelParser.hpp"
|
||||
#include "../../inc/unicode.hpp"
|
||||
#include "../../renderer/base/renderer.hpp"
|
||||
#include "../../types/inc/CodepointWidthDetector.hpp"
|
||||
@ -626,6 +627,8 @@ void AdaptDispatch::_ScrollRectVertically(const Page& page, const til::rect& scr
|
||||
textBuffer.WriteLine(OutputCellIterator({ ¤t, 1 }), dstPos);
|
||||
srcView.WalkInBounds(srcPos, walkDirection);
|
||||
} while (dstView.WalkInBounds(dstPos, walkDirection));
|
||||
// Copy any image content in the affected area.
|
||||
ImageSlice::CopyBlock(textBuffer, srcView.ToExclusive(), textBuffer, dstView.ToExclusive());
|
||||
}
|
||||
}
|
||||
|
||||
@ -676,6 +679,8 @@ void AdaptDispatch::_ScrollRectHorizontally(const Page& page, const til::rect& s
|
||||
next = OutputCell(*textBuffer.GetCellDataAt(sourcePos));
|
||||
textBuffer.WriteLine(OutputCellIterator({ ¤t, 1 }), targetPos);
|
||||
} while (target.WalkInBounds(targetPos, walkDirection));
|
||||
// Copy any image content in the affected area.
|
||||
ImageSlice::CopyBlock(textBuffer, source.ToExclusive(), textBuffer, target.ToExclusive());
|
||||
}
|
||||
|
||||
// Columns revealed by the scroll are filled with standard erase attributes.
|
||||
@ -894,6 +899,8 @@ void AdaptDispatch::_SelectiveEraseRect(const Page& page, const til::rect& erase
|
||||
{
|
||||
// The text is cleared but the attributes are left as is.
|
||||
rowBuffer.ClearCell(col);
|
||||
// Any image content also needs to be erased.
|
||||
ImageSlice::EraseCells(rowBuffer, col, col + 1);
|
||||
page.Buffer().TriggerRedraw(Viewport::FromCoord({ col, row }));
|
||||
}
|
||||
}
|
||||
@ -1252,6 +1259,8 @@ bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const
|
||||
dst.Buffer().WriteLine(OutputCellIterator({ ¤t, 1 }), dstPos);
|
||||
}
|
||||
} while (dstView.WalkInBounds(dstPos, walkDirection));
|
||||
// Copy any image content in the affected area.
|
||||
ImageSlice::CopyBlock(src.Buffer(), srcView.ToExclusive(), dst.Buffer(), dstView.ToExclusive());
|
||||
_api.NotifyAccessibilityChange(dstRect);
|
||||
}
|
||||
|
||||
@ -1530,6 +1539,7 @@ bool AdaptDispatch::DeviceAttributes()
|
||||
// extensions.
|
||||
//
|
||||
// 1 = 132 column mode (ConHost only)
|
||||
// 4 = Sixel Graphics (ConHost only)
|
||||
// 6 = Selective erase
|
||||
// 7 = Soft fonts
|
||||
// 14 = 8-bit interface architecture
|
||||
@ -1547,7 +1557,7 @@ bool AdaptDispatch::DeviceAttributes()
|
||||
}
|
||||
else
|
||||
{
|
||||
_api.ReturnResponse(L"\x1b[?61;1;6;7;14;21;22;23;24;28;32;42c");
|
||||
_api.ReturnResponse(L"\x1b[?61;1;4;6;7;14;21;22;23;24;28;32;42c");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -1991,6 +2001,13 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con
|
||||
page.Buffer().ResetLineRenditionRange(page.Top(), page.Bottom());
|
||||
}
|
||||
return true;
|
||||
case DispatchTypes::ModeParams::DECSDM_SixelDisplayMode:
|
||||
_modes.set(Mode::SixelDisplay, enable);
|
||||
if (_sixelParser)
|
||||
{
|
||||
_sixelParser->SetDisplayMode(enable);
|
||||
}
|
||||
return true;
|
||||
case DispatchTypes::ModeParams::DECECM_EraseColorMode:
|
||||
_modes.set(Mode::EraseColor, enable);
|
||||
return true;
|
||||
@ -2139,6 +2156,9 @@ bool AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param)
|
||||
case DispatchTypes::ModeParams::DECLRMM_LeftRightMarginMode:
|
||||
state = mapTemp(_modes.test(Mode::AllowDECSLRM));
|
||||
break;
|
||||
case DispatchTypes::ModeParams::DECSDM_SixelDisplayMode:
|
||||
state = mapTemp(_modes.test(Mode::SixelDisplay));
|
||||
break;
|
||||
case DispatchTypes::ModeParams::DECECM_EraseColorMode:
|
||||
state = mapTemp(_modes.test(Mode::EraseColor));
|
||||
break;
|
||||
@ -3152,6 +3172,12 @@ bool AdaptDispatch::SoftReset()
|
||||
_savedCursorState.at(0).TermOutput = _termOutput;
|
||||
_savedCursorState.at(1).TermOutput = _termOutput;
|
||||
|
||||
// Soft reset the Sixel parser if in use.
|
||||
if (_sixelParser)
|
||||
{
|
||||
_sixelParser->SoftReset();
|
||||
}
|
||||
|
||||
return !_api.IsConsolePty();
|
||||
}
|
||||
|
||||
@ -3189,6 +3215,9 @@ bool AdaptDispatch::HardReset()
|
||||
// Reset all page buffers.
|
||||
_pages.Reset();
|
||||
|
||||
// Reset the Sixel parser.
|
||||
_sixelParser = nullptr;
|
||||
|
||||
// Completely reset the TerminalOutput state.
|
||||
_termOutput = {};
|
||||
if (_initialCodePage.has_value())
|
||||
@ -4043,6 +4072,28 @@ bool AdaptDispatch::DoWTAction(const std::wstring_view string)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - SIXEL - Defines an image transmitted in sixel format via the returned
|
||||
// StringHandler function.
|
||||
// Arguments:
|
||||
// - macroParameter - Selects one a of set of predefined aspect ratios.
|
||||
// - backgroundSelect - Whether the background should be transparent or opaque.
|
||||
// - backgroundColor - The color number used for the background (VT240).
|
||||
// Return Value:
|
||||
// - a function to receive the pixel data or nullptr if parameters are invalid
|
||||
ITermDispatch::StringHandler AdaptDispatch::DefineSixelImage(const VTInt macroParameter,
|
||||
const DispatchTypes::SixelBackground backgroundSelect,
|
||||
const VTParameter backgroundColor)
|
||||
{
|
||||
// The sixel parser is created on demand.
|
||||
if (!_sixelParser)
|
||||
{
|
||||
_sixelParser = std::make_unique<SixelParser>(*this, _api.GetStateMachine());
|
||||
_sixelParser->SetDisplayMode(_modes.test(Mode::SixelDisplay));
|
||||
}
|
||||
return _sixelParser->DefineImage(macroParameter, backgroundSelect, backgroundColor);
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - DECDLD - Downloads one or more characters of a dynamically redefinable
|
||||
// character set (DRCS) with a specified pixel pattern. The pixel array is
|
||||
|
||||
@ -152,6 +152,10 @@ namespace Microsoft::Console::VirtualTerminal
|
||||
|
||||
bool DoWTAction(const std::wstring_view string) override;
|
||||
|
||||
StringHandler DefineSixelImage(const VTInt macroParameter,
|
||||
const DispatchTypes::SixelBackground backgroundSelect,
|
||||
const VTParameter backgroundColor) override; // SIXEL
|
||||
|
||||
StringHandler DownloadDRCS(const VTInt fontNumber,
|
||||
const VTParameter startChar,
|
||||
const DispatchTypes::DrcsEraseControl eraseControl,
|
||||
@ -186,6 +190,7 @@ namespace Microsoft::Console::VirtualTerminal
|
||||
Column,
|
||||
AllowDECCOLM,
|
||||
AllowDECSLRM,
|
||||
SixelDisplay,
|
||||
EraseColor,
|
||||
RectangularChangeExtent,
|
||||
PageCursorCoupling
|
||||
@ -293,6 +298,8 @@ namespace Microsoft::Console::VirtualTerminal
|
||||
TerminalInput& _terminalInput;
|
||||
TerminalOutput _termOutput;
|
||||
PageManager _pages;
|
||||
friend class SixelParser;
|
||||
std::shared_ptr<SixelParser> _sixelParser;
|
||||
std::unique_ptr<FontBuffer> _fontBuffer;
|
||||
std::shared_ptr<MacroBuffer> _macroBuffer;
|
||||
std::optional<unsigned int> _initialCodePage;
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
<ClCompile Include="..\InteractDispatch.cpp" />
|
||||
<ClCompile Include="..\MacroBuffer.cpp" />
|
||||
<ClCompile Include="..\PageManager.cpp" />
|
||||
<ClCompile Include="..\SixelParser.cpp" />
|
||||
<ClCompile Include="..\adaptDispatchGraphics.cpp" />
|
||||
<ClCompile Include="..\terminalOutput.cpp" />
|
||||
<ClCompile Include="..\precomp.cpp">
|
||||
@ -31,6 +32,7 @@
|
||||
<ClInclude Include="..\ITerminalApi.hpp" />
|
||||
<ClInclude Include="..\MacroBuffer.hpp" />
|
||||
<ClInclude Include="..\PageManager.hpp" />
|
||||
<ClInclude Include="..\SixelParser.hpp" />
|
||||
<ClInclude Include="..\precomp.h" />
|
||||
<ClInclude Include="..\terminalOutput.hpp" />
|
||||
<ClInclude Include="..\ITermDispatch.hpp" />
|
||||
|
||||
@ -39,6 +39,9 @@
|
||||
<ClCompile Include="..\PageManager.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\SixelParser.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\adaptDispatch.hpp">
|
||||
@ -80,6 +83,9 @@
|
||||
<ClInclude Include="..\PageManager.hpp">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\SixelParser.hpp">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />
|
||||
|
||||
@ -35,6 +35,7 @@ SOURCES= \
|
||||
..\InteractDispatch.cpp \
|
||||
..\MacroBuffer.cpp \
|
||||
..\PageManager.cpp \
|
||||
..\SixelParser.cpp \
|
||||
..\adaptDispatchGraphics.cpp \
|
||||
..\terminalOutput.cpp \
|
||||
|
||||
|
||||
@ -142,6 +142,10 @@ public:
|
||||
|
||||
bool DoWTAction(const std::wstring_view /*string*/) override { return false; }
|
||||
|
||||
StringHandler DefineSixelImage(const VTInt /*macroParameter*/,
|
||||
const DispatchTypes::SixelBackground /*backgroundSelect*/,
|
||||
const VTParameter /*backgroundColor*/) override { return nullptr; }; // SIXEL
|
||||
|
||||
StringHandler DownloadDRCS(const VTInt /*fontNumber*/,
|
||||
const VTParameter /*startChar*/,
|
||||
const DispatchTypes::DrcsEraseControl /*eraseControl*/,
|
||||
|
||||
@ -1710,7 +1710,7 @@ public:
|
||||
_testGetSet->PrepData();
|
||||
VERIFY_IS_TRUE(_pDispatch->DeviceAttributes());
|
||||
|
||||
auto pwszExpectedResponse = L"\x1b[?61;1;6;7;14;21;22;23;24;28;32;42c";
|
||||
auto pwszExpectedResponse = L"\x1b[?61;1;4;6;7;14;21;22;23;24;28;32;42c";
|
||||
_testGetSet->ValidateInputEvent(pwszExpectedResponse);
|
||||
|
||||
Log::Comment(L"Test 2: Verify failure when ReturnResponse doesn't work.");
|
||||
|
||||
@ -720,6 +720,11 @@ IStateMachineEngine::StringHandler OutputStateMachineEngine::ActionDcsDispatch(c
|
||||
|
||||
switch (id)
|
||||
{
|
||||
case DcsActionCodes::SIXEL_DefineImage:
|
||||
handler = _dispatch->DefineSixelImage(parameters.at(0),
|
||||
parameters.at(1),
|
||||
parameters.at(2));
|
||||
break;
|
||||
case DcsActionCodes::DECDLD_DownloadDRCS:
|
||||
handler = _dispatch->DownloadDRCS(parameters.at(0),
|
||||
parameters.at(1),
|
||||
|
||||
@ -182,6 +182,7 @@ namespace Microsoft::Console::VirtualTerminal
|
||||
|
||||
enum DcsActionCodes : uint64_t
|
||||
{
|
||||
SIXEL_DefineImage = VTID("q"),
|
||||
DECDLD_DownloadDRCS = VTID("{"),
|
||||
DECAUPSS_AssignUserPreferenceSupplementalSet = VTID("!u"),
|
||||
DECDMAC_DefineMacro = VTID("!z"),
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
using namespace Microsoft::Console;
|
||||
using namespace std::string_view_literals;
|
||||
|
||||
static constexpr std::array<til::color, 256> standard256ColorTable{
|
||||
static constexpr std::array<til::color, 16> campbellColorTable{
|
||||
til::color{ 0x0C, 0x0C, 0x0C },
|
||||
til::color{ 0xC5, 0x0F, 0x1F },
|
||||
til::color{ 0x13, 0xA1, 0x0E },
|
||||
@ -25,246 +25,25 @@ static constexpr std::array<til::color, 256> standard256ColorTable{
|
||||
til::color{ 0xB4, 0x00, 0x9E },
|
||||
til::color{ 0x61, 0xD6, 0xD6 },
|
||||
til::color{ 0xF2, 0xF2, 0xF2 },
|
||||
};
|
||||
|
||||
static constexpr std::array<til::color, 16> vt340ColorTable{
|
||||
til::color{ 0x00, 0x00, 0x00 },
|
||||
til::color{ 0x00, 0x00, 0x5F },
|
||||
til::color{ 0x00, 0x00, 0x87 },
|
||||
til::color{ 0x00, 0x00, 0xAF },
|
||||
til::color{ 0x00, 0x00, 0xD7 },
|
||||
til::color{ 0x00, 0x00, 0xFF },
|
||||
til::color{ 0x00, 0x5F, 0x00 },
|
||||
til::color{ 0x00, 0x5F, 0x5F },
|
||||
til::color{ 0x00, 0x5F, 0x87 },
|
||||
til::color{ 0x00, 0x5F, 0xAF },
|
||||
til::color{ 0x00, 0x5F, 0xD7 },
|
||||
til::color{ 0x00, 0x5F, 0xFF },
|
||||
til::color{ 0x00, 0x87, 0x00 },
|
||||
til::color{ 0x00, 0x87, 0x5F },
|
||||
til::color{ 0x00, 0x87, 0x87 },
|
||||
til::color{ 0x00, 0x87, 0xAF },
|
||||
til::color{ 0x00, 0x87, 0xD7 },
|
||||
til::color{ 0x00, 0x87, 0xFF },
|
||||
til::color{ 0x00, 0xAF, 0x00 },
|
||||
til::color{ 0x00, 0xAF, 0x5F },
|
||||
til::color{ 0x00, 0xAF, 0x87 },
|
||||
til::color{ 0x00, 0xAF, 0xAF },
|
||||
til::color{ 0x00, 0xAF, 0xD7 },
|
||||
til::color{ 0x00, 0xAF, 0xFF },
|
||||
til::color{ 0x00, 0xD7, 0x00 },
|
||||
til::color{ 0x00, 0xD7, 0x5F },
|
||||
til::color{ 0x00, 0xD7, 0x87 },
|
||||
til::color{ 0x00, 0xD7, 0xAF },
|
||||
til::color{ 0x00, 0xD7, 0xD7 },
|
||||
til::color{ 0x00, 0xD7, 0xFF },
|
||||
til::color{ 0x00, 0xFF, 0x00 },
|
||||
til::color{ 0x00, 0xFF, 0x5F },
|
||||
til::color{ 0x00, 0xFF, 0x87 },
|
||||
til::color{ 0x00, 0xFF, 0xAF },
|
||||
til::color{ 0x00, 0xFF, 0xD7 },
|
||||
til::color{ 0x00, 0xFF, 0xFF },
|
||||
til::color{ 0x5F, 0x00, 0x00 },
|
||||
til::color{ 0x5F, 0x00, 0x5F },
|
||||
til::color{ 0x5F, 0x00, 0x87 },
|
||||
til::color{ 0x5F, 0x00, 0xAF },
|
||||
til::color{ 0x5F, 0x00, 0xD7 },
|
||||
til::color{ 0x5F, 0x00, 0xFF },
|
||||
til::color{ 0x5F, 0x5F, 0x00 },
|
||||
til::color{ 0x5F, 0x5F, 0x5F },
|
||||
til::color{ 0x5F, 0x5F, 0x87 },
|
||||
til::color{ 0x5F, 0x5F, 0xAF },
|
||||
til::color{ 0x5F, 0x5F, 0xD7 },
|
||||
til::color{ 0x5F, 0x5F, 0xFF },
|
||||
til::color{ 0x5F, 0x87, 0x00 },
|
||||
til::color{ 0x5F, 0x87, 0x5F },
|
||||
til::color{ 0x5F, 0x87, 0x87 },
|
||||
til::color{ 0x5F, 0x87, 0xAF },
|
||||
til::color{ 0x5F, 0x87, 0xD7 },
|
||||
til::color{ 0x5F, 0x87, 0xFF },
|
||||
til::color{ 0x5F, 0xAF, 0x00 },
|
||||
til::color{ 0x5F, 0xAF, 0x5F },
|
||||
til::color{ 0x5F, 0xAF, 0x87 },
|
||||
til::color{ 0x5F, 0xAF, 0xAF },
|
||||
til::color{ 0x5F, 0xAF, 0xD7 },
|
||||
til::color{ 0x5F, 0xAF, 0xFF },
|
||||
til::color{ 0x5F, 0xD7, 0x00 },
|
||||
til::color{ 0x5F, 0xD7, 0x5F },
|
||||
til::color{ 0x5F, 0xD7, 0x87 },
|
||||
til::color{ 0x5F, 0xD7, 0xAF },
|
||||
til::color{ 0x5F, 0xD7, 0xD7 },
|
||||
til::color{ 0x5F, 0xD7, 0xFF },
|
||||
til::color{ 0x5F, 0xFF, 0x00 },
|
||||
til::color{ 0x5F, 0xFF, 0x5F },
|
||||
til::color{ 0x5F, 0xFF, 0x87 },
|
||||
til::color{ 0x5F, 0xFF, 0xAF },
|
||||
til::color{ 0x5F, 0xFF, 0xD7 },
|
||||
til::color{ 0x5F, 0xFF, 0xFF },
|
||||
til::color{ 0x87, 0x00, 0x00 },
|
||||
til::color{ 0x87, 0x00, 0x5F },
|
||||
til::color{ 0x87, 0x00, 0x87 },
|
||||
til::color{ 0x87, 0x00, 0xAF },
|
||||
til::color{ 0x87, 0x00, 0xD7 },
|
||||
til::color{ 0x87, 0x00, 0xFF },
|
||||
til::color{ 0x87, 0x5F, 0x00 },
|
||||
til::color{ 0x87, 0x5F, 0x5F },
|
||||
til::color{ 0x87, 0x5F, 0x87 },
|
||||
til::color{ 0x87, 0x5F, 0xAF },
|
||||
til::color{ 0x87, 0x5F, 0xD7 },
|
||||
til::color{ 0x87, 0x5F, 0xFF },
|
||||
til::color{ 0x87, 0x87, 0x00 },
|
||||
til::color{ 0x87, 0x87, 0x5F },
|
||||
til::color{ 0x87, 0x87, 0x87 },
|
||||
til::color{ 0x87, 0x87, 0xAF },
|
||||
til::color{ 0x87, 0x87, 0xD7 },
|
||||
til::color{ 0x87, 0x87, 0xFF },
|
||||
til::color{ 0x87, 0xAF, 0x00 },
|
||||
til::color{ 0x87, 0xAF, 0x5F },
|
||||
til::color{ 0x87, 0xAF, 0x87 },
|
||||
til::color{ 0x87, 0xAF, 0xAF },
|
||||
til::color{ 0x87, 0xAF, 0xD7 },
|
||||
til::color{ 0x87, 0xAF, 0xFF },
|
||||
til::color{ 0x87, 0xD7, 0x00 },
|
||||
til::color{ 0x87, 0xD7, 0x5F },
|
||||
til::color{ 0x87, 0xD7, 0x87 },
|
||||
til::color{ 0x87, 0xD7, 0xAF },
|
||||
til::color{ 0x87, 0xD7, 0xD7 },
|
||||
til::color{ 0x87, 0xD7, 0xFF },
|
||||
til::color{ 0x87, 0xFF, 0x00 },
|
||||
til::color{ 0x87, 0xFF, 0x5F },
|
||||
til::color{ 0x87, 0xFF, 0x87 },
|
||||
til::color{ 0x87, 0xFF, 0xAF },
|
||||
til::color{ 0x87, 0xFF, 0xD7 },
|
||||
til::color{ 0x87, 0xFF, 0xFF },
|
||||
til::color{ 0xAF, 0x00, 0x00 },
|
||||
til::color{ 0xAF, 0x00, 0x5F },
|
||||
til::color{ 0xAF, 0x00, 0x87 },
|
||||
til::color{ 0xAF, 0x00, 0xAF },
|
||||
til::color{ 0xAF, 0x00, 0xD7 },
|
||||
til::color{ 0xAF, 0x00, 0xFF },
|
||||
til::color{ 0xAF, 0x5F, 0x00 },
|
||||
til::color{ 0xAF, 0x5F, 0x5F },
|
||||
til::color{ 0xAF, 0x5F, 0x87 },
|
||||
til::color{ 0xAF, 0x5F, 0xAF },
|
||||
til::color{ 0xAF, 0x5F, 0xD7 },
|
||||
til::color{ 0xAF, 0x5F, 0xFF },
|
||||
til::color{ 0xAF, 0x87, 0x00 },
|
||||
til::color{ 0xAF, 0x87, 0x5F },
|
||||
til::color{ 0xAF, 0x87, 0x87 },
|
||||
til::color{ 0xAF, 0x87, 0xAF },
|
||||
til::color{ 0xAF, 0x87, 0xD7 },
|
||||
til::color{ 0xAF, 0x87, 0xFF },
|
||||
til::color{ 0xAF, 0xAF, 0x00 },
|
||||
til::color{ 0xAF, 0xAF, 0x5F },
|
||||
til::color{ 0xAF, 0xAF, 0x87 },
|
||||
til::color{ 0xAF, 0xAF, 0xAF },
|
||||
til::color{ 0xAF, 0xAF, 0xD7 },
|
||||
til::color{ 0xAF, 0xAF, 0xFF },
|
||||
til::color{ 0xAF, 0xD7, 0x00 },
|
||||
til::color{ 0xAF, 0xD7, 0x5F },
|
||||
til::color{ 0xAF, 0xD7, 0x87 },
|
||||
til::color{ 0xAF, 0xD7, 0xAF },
|
||||
til::color{ 0xAF, 0xD7, 0xD7 },
|
||||
til::color{ 0xAF, 0xD7, 0xFF },
|
||||
til::color{ 0xAF, 0xFF, 0x00 },
|
||||
til::color{ 0xAF, 0xFF, 0x5F },
|
||||
til::color{ 0xAF, 0xFF, 0x87 },
|
||||
til::color{ 0xAF, 0xFF, 0xAF },
|
||||
til::color{ 0xAF, 0xFF, 0xD7 },
|
||||
til::color{ 0xAF, 0xFF, 0xFF },
|
||||
til::color{ 0xD7, 0x00, 0x00 },
|
||||
til::color{ 0xD7, 0x00, 0x5F },
|
||||
til::color{ 0xD7, 0x00, 0x87 },
|
||||
til::color{ 0xD7, 0x00, 0xAF },
|
||||
til::color{ 0xD7, 0x00, 0xD7 },
|
||||
til::color{ 0xD7, 0x00, 0xFF },
|
||||
til::color{ 0xD7, 0x5F, 0x00 },
|
||||
til::color{ 0xD7, 0x5F, 0x5F },
|
||||
til::color{ 0xD7, 0x5F, 0x87 },
|
||||
til::color{ 0xD7, 0x5F, 0xAF },
|
||||
til::color{ 0xD7, 0x5F, 0xD7 },
|
||||
til::color{ 0xD7, 0x5F, 0xFF },
|
||||
til::color{ 0xD7, 0x87, 0x00 },
|
||||
til::color{ 0xD7, 0x87, 0x5F },
|
||||
til::color{ 0xD7, 0x87, 0x87 },
|
||||
til::color{ 0xD7, 0x87, 0xAF },
|
||||
til::color{ 0xD7, 0x87, 0xD7 },
|
||||
til::color{ 0xD7, 0x87, 0xFF },
|
||||
til::color{ 0xD7, 0xAF, 0x00 },
|
||||
til::color{ 0xD7, 0xAF, 0x5F },
|
||||
til::color{ 0xD7, 0xAF, 0x87 },
|
||||
til::color{ 0xD7, 0xAF, 0xAF },
|
||||
til::color{ 0xD7, 0xAF, 0xD7 },
|
||||
til::color{ 0xD7, 0xAF, 0xFF },
|
||||
til::color{ 0xD7, 0xD7, 0x00 },
|
||||
til::color{ 0xD7, 0xD7, 0x5F },
|
||||
til::color{ 0xD7, 0xD7, 0x87 },
|
||||
til::color{ 0xD7, 0xD7, 0xAF },
|
||||
til::color{ 0xD7, 0xD7, 0xD7 },
|
||||
til::color{ 0xD7, 0xD7, 0xFF },
|
||||
til::color{ 0xD7, 0xFF, 0x00 },
|
||||
til::color{ 0xD7, 0xFF, 0x5F },
|
||||
til::color{ 0xD7, 0xFF, 0x87 },
|
||||
til::color{ 0xD7, 0xFF, 0xAF },
|
||||
til::color{ 0xD7, 0xFF, 0xD7 },
|
||||
til::color{ 0xD7, 0xFF, 0xFF },
|
||||
til::color{ 0xFF, 0x00, 0x00 },
|
||||
til::color{ 0xFF, 0x00, 0x5F },
|
||||
til::color{ 0xFF, 0x00, 0x87 },
|
||||
til::color{ 0xFF, 0x00, 0xAF },
|
||||
til::color{ 0xFF, 0x00, 0xD7 },
|
||||
til::color{ 0xFF, 0x00, 0xFF },
|
||||
til::color{ 0xFF, 0x5F, 0x00 },
|
||||
til::color{ 0xFF, 0x5F, 0x5F },
|
||||
til::color{ 0xFF, 0x5F, 0x87 },
|
||||
til::color{ 0xFF, 0x5F, 0xAF },
|
||||
til::color{ 0xFF, 0x5F, 0xD7 },
|
||||
til::color{ 0xFF, 0x5F, 0xFF },
|
||||
til::color{ 0xFF, 0x87, 0x00 },
|
||||
til::color{ 0xFF, 0x87, 0x5F },
|
||||
til::color{ 0xFF, 0x87, 0x87 },
|
||||
til::color{ 0xFF, 0x87, 0xAF },
|
||||
til::color{ 0xFF, 0x87, 0xD7 },
|
||||
til::color{ 0xFF, 0x87, 0xFF },
|
||||
til::color{ 0xFF, 0xAF, 0x00 },
|
||||
til::color{ 0xFF, 0xAF, 0x5F },
|
||||
til::color{ 0xFF, 0xAF, 0x87 },
|
||||
til::color{ 0xFF, 0xAF, 0xAF },
|
||||
til::color{ 0xFF, 0xAF, 0xD7 },
|
||||
til::color{ 0xFF, 0xAF, 0xFF },
|
||||
til::color{ 0xFF, 0xD7, 0x00 },
|
||||
til::color{ 0xFF, 0xD7, 0x5F },
|
||||
til::color{ 0xFF, 0xD7, 0x87 },
|
||||
til::color{ 0xFF, 0xD7, 0xAF },
|
||||
til::color{ 0xFF, 0xD7, 0xD7 },
|
||||
til::color{ 0xFF, 0xD7, 0xFF },
|
||||
til::color{ 0xFF, 0xFF, 0x00 },
|
||||
til::color{ 0xFF, 0xFF, 0x5F },
|
||||
til::color{ 0xFF, 0xFF, 0x87 },
|
||||
til::color{ 0xFF, 0xFF, 0xAF },
|
||||
til::color{ 0xFF, 0xFF, 0xD7 },
|
||||
til::color{ 0xFF, 0xFF, 0xFF },
|
||||
til::color{ 0x08, 0x08, 0x08 },
|
||||
til::color{ 0x12, 0x12, 0x12 },
|
||||
til::color{ 0x1C, 0x1C, 0x1C },
|
||||
til::color{ 0x26, 0x26, 0x26 },
|
||||
til::color{ 0x30, 0x30, 0x30 },
|
||||
til::color{ 0x3A, 0x3A, 0x3A },
|
||||
til::color{ 0x44, 0x44, 0x44 },
|
||||
til::color{ 0x4E, 0x4E, 0x4E },
|
||||
til::color{ 0x58, 0x58, 0x58 },
|
||||
til::color{ 0x62, 0x62, 0x62 },
|
||||
til::color{ 0x6C, 0x6C, 0x6C },
|
||||
til::color{ 0x76, 0x76, 0x76 },
|
||||
til::color{ 0x80, 0x80, 0x80 },
|
||||
til::color{ 0x8A, 0x8A, 0x8A },
|
||||
til::color{ 0x94, 0x94, 0x94 },
|
||||
til::color{ 0x9E, 0x9E, 0x9E },
|
||||
til::color{ 0xA8, 0xA8, 0xA8 },
|
||||
til::color{ 0xB2, 0xB2, 0xB2 },
|
||||
til::color{ 0xBC, 0xBC, 0xBC },
|
||||
til::color{ 0xC6, 0xC6, 0xC6 },
|
||||
til::color{ 0xD0, 0xD0, 0xD0 },
|
||||
til::color{ 0xDA, 0xDA, 0xDA },
|
||||
til::color{ 0xE4, 0xE4, 0xE4 },
|
||||
til::color{ 0xEE, 0xEE, 0xEE },
|
||||
til::color{ 0x33, 0x33, 0xCC },
|
||||
til::color{ 0xCC, 0x24, 0x24 },
|
||||
til::color{ 0x33, 0xCC, 0x33 },
|
||||
til::color{ 0xCC, 0x33, 0xCC },
|
||||
til::color{ 0x33, 0xCC, 0xCC },
|
||||
til::color{ 0xCC, 0xCC, 0x33 },
|
||||
til::color{ 0x78, 0x78, 0x78 },
|
||||
til::color{ 0x45, 0x45, 0x45 },
|
||||
til::color{ 0x57, 0x57, 0x99 },
|
||||
til::color{ 0x99, 0x45, 0x45 },
|
||||
til::color{ 0x57, 0x99, 0x57 },
|
||||
til::color{ 0x99, 0x57, 0x99 },
|
||||
til::color{ 0x57, 0x99, 0x99 },
|
||||
til::color{ 0x99, 0x99, 0x57 },
|
||||
til::color{ 0xCC, 0xCC, 0xCC },
|
||||
};
|
||||
|
||||
static constexpr til::presorted_static_map xorgAppVariantColorTable{
|
||||
@ -437,7 +216,7 @@ static constexpr til::presorted_static_map xorgAppColorTable{
|
||||
|
||||
std::span<const til::color> Utils::CampbellColorTable() noexcept
|
||||
{
|
||||
return std::span{ standard256ColorTable }.first(16);
|
||||
return std::span{ campbellColorTable };
|
||||
}
|
||||
|
||||
// Function Description:
|
||||
@ -446,10 +225,58 @@ std::span<const til::color> Utils::CampbellColorTable() noexcept
|
||||
// - table: a color table to be filled
|
||||
// Return Value:
|
||||
// - <none>
|
||||
void Utils::InitializeColorTable(const std::span<COLORREF> table)
|
||||
void Utils::InitializeColorTable(const std::span<COLORREF> table) noexcept
|
||||
{
|
||||
const auto tableSize = std::min(table.size(), standard256ColorTable.size());
|
||||
std::copy_n(standard256ColorTable.begin(), tableSize, table.begin());
|
||||
InitializeANSIColorTable(table);
|
||||
InitializeExtendedColorTable(table);
|
||||
}
|
||||
|
||||
void Utils::InitializeANSIColorTable(const std::span<COLORREF> table) noexcept
|
||||
{
|
||||
if (table.size() >= campbellColorTable.size())
|
||||
{
|
||||
std::copy_n(campbellColorTable.begin(), campbellColorTable.size(), table.begin());
|
||||
}
|
||||
}
|
||||
|
||||
void Utils::InitializeVT340ColorTable(const std::span<COLORREF> table) noexcept
|
||||
{
|
||||
if (table.size() >= vt340ColorTable.size())
|
||||
{
|
||||
std::copy_n(vt340ColorTable.begin(), vt340ColorTable.size(), table.begin());
|
||||
}
|
||||
}
|
||||
|
||||
// Function Description:
|
||||
// - Fill color table entries from 16 to 255 with the default colors used by
|
||||
// modern terminals. This includes a range of colors from a 6x6x6 color cube
|
||||
// for entries 16 to 231, and 24 shades of gray for entries 232 to 255.
|
||||
// Arguments:
|
||||
// - table: a color table to be filled
|
||||
// Return Value:
|
||||
// - <none>
|
||||
constexpr void Utils::InitializeExtendedColorTable(const std::span<COLORREF> table, const bool monochrome) noexcept
|
||||
{
|
||||
if (table.size() >= 256)
|
||||
{
|
||||
for (size_t i = 0; i < 216; i++)
|
||||
{
|
||||
constexpr auto scale = std::array<uint8_t, 6>{ 0, 0x5F, 0x87, 0xAF, 0xD7, 0xFF };
|
||||
auto r = til::at(scale, (i / 36) % 6);
|
||||
auto g = til::at(scale, (i / 6) % 6);
|
||||
auto b = til::at(scale, i % 6);
|
||||
if (monochrome)
|
||||
{
|
||||
r = g = b = (r + g + b) / 3;
|
||||
}
|
||||
til::at(table, i + 16) = til::color{ r, g, b };
|
||||
}
|
||||
for (size_t i = 0; i < 24; i++)
|
||||
{
|
||||
const auto l = gsl::narrow_cast<uint8_t>(i * 10 + 8);
|
||||
til::at(table, i + 232) = til::color{ l, l, l };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning(push)
|
||||
|
||||
@ -12,7 +12,10 @@ Abstract:
|
||||
|
||||
namespace Microsoft::Console::Utils
|
||||
{
|
||||
void InitializeColorTable(const std::span<COLORREF> table);
|
||||
void InitializeColorTable(const std::span<COLORREF> table) noexcept;
|
||||
void InitializeANSIColorTable(const std::span<COLORREF> table) noexcept;
|
||||
void InitializeVT340ColorTable(const std::span<COLORREF> table) noexcept;
|
||||
constexpr void InitializeExtendedColorTable(const std::span<COLORREF> table, const bool monochrome = false) noexcept;
|
||||
std::span<const til::color> CampbellColorTable() noexcept;
|
||||
|
||||
std::optional<til::color> ColorFromXOrgAppColorName(const std::wstring_view wstr) noexcept;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user