Skip to content

Commit b1a05af

Browse files
[terminal] *EXPERIMENTAL* Good Image Protocol (work-in-progress)
1 parent 2354c29 commit b1a05af

15 files changed

+943
-10
lines changed

CMakeLists.txt

+5
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ option(CONTOUR_SANITIZE "Builds with Address sanitizer enabled [default: OFF]" "
5757
option(CONTOUR_STACKTRACE_ADDR2LINE "Uses addr2line to pretty-print SEGV stacktrace." ${ADDR2LINE_DEFAULT})
5858
option(CONTOUR_BUILD_WITH_MIMALLOC "Builds with mimalloc [default: OFF]" OFF)
5959
option(CONTOUR_INSTALL_TOOLS "Installs tools, if built [default: OFF]" OFF)
60+
option(CONTOUR_GOOD_IMAGE_PROTOCOL "Enables *EXPERIMENTAL* Good Image Protocol support [default: OFF]" OFF)
61+
62+
if(CONTOUR_GOOD_IMAGE_PROTOCOL)
63+
add_definitions(-DGOOD_IMAGE_PROTOCOL=1)
64+
endif()
6065

6166
if(NOT WIN32 AND NOT CONTOUR_SANITIZE AND NOT CMAKE_CONFIGURATION_TYPES)
6267
set(CONTOUR_SANITIZE "OFF" CACHE STRING "Choose the sanitizer mode." FORCE)

src/contour/ContourApp.cpp

+131
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
#include <crispy/App.h>
2323
#include <crispy/StackTrace.h>
24+
#include <crispy/base64.h>
2425
#include <crispy/utils.h>
2526

2627
#include <fmt/chrono.h>
@@ -58,6 +59,7 @@ using std::string_view;
5859
using std::unique_ptr;
5960

6061
using namespace std::string_literals;
62+
using namespace std::string_view_literals;
6163

6264
namespace CLI = crispy::cli;
6365

@@ -280,6 +282,106 @@ int ContourApp::captureAction()
280282
return EXIT_FAILURE;
281283
}
282284

