diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index e33937c7fb5..56535aaa8b7 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -1145,7 +1145,7 @@ std::string TextBuffer::GenHTML(const TextAndColor& rows, const int fontHeightPo } const auto writeAccumulatedChars = [&](bool includeCurrent) { - if (col > startOffset) + if (col >= startOffset) { const auto unescapedText = ConvertToA(CP_UTF8, std::wstring_view(rows.text.at(row)).substr(startOffset, col - startOffset + includeCurrent)); for (const auto c : unescapedText) @@ -1240,3 +1240,185 @@ std::string TextBuffer::GenHTML(const TextAndColor& rows, const int fontHeightPo return {}; } } + +// Routine Description: +// - Generates an RTF document based on the passed in text and color data +// RTF 1.5 Spec: https://www.biblioscape.com/rtf15_spec.htm +// Arguments: +// - rows - the text and color data we will format & encapsulate +// - backgroundColor - default background color for characters, also used in padding +// - fontHeightPoints - the unscaled font height +// - fontFaceName - the name of the font used +// - htmlTitle - value used in title tag of html header. Used to name the application +// Return Value: +// - string containing the generated RTF +std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoints, const std::wstring_view fontFaceName, const COLORREF backgroundColor) +{ + try + { + std::ostringstream rtfBuilder; + + // start rtf + rtfBuilder << "{"; + + // Standard RTF header. + // This is similar to the header gnerated by WordPad. + // \ansi - specifies that the ANSI char set is used in the current doc + // \ansicpg1252 - represents the ANSI code page which is used to perform the Unicode to ANSI conversion when writing RTF text + // \deff0 - specifes that the default font for the document is the one at index 0 in the font table + // \nouicompat - ? + rtfBuilder << "\\rtf1\\ansi\\ansicpg1252\\deff0\\nouicompat"; + + // font table + rtfBuilder << "{\\fonttbl{\\f0\\fmodern\\fcharset0 " << ConvertToA(CP_UTF8, fontFaceName) << ";}}"; + + // map to keep track of colors: + // keys are colors represented by COLORREF + // values are indices of the corresponding colors in the color table + std::unordered_map colorMap; + int nextColorIndex = 1; // leave 0 for the default color and start from 1. + + // RTF color table + std::ostringstream colorTableBuilder; + colorTableBuilder << "{\\colortbl ;"; + colorTableBuilder << "\\red" << static_cast(GetRValue(backgroundColor)) + << "\\green" << static_cast(GetGValue(backgroundColor)) + << "\\blue" << static_cast(GetBValue(backgroundColor)) + << ";"; + colorMap[backgroundColor] = nextColorIndex++; + + // content + std::ostringstream contentBuilder; + contentBuilder << "\\viewkind4\\uc4"; + + // paragraph styles + // \fs specificies font size in half-points i.e. \fs20 results in a font size + // of 10 pts. That's why, font size is multiplied by 2 here. + contentBuilder << "\\pard\\slmult1\\f0\\fs" << std::to_string(2 * fontHeightPoints) + << "\\highlight1" + << " "; + + std::optional fgColor = std::nullopt; + std::optional bkColor = std::nullopt; + for (size_t row = 0; row < rows.text.size(); ++row) + { + size_t startOffset = 0; + + if (row != 0) + { + contentBuilder << "\\line "; // new line + } + + for (size_t col = 0; col < rows.text.at(row).length(); ++col) + { + const bool isLastCharInRow = + col == rows.text.at(row).length() - 1 || + rows.text.at(row).at(col + 1) == '\r' || + rows.text.at(row).at(col + 1) == '\n'; + + bool colorChanged = false; + if (!fgColor.has_value() || rows.FgAttr.at(row).at(col) != fgColor.value()) + { + fgColor = rows.FgAttr.at(row).at(col); + colorChanged = true; + } + + if (!bkColor.has_value() || rows.BkAttr.at(row).at(col) != bkColor.value()) + { + bkColor = rows.BkAttr.at(row).at(col); + colorChanged = true; + } + + const auto writeAccumulatedChars = [&](bool includeCurrent) { + if (col >= startOffset) + { + const auto unescapedText = ConvertToA(CP_UTF8, std::wstring_view(rows.text.at(row)).substr(startOffset, col - startOffset + includeCurrent)); + for (const auto c : unescapedText) + { + switch (c) + { + case '\\': + case '{': + case '}': + contentBuilder << "\\" << c; + break; + default: + contentBuilder << c; + } + } + + startOffset = col; + } + }; + + if (colorChanged) + { + writeAccumulatedChars(false); + + int bkColorIndex = 0; + if (colorMap.find(bkColor.value()) != colorMap.end()) + { + // color already exists in the map, just retrieve the index + bkColorIndex = colorMap[bkColor.value()]; + } + else + { + // color not present in the map, so add it + colorTableBuilder << "\\red" << static_cast(GetRValue(bkColor.value())) + << "\\green" << static_cast(GetGValue(bkColor.value())) + << "\\blue" << static_cast(GetBValue(bkColor.value())) + << ";"; + colorMap[bkColor.value()] = nextColorIndex; + bkColorIndex = nextColorIndex++; + } + + int fgColorIndex = 0; + if (colorMap.find(fgColor.value()) != colorMap.end()) + { + // color already exists in the map, just retrieve the index + fgColorIndex = colorMap[fgColor.value()]; + } + else + { + // color not present in the map, so add it + colorTableBuilder << "\\red" << static_cast(GetRValue(fgColor.value())) + << "\\green" << static_cast(GetGValue(fgColor.value())) + << "\\blue" << static_cast(GetBValue(fgColor.value())) + << ";"; + colorMap[fgColor.value()] = nextColorIndex; + fgColorIndex = nextColorIndex++; + } + + contentBuilder << "\\highglight" << bkColorIndex + << "\\cf" << fgColorIndex + << " "; + } + + if (isLastCharInRow) + { + writeAccumulatedChars(true); + break; + } + } + } + + // end colortbl + colorTableBuilder << "}"; + + // add color table to the final RTF + rtfBuilder << colorTableBuilder.str(); + + // add the text content to the final RTF + rtfBuilder << contentBuilder.str(); + + // end rtf + rtfBuilder << "}"; + + return rtfBuilder.str(); + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + return {}; + } +} diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index b9282163a5a..b66581fe42c 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -150,6 +150,11 @@ class TextBuffer final const COLORREF backgroundColor, const std::string& htmlTitle); + static std::string GenRTF(const TextAndColor& rows, + const int fontHeightPoints, + const std::wstring_view fontFaceName, + const COLORREF backgroundColor); + private: std::deque _storage; Cursor _cursor; diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index f17a19a5b7f..688894e02dc 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1101,6 +1101,13 @@ namespace winrt::TerminalApp::implementation dataPack.SetHtmlFormat(htmlData); } + // copy rtf data to dataPack + const auto rtfData = copiedData.Rtf(); + if (!rtfData.empty()) + { + dataPack.SetRtf(rtfData); + } + try { Clipboard::SetContent(dataPack); diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index a75cbee7c06..a479064b33b 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1468,13 +1468,21 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation _settings.DefaultBackground(), "Windows Terminal"); + // convert to RTF format + const auto rtfData = TextBuffer::GenRTF(bufferData, + _actualFont.GetUnscaledSize().Y, + _actualFont.GetFaceName(), + _settings.DefaultBackground()); + if (!_terminal->IsCopyOnSelectActive()) { _terminal->ClearSelection(); } // send data up for clipboard - auto copyArgs = winrt::make_self(winrt::hstring(textData.data(), gsl::narrow(textData.size())), winrt::to_hstring(htmlData)); + auto copyArgs = winrt::make_self(winrt::hstring(textData.data(), gsl::narrow(textData.size())), + winrt::to_hstring(htmlData), + winrt::to_hstring(rtfData)); _clipboardCopyHandlers(*this, *copyArgs); return true; } diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 146ae5f222d..de0a1a10466 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -19,16 +19,19 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation public CopyToClipboardEventArgsT { public: - CopyToClipboardEventArgs(hstring text, hstring html) : + CopyToClipboardEventArgs(hstring text, hstring html, hstring rtf) : _text(text), - _html(html) {} + _html(html), + _rtf(rtf) {} hstring Text() { return _text; }; hstring Html() { return _html; }; + hstring Rtf() { return _rtf; }; private: hstring _text; hstring _html; + hstring _rtf; }; struct PasteFromClipboardEventArgs : diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 82452d14add..2fc56ed7e78 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -11,6 +11,7 @@ namespace Microsoft.Terminal.TerminalControl { String Text { get; }; String Html { get; }; + String Rtf { get; }; } runtimeclass PasteFromClipboardEventArgs