Skip to content

Commit e357064

Browse files
[terminal] *EXPERIMENTAL* Good Image Protocol (work-in-progress)
1 parent 2c3494f commit e357064

15 files changed

+943
-10
lines changed

CMakeLists.txt

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

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

src/contour/ContourApp.cpp

+131
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
#include <crispy/App.h>
2424
#include <crispy/StackTrace.h>
25+
#include <crispy/base64.h>
2526
#include <crispy/utils.h>
2627

2728
#include <fmt/chrono.h>
@@ -57,6 +58,7 @@ using std::string_view;
5758
using std::unique_ptr;
5859

5960
using namespace std::string_literals;
61+
using namespace std::string_view_literals;
6062

6163
namespace CLI = crispy::cli;
6264

@@ -279,6 +281,106 @@ int ContourApp::captureAction()
279281
return EXIT_FAILURE;
280282
}
281283

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

src/contour/display/TerminalWidget.cpp

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

1199+
optional<terminal::Image> TerminalWidget::decodeImage(gsl::span<uint8_t> _imageData)
1200+
{
1201+
QImage image;
1202+
image.loadFromData(static_cast<uchar const*>(_imageData.data()), static_cast<int>(_imageData.size()));
1203+
1204+
qDebug() << "decodeImage()" << image.format();
1205+
if (image.hasAlphaChannel() && image.format() != QImage::Format_ARGB32)
1206+
image = image.convertToFormat(QImage::Format_ARGB32);
1207+
else
1208+
image = image.convertToFormat(QImage::Format_RGB888);
1209+
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
1210+
qDebug() << "|> decodeImage()" << image.format() << image.sizeInBytes() << image.size();
1211+
#else
1212+
qDebug() << "|> decodeImage()" << image.format() << image.byteCount() << image.size();
1213+
#endif
1214+
1215+
static auto nextImageId = terminal::ImageId(0);
1216+
1217+
terminal::Image::Data pixels;
1218+
auto* p = &pixels[0];
1219+
pixels.resize(static_cast<size_t>(image.bytesPerLine() * image.height()));
1220+
for (int i = 0; i < image.height(); ++i)
1221+
{
1222+
memcpy(p, image.constScanLine(i), static_cast<size_t>(image.bytesPerLine()));
1223+
p += image.bytesPerLine();
1224+
}
1225+
1226+
terminal::ImageFormat format = terminal::ImageFormat::RGBA;
1227+
switch (image.format())
1228+
{
1229+
case QImage::Format_RGBA8888: format = terminal::ImageFormat::RGBA; break;
1230+
case QImage::Format_RGB888: format = terminal::ImageFormat::RGB; break;
1231+
default: return nullopt;
1232+
}
1233+
ImageSize size { Width::cast_from(image.width()), Height::cast_from(image.height()) };
1234+
auto onRemove = terminal::Image::OnImageRemove {};
1235+
1236+
auto img = terminal::Image(nextImageId++, format, std::move(pixels), size, onRemove);
1237+
return { std::move(img) };
1238+
}
1239+
11991240
} // 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
@@ -32,6 +32,7 @@ set(terminal_HEADERS
3232
InputGenerator.h
3333
Line.h
3434
MatchModes.h
35+
MessageParser.h
3536
MockTerm.h
3637
RenderBuffer.h
3738
RenderBufferBuilder.h
@@ -62,6 +63,7 @@ set(terminal_SOURCES
6263
InputGenerator.cpp
6364
Line.cpp
6465
MatchModes.cpp
66+
MessageParser.cpp
6567
MockTerm.cpp
6668
RenderBuffer.cpp
6769
RenderBufferBuilder.cpp
@@ -117,6 +119,7 @@ if(LIBTERMINAL_TESTING)
117119
Functions_test.cpp
118120
Grid_test.cpp
119121
Line_test.cpp
122+
MessageParser_test.cpp
120123
Screen_test.cpp
121124
Sequence_test.cpp
122125
Terminal_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)