285+
#if defined(GOOD_IMAGE_PROTOCOL)
286+
namespace
287+
{
288+
crispy::Size parseSize(string_view _text)
289+
{
290+
(void) _text;
291+
return crispy::Size {}; // TODO
292+
}
293+
294+
terminal::ImageAlignment parseImageAlignment(string_view _text)
295+
{
296+
(void) _text;
297+
return terminal::ImageAlignment::TopStart; // TODO
298+
}
299+
300+
terminal::ImageResize parseImageResize(string_view _text)
301+
{
302+
(void) _text;
303+
return terminal::ImageResize::NoResize; // TODO
304+
}
305+
306+
// terminal::CellLocation parsePosition(string_view _text)
307+
// {
308+
// (void) _text;
309+
// return {}; // TODO
310+
// }
311+
312+
// TODO: chunkedFileReader(path) to return iterator over spans of data chunks.
313+
std::vector<uint8_t> readFile(FileSystem::path const& _path)
314+
{
315+
auto ifs = std::ifstream(_path.string());
316+
if (!ifs.good())
317+
return {};
318+
319+
auto const size = FileSystem::file_size(_path);
320+
auto text = std::vector<uint8_t>();
321+
text.resize(size);
322+
ifs.read((char*) &text[0], static_cast<std::streamsize>(size));
323+
return text;
324+
}
325+
326+
void displayImage(terminal::ImageResize _resizePolicy,
327+
terminal::ImageAlignment _alignmentPolicy,
328+
crispy::Size _screenSize,
329+
string_view _fileName)
330+
{
331+
auto constexpr ST = "\033\\"sv;
332+
333+
cout << fmt::format("{}f={},c={},l={},a={},z={};",
334+
"\033Ps"sv, // GIONESHOT
335+
'0', // image format: 0 = auto detect
336+
_screenSize.width,
337+
_screenSize.height,
338+
int(_alignmentPolicy),
339+
int(_resizePolicy));
340+
341+
#if 1
342+
auto const data = readFile(FileSystem::path(string(_fileName))); // TODO: incremental buffered read
343+
auto encoderState = crispy::base64::EncoderState {};
344+
345+
std::vector<char> buf;
346+
auto const writer = [&](char a, char b, char c, char d) {
347+
buf.push_back(a);
348+
buf.push_back(b);
349+
buf.push_back(c);
350+
buf.push_back(d);
351+
};
352+
auto const flush = [&]() {
353+
cout.write(buf.data(), static_cast<std::streamsize>(buf.size()));
354+
buf.clear();
355+
};
356+
357+
for (uint8_t const byte: data)
358+
{
359+
crispy::base64::encode(byte, encoderState, writer);
360+
if (buf.size() >= 4096)
361+
flush();
362+
}
363+
flush();
364+
#endif
365+
366+
cout << ST;
367+
}
368+
} // namespace
369+
370+
int ContourApp::imageAction()
371+
{
372+
auto const resizePolicy = parseImageResize(parameters().get<string>("contour.image.resize"));
373+
auto const alignmentPolicy = parseImageAlignment(parameters().get<string>("contour.image.align"));
374+
auto const size = parseSize(parameters().get<string>("contour.image.size"));
375+
auto const fileName = parameters().verbatim.front();
376+
// TODO: how do we wanna handle more than one verbatim arg (image)?
377+
// => report error and EXIT_FAILURE as only one verbatim arg is allowed.
378+
// FIXME: What if parameter `size` is given as `_size` instead, it should cause an
379+
// invalid-argument error above already!
380+
displayImage(resizePolicy, alignmentPolicy, size, fileName);
381+
return EXIT_SUCCESS;
382+
}
383+
#endif
384+
283385
int ContourApp::parserTableAction()
284386
{
285387
terminal::parser::dot(std::cout, terminal::parser::ParserTable::get());
@@ -367,6 +469,35 @@ crispy::cli::Command ContourApp::parameterDefinition() const
367469
"FILE",
368470
CLI::Presence::Required },
369471
} } } },
472+
#if defined(GOOD_IMAGE_PROTOCOL)
473+
CLI::Command {
474+
"image",
475+
"Sends an image to the terminal emulator for display.",
476+
CLI::OptionList {
477+
CLI::Option { "resize",
478+
CLI::Value { "fit"s },
479+
"Sets the image resize policy.\n"
480+
"Policies available are:\n"
481+
" - no (no resize),\n"
482+
" - fit (resize to fit),\n"
483+
" - fill (resize to fill),\n"
484+
" - stretch (stretch to fill)." },
485+
CLI::Option { "align",
486+
CLI::Value { "center"s },
487+
"Sets the image alignment policy.\n"
488+
"Possible policies are: TopLeft, TopCenter, TopRight, MiddleLeft, "
489+
"MiddleCenter, MiddleRight, BottomLeft, BottomCenter, BottomRight." },
490+
CLI::Option { "size",
491+
CLI::Value { ""s },
492+
"Sets the amount of columns and rows to place the image onto. "
493+
"The top-left of the this area is the current cursor position, "
494+
"and it will be scrolled automatically if not enough rows are present." } },
495+
CLI::CommandList {},
496+
CLI::CommandSelect::Explicit,
497+
CLI::Verbatim {
498+
"IMAGE_FILE",
499+
"Path to image to be displayed. Image formats supported are at least PNG, JPG." } },
500+
#endif
370501
CLI::Command {
371502
"capture",
372503
"Captures the screen buffer of the currently running terminal.",

src/contour/ContourApp.h

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class ContourApp: public crispy::App
3737
int configAction();
3838
int integrationAction();
3939
int infoVT();
40+
int imageAction();
4041
};
4142

4243
} // namespace contour

src/contour/display/OpenGLRenderer.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ namespace
146146
{
147147
case terminal::ImageFormat::RGB: return GL_RGB;
148148
case terminal::ImageFormat::RGBA: return GL_RGBA;
149+
case terminal::ImageFormat::PNG: Require(false);
149150
}
150151
Guarantee(false);
151152
crispy::unreachable();

src/contour/display/TerminalWidget.cpp

