From 4004569adda017d0f63bcfc91f067660cdcac6d3 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Fri, 9 Aug 2024 22:20:45 +0100 Subject: [PATCH] Implement the DECCTR color table report. --- src/terminal/adapter/adaptDispatch.cpp | 31 +++++++++- src/types/inc/utils.hpp | 2 + src/types/utils.cpp | 82 ++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 91fe79668a2..78b1299ec25 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -4211,8 +4211,37 @@ ITermDispatch::StringHandler AdaptDispatch::RestoreTerminalState(const DispatchT // - colorModel - the color model to use in the report (1 = HLS, 2 = RGB). // Return Value: // - None -void AdaptDispatch::_ReportColorTable(const DispatchTypes::ColorModel /*colorModel*/) const +void AdaptDispatch::_ReportColorTable(const DispatchTypes::ColorModel colorModel) const { + using namespace std::string_view_literals; + + // A valid response always starts with DCS 2 $ s. + fmt::basic_memory_buffer response; + response.append(L"\033P2$s"sv); + + const auto modelNumber = static_cast(colorModel); + for (size_t colorNumber = 0; colorNumber < TextColor::TABLE_SIZE; colorNumber++) + { + response.append(colorNumber > 0 ? L"/"sv : L""sv); + const auto color = til::color(_renderSettings.GetColorTableEntry(colorNumber)); + auto x = 0, y = 0, z = 0; + switch (colorModel) + { + case DispatchTypes::ColorModel::HLS: + std::tie(x, y, z) = Utils::ColorToHLS(color); + break; + case DispatchTypes::ColorModel::RGB: + std::tie(x, y, z) = Utils::ColorToRGB100(color); + break; + default: + return; + } + fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{};{};{};{};{}"), colorNumber, modelNumber, x, y, z); + } + + // An ST ends the sequence. + response.append(L"\033\\"sv); + _api.ReturnResponse({ response.data(), response.size() }); } // Method Description: diff --git a/src/types/inc/utils.hpp b/src/types/inc/utils.hpp index 18221bac2da..af730836e66 100644 --- a/src/types/inc/utils.hpp +++ b/src/types/inc/utils.hpp @@ -60,7 +60,9 @@ namespace Microsoft::Console::Utils std::optional ColorFromXTermColor(const std::wstring_view wstr) noexcept; std::optional ColorFromXParseColorSpec(const std::wstring_view wstr) noexcept; til::color ColorFromHLS(const int h, const int l, const int s) noexcept; + std::tuple ColorToHLS(const til::color color) noexcept; til::color ColorFromRGB100(const int r, const int g, const int b) noexcept; + std::tuple ColorToRGB100(const til::color color) noexcept; bool HexToUint(const wchar_t wch, unsigned int& value) noexcept; bool StringToUint(const std::wstring_view wstr, unsigned int& value); diff --git a/src/types/utils.cpp b/src/types/utils.cpp index 0d099f7ad28..3686ea9fb5f 100644 --- a/src/types/utils.cpp +++ b/src/types/utils.cpp @@ -374,6 +374,32 @@ til::color Utils::ColorFromRGB100(const int r, const int g, const int b) noexcep return { red, green, blue }; } +// Function Description: +// - Returns the RGB percentage components of a given til::color value. +// Arguments: +// - color: the color being queried +// Return Value: +// - a tuple containing the three components +std::tuple Utils::ColorToRGB100(const til::color color) noexcept +{ + // The color class components are in the range 0 to 255, so we + // need to scale them by 100/255 to obtain percentage values. We + // can optimise this conversion with a pre-created lookup table. + static constexpr auto scale255To100 = [] { + std::array lut{}; + for (size_t i = 0; i < std::size(lut); i++) + { + lut.at(i) = gsl::narrow_cast((i * 100 + 128) / 255); + } + return lut; + }(); + + const auto red = til::at(scale255To100, color.r); + const auto green = til::at(scale255To100, color.g); + const auto blue = til::at(scale255To100, color.b); + return { red, green, blue }; +} + // Routine Description: // - Constructs a til::color value from HLS components. // Arguments: @@ -424,6 +450,62 @@ til::color Utils::ColorFromHLS(const int h, const int l, const int s) noexcept return { comp3, comp2, comp1 }; // cyan to blue } +// Function Description: +// - Returns the HLS components of a given til::color value. +// Arguments: +// - color: the color being queried +// Return Value: +// - a tuple containing the three components +std::tuple Utils::ColorToHLS(const til::color color) noexcept +{ + const auto red = color.r / 255.f; + const auto green = color.g / 255.f; + const auto blue = color.b / 255.f; + + // This calculation is based on the RGB to HSL algorithm described in + // Wikipedia: https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB + // We start by calculating the maximum and minimum component values. + const auto maxComp = std::max(std::max(red, green), blue); + const auto minComp = std::min(std::min(red, green), blue); + + // The chroma value is the range of those components. + const auto chroma = maxComp - minComp; + + // And the luma is the middle of the range. But we're actually calculating + // double that value here to save on a division. + const auto luma2 = (maxComp + minComp); + + // The saturation is half the chroma value divided by min(luma, 1-luma), + // but since the luma is already doubled, we can use the chroma as is. + const auto divisor = std::min(luma2, 2.f - luma2); + const auto sat = divisor > 0 ? chroma / divisor : 0.f; + + // Finally we calculate the hue, which is represented by the angle of a + // vector to a point in a color hexagon with blue, magenta, red, yellow, + // green, and cyan at its corners. As noted above, the DEC standard has + // blue at 0°, red at 120°, and green at 240°, which is slightly different + // from the way that hue is typically mapped in modern color models. + auto hue = 0.f; + if (chroma != 0) + { + if (maxComp == red) + hue = (green - blue) / chroma + 2.f; // magenta to yellow + else if (maxComp == green) + hue = (blue - red) / chroma + 4.f; // yellow to cyan + else if (maxComp == blue) + hue = (red - green) / chroma + 6.f; // cyan to magenta + } + + // The hue value calculated above is essentially a fractional offset from the + // six hexagon corners, so it has to be scaled by 60 to get the angle value. + // Luma and saturation are percentages so must be scaled by 100, but our luma + // value is already doubled, so only needs to be scaled by 50. + const auto h = static_cast(hue * 60.f + 0.5f) % 360; + const auto l = static_cast(luma2 * 50.f + 0.5f); + const auto s = static_cast(sat * 100.f + 0.5f); + return { h, l, s }; +} + // Routine Description: // - Converts a hex character to its equivalent integer value. // Arguments: