diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 7adf87508ae..6495be1be48 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -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 diff --git a/src/buffer/out/ImageSlice.cpp b/src/buffer/out/ImageSlice.cpp new file mode 100644 index 00000000000..37aacb31ce2 --- /dev/null +++ b/src/buffer/out/ImageSlice.cpp @@ -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 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(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(*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(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(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; + } +} diff --git a/src/buffer/out/ImageSlice.hpp b/src/buffer/out/ImageSlice.hpp new file mode 100644 index 00000000000..c17277851bb --- /dev/null +++ b/src/buffer/out/ImageSlice.hpp @@ -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 +#include + +class ROW; +class TextBuffer; + +class ImageSlice +{ +public: + using Pointer = std::unique_ptr; + + 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 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 _pixelBuffer; + til::CoordType _columnBegin = 0; + til::CoordType _columnEnd = 0; + til::CoordType _pixelWidth = 0; +}; diff --git a/src/buffer/out/Row.cpp b/src/buffer/out/Row.cpp index a58f1cbf02c..e85b113cd7d 100644 --- a/src/buffer/out/Row.cpp +++ b/src/buffer/out/Row.cpp @@ -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 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; diff --git a/src/buffer/out/Row.hpp b/src/buffer/out/Row.hpp index a13e6e7996a..f4462f0488d 100644 --- a/src/buffer/out/Row.hpp +++ b/src/buffer/out/Row.hpp @@ -5,6 +5,7 @@ #include +#include "ImageSlice.hpp" #include "LineRendition.hpp" #include "OutputCell.hpp" #include "OutputCellIterator.hpp" @@ -151,6 +152,8 @@ class ROW final const til::small_rle& Attributes() const noexcept; TextAttribute GetAttrByColumn(til::CoordType column) const; std::vector 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 @@ class ROW final til::small_rle _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 diff --git a/src/buffer/out/lib/bufferout.vcxproj b/src/buffer/out/lib/bufferout.vcxproj index 20385eff096..9362e88ea99 100644 --- a/src/buffer/out/lib/bufferout.vcxproj +++ b/src/buffer/out/lib/bufferout.vcxproj @@ -12,6 +12,7 @@ + @@ -32,6 +33,7 @@ + diff --git a/src/buffer/out/sources.inc b/src/buffer/out/sources.inc index 6611cc0aa5e..90422ec6c84 100644 --- a/src/buffer/out/sources.inc +++ b/src/buffer/out/sources.inc @@ -30,6 +30,7 @@ PRECOMPILED_INCLUDE = ..\precomp.h SOURCES= \ ..\cursor.cpp \ + ..\ImageSlice.cpp \ ..\OutputCell.cpp \ ..\OutputCellIterator.cpp \ ..\OutputCellRect.cpp \ diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index b54d0d88378..86987e68b59 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.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(state.columnBegin), gsl::narrow(state.columnBegin + copyAmount)); rowAttr.replace(gsl::narrow(restoreState.columnBegin), gsl::narrow(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(oldX), oldAttr.size()); diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index 7d3a0074297..a6a4ea88f7d 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -135,6 +135,7 @@ class TextBuffer final 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; diff --git a/src/host/_output.cpp b/src/host/_output.cpp index 44701989a51..bc8d6a104ad 100644 --- a/src/host/_output.cpp +++ b/src/host/_output.cpp @@ -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()) { diff --git a/src/host/directio.cpp b/src/host/directio.cpp index b93252ad1ef..8bc0bf36fd2 100644 --- a/src/host/directio.cpp +++ b/src/host/directio.cpp @@ -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; diff --git a/src/host/output.cpp b/src/host/output.cpp index 1958e0671a4..e57412f5310 100644 --- a/src/host/output.cpp +++ b/src/host/output.cpp @@ -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) diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index b5a89f6c09d..7bc2755721b 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -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 { diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h index ccb4da9fb4e..e022a38c3fa 100644 --- a/src/renderer/atlas/AtlasEngine.h +++ b/src/renderer/atlas/AtlasEngine.h @@ -45,6 +45,7 @@ namespace Microsoft::Console::Render::Atlas [[nodiscard]] HRESULT PaintBackground() noexcept override; [[nodiscard]] HRESULT PaintBufferLine(std::span 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 pData, bool usingSoftFont, bool isSettingDefaultBrushes) noexcept override; diff --git a/src/renderer/base/RenderEngineBase.cpp b/src/renderer/base/RenderEngineBase.cpp index 1847c1d287f..dfc54432f22 100644 --- a/src/renderer/base/RenderEngineBase.cpp +++ b/src/renderer/base/RenderEngineBase.cpp @@ -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. diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index 7d7a4b356eb..b3b431726d5 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -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())); + } } } } diff --git a/src/renderer/gdi/gdirenderer.hpp b/src/renderer/gdi/gdirenderer.hpp index 78a3f02953f..48eec99a920 100644 --- a/src/renderer/gdi/gdirenderer.hpp +++ b/src/renderer/gdi/gdirenderer.hpp @@ -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 _polyStrings; std::pmr::vector> _polyWidths; + std::vector _imageMask; + [[nodiscard]] HRESULT _InvalidCombine(const til::rect* const prc) noexcept; [[nodiscard]] HRESULT _InvalidOffset(const til::point* const ppt) noexcept; [[nodiscard]] HRESULT _InvalidRestrict() noexcept; diff --git a/src/renderer/gdi/paint.cpp b/src/renderer/gdi/paint.cpp index 6fd8d7521ee..17e0f66e4ee 100644 --- a/src/renderer/gdi/paint.cpp +++ b/src/renderer/gdi/paint.cpp @@ -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: diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp index a7afb4244ba..cdbcd774855 100644 --- a/src/renderer/inc/IRenderEngine.hpp +++ b/src/renderer/inc/IRenderEngine.hpp @@ -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 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 pData, bool usingSoftFont, bool isSettingDefaultBrushes) noexcept = 0; diff --git a/src/renderer/inc/RenderEngineBase.hpp b/src/renderer/inc/RenderEngineBase.hpp index 1108921edb5..2794fcf98da 100644 --- a/src/renderer/inc/RenderEngineBase.hpp +++ b/src/renderer/inc/RenderEngineBase.hpp @@ -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; diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp index 46b8a316d39..7ba0132c07c 100644 --- a/src/terminal/adapter/DispatchTypes.hpp +++ b/src/terminal/adapter/DispatchTypes.hpp @@ -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, diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index a83205b4503..a7af79a0958 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -149,6 +149,10 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch 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, diff --git a/src/terminal/adapter/PageManager.cpp b/src/terminal/adapter/PageManager.cpp index 9a0ae4de711..284bee8530a 100644 --- a/src/terminal/adapter/PageManager.cpp +++ b/src/terminal/adapter/PageManager.cpp @@ -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; diff --git a/src/terminal/adapter/SixelParser.cpp b/src/terminal/adapter/SixelParser.cpp new file mode 100644 index 00000000000..baf37f75eec --- /dev/null +++ b/src/terminal/adapter/SixelParser.cpp @@ -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 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(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(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(_maxColors - 1, 15); + _foregroundPixel = { .colorIndex = gsl::narrow_cast(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(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::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(c1) - static_cast(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(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(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(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(_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()); + } + } +} diff --git a/src/terminal/adapter/SixelParser.hpp b/src/terminal/adapter/SixelParser.hpp new file mode 100644 index 00000000000..097a07533be --- /dev/null +++ b/src/terminal/adapter/SixelParser.hpp @@ -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 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 _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 _colorMap = {}; + std::array _colorMapUsed = {}; + std::array _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 _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; + }; +} diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 1a4d5b431c4..9b35e29f458 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -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(*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 diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 4c9c167e928..d4e46c6feeb 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -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; std::unique_ptr _fontBuffer; std::shared_ptr _macroBuffer; std::optional _initialCodePage; diff --git a/src/terminal/adapter/lib/adapter.vcxproj b/src/terminal/adapter/lib/adapter.vcxproj index a05c8e5b83e..ef562d59e8a 100644 --- a/src/terminal/adapter/lib/adapter.vcxproj +++ b/src/terminal/adapter/lib/adapter.vcxproj @@ -16,6 +16,7 @@ + @@ -31,6 +32,7 @@ + diff --git a/src/terminal/adapter/lib/adapter.vcxproj.filters b/src/terminal/adapter/lib/adapter.vcxproj.filters index 501f6ce9bdc..061e9c378aa 100644 --- a/src/terminal/adapter/lib/adapter.vcxproj.filters +++ b/src/terminal/adapter/lib/adapter.vcxproj.filters @@ -39,6 +39,9 @@ Source Files + + Source Files + @@ -80,6 +83,9 @@ Header Files + + Header Files + diff --git a/src/terminal/adapter/sources.inc b/src/terminal/adapter/sources.inc index 3ffd6bed7c7..3dd7c387028 100644 --- a/src/terminal/adapter/sources.inc +++ b/src/terminal/adapter/sources.inc @@ -35,6 +35,7 @@ SOURCES= \ ..\InteractDispatch.cpp \ ..\MacroBuffer.cpp \ ..\PageManager.cpp \ + ..\SixelParser.cpp \ ..\adaptDispatchGraphics.cpp \ ..\terminalOutput.cpp \ diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index 1480c38fa1e..1b8ac3986fb 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -142,6 +142,10 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons 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*/, diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index 8d5ca44588a..44274d66a7d 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -1710,7 +1710,7 @@ class AdapterTest _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."); diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 17c9615a726..0c12c5e9b1f 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -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), diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index 62de23e9a4b..68097473995 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -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"), diff --git a/src/types/colorTable.cpp b/src/types/colorTable.cpp index e6498a6b0c8..0a67fe510c7 100644 --- a/src/types/colorTable.cpp +++ b/src/types/colorTable.cpp @@ -8,7 +8,7 @@ using namespace Microsoft::Console; using namespace std::string_view_literals; -static constexpr std::array standard256ColorTable{ +static constexpr std::array campbellColorTable{ til::color{ 0x0C, 0x0C, 0x0C }, til::color{ 0xC5, 0x0F, 0x1F }, til::color{ 0x13, 0xA1, 0x0E }, @@ -25,246 +25,25 @@ static constexpr std::array standard256ColorTable{ til::color{ 0xB4, 0x00, 0x9E }, til::color{ 0x61, 0xD6, 0xD6 }, til::color{ 0xF2, 0xF2, 0xF2 }, +}; + +static constexpr std::array 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 Utils::CampbellColorTable() noexcept { - return std::span{ standard256ColorTable }.first(16); + return std::span{ campbellColorTable }; } // Function Description: @@ -446,10 +225,58 @@ std::span Utils::CampbellColorTable() noexcept // - table: a color table to be filled // Return Value: // - -void Utils::InitializeColorTable(const std::span table) +void Utils::InitializeColorTable(const std::span table) noexcept +{ + InitializeANSIColorTable(table); + InitializeExtendedColorTable(table); +} + +void Utils::InitializeANSIColorTable(const std::span table) noexcept +{ + if (table.size() >= campbellColorTable.size()) + { + std::copy_n(campbellColorTable.begin(), campbellColorTable.size(), table.begin()); + } +} + +void Utils::InitializeVT340ColorTable(const std::span 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: +// - +constexpr void Utils::InitializeExtendedColorTable(const std::span table, const bool monochrome) noexcept { - const auto tableSize = std::min(table.size(), standard256ColorTable.size()); - std::copy_n(standard256ColorTable.begin(), tableSize, table.begin()); + if (table.size() >= 256) + { + for (size_t i = 0; i < 216; i++) + { + constexpr auto scale = std::array{ 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(i * 10 + 8); + til::at(table, i + 232) = til::color{ l, l, l }; + } + } } #pragma warning(push) diff --git a/src/types/inc/colorTable.hpp b/src/types/inc/colorTable.hpp index d81445fc184..8e2187dc98b 100644 --- a/src/types/inc/colorTable.hpp +++ b/src/types/inc/colorTable.hpp @@ -12,7 +12,10 @@ Module Name: namespace Microsoft::Console::Utils { - void InitializeColorTable(const std::span table); + void InitializeColorTable(const std::span table) noexcept; + void InitializeANSIColorTable(const std::span table) noexcept; + void InitializeVT340ColorTable(const std::span table) noexcept; + constexpr void InitializeExtendedColorTable(const std::span table, const bool monochrome = false) noexcept; std::span CampbellColorTable() noexcept; std::optional ColorFromXOrgAppColorName(const std::wstring_view wstr) noexcept;