+41
Original file line numberDiff line numberDiff line change
@@ -1197,4 +1197,45 @@ void TerminalWidget::discardImage(terminal::Image const& _image)
11971197
}
11981198
// }}}
11991199

1200+
optional<terminal::Image> TerminalWidget::decodeImage(gsl::span<uint8_t> _imageData)
1201+
{
1202+
QImage image;
1203+
image.loadFromData(static_cast<uchar const*>(_imageData.data()), static_cast<int>(_imageData.size()));
1204+
1205+
qDebug() << "decodeImage()" << image.format();
1206+
if (image.hasAlphaChannel() && image.format() != QImage::Format_ARGB32)
1207+
image = image.convertToFormat(QImage::Format_ARGB32);
1208+
else
1209+
image = image.convertToFormat(QImage::Format_RGB888);
1210+
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
1211+
qDebug() << "|> decodeImage()" << image.format() << image.sizeInBytes() << image.size();
1212+
#else
1213+
qDebug() << "|> decodeImage()" << image.format() << image.byteCount() << image.size();
1214+
#endif
1215+
1216+
static auto nextImageId = terminal::ImageId(0);
1217+
1218+
terminal::Image::Data pixels;
1219+
auto* p = &pixels[0];
1220+
pixels.resize(static_cast<size_t>(image.bytesPerLine() * image.height()));
1221+
for (int i = 0; i < image.height(); ++i)
1222+
{
1223+
memcpy(p, image.constScanLine(i), static_cast<size_t>(image.bytesPerLine()));
1224+
p += image.bytesPerLine();
1225+
}
1226+
1227+
terminal::ImageFormat format = terminal::ImageFormat::RGBA;
1228+
switch (image.format())
1229+
{
1230+
case QImage::Format_RGBA8888: format = terminal::ImageFormat::RGBA; break;
1231+
case QImage::Format_RGB888: format = terminal::ImageFormat::RGB; break;
1232+
default: return nullopt;
1233+
}
1234+
ImageSize size { Width::cast_from(image.width()), Height::cast_from(image.height()) };
1235+
auto onRemove = terminal::Image::OnImageRemove {};
1236+
1237+
auto img = terminal::Image(nextImageId++, format, std::move(pixels), size, onRemove);
1238+
return { std::move(img) };
1239+
}
1240+
12001241
} // namespace contour::display

src/contour/display/TerminalWidget.h

+2
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ class TerminalWidget: public QOpenGLWidget, private QOpenGLExtraFunctions
165165
void logDisplayTopInfo();
166166
void logDisplayInfo();
167167

168+
std::optional<terminal::Image> decodeImage(gsl::span<uint8_t> _imageData);
169+
168170
public Q_SLOTS:
169171
void onFrameSwapped();
170172
void onScrollBarValueChanged(int _value);

