-
Notifications
You must be signed in to change notification settings - Fork 9k
Add support for Sixel images in conhost #17421
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
6eee84a
Hook up the Sixel DCS sequence to the dispatch classes.
j4james dda425a
Provide storage for images in the text buffer.
j4james 4b5d784
Create a Sixel parsing class.
j4james 8795ba3
Output image content from the renderer.
j4james 4acc50d
Tie everything together.
j4james 004a183
Add terms to spellbot dictionary.
j4james 7916799
Update the device attributes test.
j4james 518cc05
Correct campbell color table size.
j4james e52eef1
Avoid debug assertions when advancing iterators.
j4james 673bef9
Make sure the pixel aspect ratio isn't too big.
j4james 4853123
Prevent overflow when copying to resized buffer.
j4james 8c7587d
Fix comment typo.
j4james 91b79c0
Merge branch 'main' into feature-sixel
j4james 6c072bb
Trigger redraws via the active page buffer.
j4james dc099c6
Make sure image content is erased when console APIs overwrite it.
j4james bf5c6fe
Erase image content when line rendition changes.
j4james baba91b
Correct the VT125 color handling.
j4james da3f95d
Merge branch 'main' into feature-sixel
j4james adf507f
Use the newly merged mode reporting mechanism.
j4james 48d203d
PR feedback
j4james 6401446
Merge branch 'main' into feature-sixel
j4james File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
lhecker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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) | ||
lhecker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| 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) | ||
lhecker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| // 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; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that I'm using an |
||
| til::CoordType _columnBegin = 0; | ||
| til::CoordType _columnEnd = 0; | ||
| til::CoordType _pixelWidth = 0; | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.