src/terminal/CMakeLists.txt

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ set(terminal_HEADERS
3535
InputGenerator.h
3636
Line.h
3737
MatchModes.h
38+
MessageParser.h
3839
MockTerm.h
3940
Parser.h
4041
Process.h
@@ -79,6 +80,7 @@ set(terminal_SOURCES
7980
InputGenerator.cpp
8081
Line.cpp
8182
MatchModes.cpp
83+
MessageParser.cpp
8284
MockTerm.cpp
8385
Parser.cpp
8486
Process${PLATFORM_SUFFIX}.cpp
@@ -158,6 +160,7 @@ if(LIBTERMINAL_TESTING)
158160
Functions_test.cpp
159161
Grid_test.cpp
160162
Line_test.cpp
163+
MessageParser_test.cpp
161164
Parser_test.cpp
162165
Screen_test.cpp
163166
Sequence_test.cpp

src/terminal/Functions.h

+19-2
Original file line numberDiff line numberDiff line change
@@ -466,14 +466,24 @@ constexpr inline auto RCOLORHIGHLIGHTBG = detail::OSC(117, VTExtension::XTerm,"R
466466
constexpr inline auto NOTIFY = detail::OSC(777, VTExtension::XTerm,"NOTIFY", "Send Notification.");
467467
constexpr inline auto DUMPSTATE = detail::OSC(888, VTExtension::Contour, "DUMPSTATE", "Dumps internal state to debug stream.");
468468

469+
// DCS: Good Image Protocol
470+
#if defined(GOOD_IMAGE_PROTOCOL)
471+
// TODO: use OSC instead of DCS?
472+
constexpr inline auto GIUPLOAD = detail::DCS(std::nullopt, 0, 0, std::nullopt, 'u', VTType::VT525, "GIUPLOAD", "Uploads an image.");
473+
constexpr inline auto GIRENDER = detail::DCS(std::nullopt, 0, 0, std::nullopt, 'r', VTType::VT525, "GIRENDER", "Renders an image.");
474+
constexpr inline auto GIDELETE = detail::DCS(std::nullopt, 0, 0, std::nullopt, 'd', VTType::VT525, "GIDELETE", "Deletes an image.");
475+
constexpr inline auto GIONESHOT = detail::DCS(std::nullopt, 0, 0, std::nullopt, 's', VTType::VT525, "GIONESHOT", "Uploads and renders an unnamed image.");
476+
#endif
477+
469478
constexpr inline auto CaptureBufferCode = 314;
470479

471480
// clang-format on
472481

473482
inline auto const& functions() noexcept
474483
{
475484
static auto const funcs = []() constexpr
476-
{ // {{{
485+
{
486+
// clang-format off
477487
auto f = std::array {
478488
// C0
479489
EOT,
@@ -581,6 +591,12 @@ inline auto const& functions() noexcept
581591
XTVERSION,
582592

583593
// DCS
594+
#if defined(GOOD_IMAGE_PROTOCOL)
595+
GIUPLOAD,
596+
GIRENDER,
597+
GIDELETE,
598+
GIONESHOT,
599+
#endif
584600
STP,
585601
DECRQSS,
586602
DECSIXEL,
@@ -614,12 +630,13 @@ inline auto const& functions() noexcept
614630
NOTIFY,
615631
DUMPSTATE,
616632
};
633+
// clang-format off
617634
crispy::sort(
618635
f,
619636
[](FunctionDefinition const& a, FunctionDefinition const& b) constexpr { return compare(a, b); });
620637
return f;
621638
}
622-
(); // }}}
639+
();
623640

624641
#if 0
625642
for (auto [a, b] : crispy::indexed(funcs))

src/terminal/Image.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,9 @@ shared_ptr<RasterizedImage> ImagePool::rasterize(shared_ptr<Image const> _image,
148148
std::move(_image), _alignmentPolicy, _resizePolicy, _defaultColor, _cellSpan, _cellSize);
149149
}
150150

151-
void ImagePool::link(string const& _name, shared_ptr<Image const> _imageRef)
151+
void ImagePool::link(string _name, shared_ptr<Image const> _imageRef)
152152
{
153-
imageNameToImageCache_.emplace(_name, std::move(_imageRef));
153+
imageNameToImageCache_.emplace(std::move(_name), std::move(_imageRef));
154154
}
155155

156156
shared_ptr<Image const> ImagePool::findImageByName(string const& _name) const noexcept

src/terminal/Image.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ enum class ImageFormat
4141
{
4242
RGB,
4343
RGBA,
44+
PNG,
4445
};
4546

4647
// clang-format off
@@ -259,7 +260,7 @@ class ImagePool
259260

260261
// named image access
261262
//
262-
void link(std::string const& _name, std::shared_ptr<Image const> _imageRef);
263+
void link(std::string _name, std::shared_ptr<Image const> _imageRef);
263264
[[nodiscard]] std::shared_ptr<Image const> findImageByName(std::string const& _name) const noexcept;
264265
void unlink(std::string const& _name);
265266

@@ -300,6 +301,7 @@ struct formatter<terminal::ImageFormat>
300301
{
301302
case terminal::ImageFormat::RGB: return fmt::format_to(ctx.out(), "RGB");
302303
case terminal::ImageFormat::RGBA: return fmt::format_to(ctx.out(), "RGBA");
304+
case terminal::ImageFormat::PNG: return fmt::format_to(ctx.out(), "PNG");
303305
}
304306
return fmt::format_to(ctx.out(), "{}", unsigned(value));
305307
}

0 commit comments

Comments
 (0)