Skip to content

Commit 03ab5c9

Browse files
[terminal] *EXPERIMENTAL* Good Image Protocol (work-in-progress)
1 parent 503f917 commit 03ab5c9

15 files changed

+947
-9
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
@@ -20,6 +20,7 @@
2020

2121
#include <crispy/App.h>
2222
#include <crispy/StackTrace.h>
23+
#include <crispy/base64.h>
2324
#include <crispy/utils.h>
2425

2526
#include <fmt/chrono.h>
@@ -54,6 +55,7 @@ using std::string_view;
5455
using std::unique_ptr;
5556

5657
using namespace std::string_literals;
58+
using namespace std::string_view_literals;
5759

5860
namespace CLI = crispy::cli;
5961

@@ -239,6 +241,106 @@ int ContourApp::captureAction()
239241
return EXIT_FAILURE;
240242
}
241243

244+
#if defined(GOOD_IMAGE_PROTOCOL)
245+
namespace
246+
{
247+
crispy::Size parseSize(string_view _text)
248+
{
249+
(void) _text;
250+
return crispy::Size {}; // TODO
251+
}
252+
253+
terminal::ImageAlignment parseImageAlignment(string_view _text)
254+
{
255+
(void) _text;
256+
return terminal::ImageAlignment::TopStart; // TODO
257+
}
258+
259+
terminal::ImageResize parseImageResize(string_view _text)
260+
{
261+
(void) _text;
262+
return terminal::ImageResize::NoResize; // TODO
263+
}
264+
265+
// terminal::CellLocation parsePosition(string_view _text)
266+
// {
267+
// (void) _text;
268+
// return {}; // TODO
269+
// }
270+
271+
// TODO: chunkedFileReader(path) to return iterator over spans of data chunks.
272+
std::vector<uint8_t> readFile(FileSystem::path const& _path)
273+
{
274+
auto ifs = std::ifstream(_path.string());
275+
if (!ifs.good())
276+
return {};
277+
278+
auto const size = FileSystem::file_size(_path);
279+
auto text = std::vector<uint8_t>();
280+
text.resize(size);
281+
ifs.read((char*) &text[0], static_cast<std::streamsize>(size));
282+
return text;
283+
}
284+
285+
void displayImage(terminal::ImageResize _resizePolicy,
286+
terminal::ImageAlignment _alignmentPolicy,
287+
crispy::Size _screenSize,
288+
string_view _fileName)
289+
{
290+
auto constexpr ST = "\033\\"sv;
291+
292+
cout << fmt::format("{}f={},c={},l={},a={},z={};",
293+
"\033Ps"sv, // GIONESHOT
294+
'0', // image format: 0 = auto detect
295+
_screenSize.width,
296+
_screenSize.height,
297+
int(_alignmentPolicy),
298+
int(_resizePolicy));
299+
300+
#if 1
301+
auto const data = readFile(FileSystem::path(string(_fileName))); // TODO: incremental buffered read
302+
auto encoderState = crispy::base64::EncoderState {};
303+
304+
std::vector<char> buf;
305+
auto const writer = [&](char a, char b, char c, char d) {
306+
buf.push_back(a);
307+
buf.push_back(b);
308+
buf.push_back(c);
309+
buf.push_back(d);
310+
};
311+
auto const flush = [&]() {
312+
cout.write(buf.data(), static_cast<std::streamsize>(buf.size()));
313+
buf.clear();
314+
};
315+
316+
for (uint8_t const byte: data)
317+
{
318+
crispy::base64::encode(byte, encoderState, writer);
319+
if (buf.size() >= 4096)
320+
flush();
321+
}
322+
flush();
323+
#endif
324+
325+
cout << ST;
326+
}
327+
} // namespace
328+
329+
int ContourApp::imageAction()
330+
{
331+
auto const resizePolicy = parseImageResize(parameters().get<string>("contour.image.resize"));
332+
auto const alignmentPolicy = parseImageAlignment(parameters().get<string>("contour.image.align"));
333+
auto const size = parseSize(parameters().get<string>("contour.image.size"));
334+
auto const fileName = parameters().verbatim.front();
335+
// TODO: how do we wanna handle more than one verbatim arg (image)?
336+
// => report error and EXIT_FAILURE as only one verbatim arg is allowed.
337+
// FIXME: What if parameter `size` is given as `_size` instead, it should cause an
338+
// invalid-argument error above already!
339+
displayImage(resizePolicy, alignmentPolicy, size, fileName);
340+
return EXIT_SUCCESS;
341+
}
342+
#endif
343+
242344
int ContourApp::parserTableAction()
243345
{
244346
terminal::parser::dot(std::cout, terminal::parser::ParserTable::get());
@@ -318,6 +420,35 @@ crispy::cli::Command ContourApp::parameterDefinition() const
318420
"FILE",
319421
CLI::Presence::Required },
320422
} } } },
423+
#if defined(GOOD_IMAGE_PROTOCOL)
424+
CLI::Command {
425+
"image",
426+
"Sends an image to the terminal emulator for display.",
427+
CLI::OptionList {
428+
CLI::Option { "resize",
429+
CLI::Value { "fit"s },
430+
"Sets the image resize policy.\n"
431+
"Policies available are:\n"
432+
" - no (no resize),\n"
433+
" - fit (resize to fit),\n"
434+
" - fill (resize to fill),\n"
435+
" - stretch (stretch to fill)." },
436+
CLI::Option { "align",
437+
CLI::Value { "center"s },
438+
"Sets the image alignment policy.\n"
439+
"Possible policies are: TopLeft, TopCenter, TopRight, MiddleLeft, "
440+
"MiddleCenter, MiddleRight, BottomLeft, BottomCenter, BottomRight." },
441+
CLI::Option { "size",
442+
CLI::Value { ""s },
443+
"Sets the amount of columns and rows to place the image onto. "
444+
"The top-left of the this area is the current cursor position, "
445+
"and it will be scrolled automatically if not enough rows are present." } },
446+
CLI::CommandList {},
447+
CLI::CommandSelect::Explicit,
448+
CLI::Verbatim {
449+
"IMAGE_FILE",
450+
"Path to image to be displayed. Image formats supported are at least PNG, JPG." } },
451+
#endif
321452
CLI::Command {
322453
"capture",
323454
"Captures the screen buffer of the currently running terminal.",

src/contour/ContourApp.h

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class ContourApp: public crispy::App
3636
int terminfoAction();
3737
int configAction();
3838
int integrationAction();
39+
int imageAction();
3940
};
4041

4142
} // namespace contour

src/contour/opengl/OpenGLRenderer.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ namespace
155155
{
156156
case terminal::ImageFormat::RGB: return GL_RGB;
157157
case terminal::ImageFormat::RGBA: return GL_RGBA;
158+
case terminal::ImageFormat::PNG: Require(false);
158159
}
159160
Guarantee(false);
160161
crispy::unreachable();

src/contour/opengl/TerminalWidget.cpp

+41
Original file line numberDiff line numberDiff line change
@@ -1227,4 +1227,45 @@ void TerminalWidget::discardImage(terminal::Image const& _image)
12271227
}
12281228
// }}}
12291229

1230+
optional<terminal::Image> TerminalWidget::decodeImage(crispy::span<uint8_t> _imageData)
1231+
{
1232+
QImage image;
1233+
image.loadFromData(_imageData.begin(), static_cast<int>(_imageData.size()));
1234+
1235+
qDebug() << "decodeImage()" << image.format();
1236+
if (image.hasAlphaChannel() && image.format() != QImage::Format_ARGB32)
1237+
image = image.convertToFormat(QImage::Format_ARGB32);
1238+
else
1239+
image = image.convertToFormat(QImage::Format_RGB888);
1240+
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
1241+
qDebug() << "|> decodeImage()" << image.format() << image.sizeInBytes() << image.size();
1242+
#else
1243+
qDebug() << "|> decodeImage()" << image.format() << image.byteCount() << image.size();
1244+
#endif
1245+
1246+
static auto nextImageId = terminal::ImageId(0);
1247+
1248+
terminal::Image::Data pixels;
1249+
auto* p = &pixels[0];
1250+
pixels.resize(static_cast<size_t>(image.bytesPerLine() * image.height()));
1251+
for (int i = 0; i < image.height(); ++i)
1252+
{
1253+
memcpy(p, image.constScanLine(i), static_cast<size_t>(image.bytesPerLine()));
1254+
p += image.bytesPerLine();
1255+
}
1256+
1257+
terminal::ImageFormat format = terminal::ImageFormat::RGBA;
1258+
switch (image.format())
1259+
{
1260+
case QImage::Format_RGBA8888: format = terminal::ImageFormat::RGBA; break;
1261+
case QImage::Format_RGB888: format = terminal::ImageFormat::RGB; break;
1262+
default: return nullopt;
1263+
}
1264+
ImageSize size { Width::cast_from(image.width()), Height::cast_from(image.height()) };
1265+
auto onRemove = terminal::Image::OnImageRemove {};
1266+
1267+
auto img = terminal::Image(nextImageId++, format, std::move(pixels), size, onRemove);
1268+
return { std::move(img) };
1269+
}
1270+
12301271
} // namespace contour::opengl

src/contour/opengl/TerminalWidget.h

+2
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ class TerminalWidget: public QOpenGLWidget, public TerminalDisplay, private QOpe
132132
void discardImage(terminal::Image const&) override;
133133
// }}}
134134

135+
std::optional<terminal::Image> decodeImage(crispy::span<uint8_t> _imageData);
136+
135137
public Q_SLOTS:
136138
void onFrameSwapped();
137139
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
@@ -77,6 +78,7 @@ set(terminal_SOURCES
7778
InputGenerator.cpp
7879
Line.cpp
7980
MatchModes.cpp
81+
MessageParser.cpp
8082
MockTerm.cpp
8183
Parser.cpp
8284
Process${PLATFORM_SUFFIX}.cpp
@@ -145,6 +147,7 @@ if(LIBTERMINAL_TESTING)
145147
Functions_test.cpp
146148
Grid_test.cpp
147149
Line_test.cpp
150+
MessageParser_test.cpp
148151
Parser_test.cpp
149152
Screen_test.cpp
150153
Sequence_test.cpp

src/terminal/Functions.h

+19-2
Original file line numberDiff line numberDiff line change
@@ -386,14 +386,24 @@ constexpr inline auto RCOLORHIGHLIGHTBG = detail::OSC(117, "RCOLORHIGHLIGHTBG",
386386
constexpr inline auto NOTIFY = detail::OSC(777, "NOTIFY", "Send Notification.");
387387
constexpr inline auto DUMPSTATE = detail::OSC(888, "DUMPSTATE", "Dumps internal state to debug stream.");
388388

389+
// DCS: Good Image Protocol
390+
#if defined(GOOD_IMAGE_PROTOCOL)
391+
// TODO: use OSC instead of DCS?
392+
constexpr inline auto GIUPLOAD = detail::DCS(std::nullopt, 0, 0, std::nullopt, 'u', VTType::VT525, "GIUPLOAD", "Uploads an image.");
393+
constexpr inline auto GIRENDER = detail::DCS(std::nullopt, 0, 0, std::nullopt, 'r', VTType::VT525, "GIRENDER", "Renders an image.");
394+
constexpr inline auto GIDELETE = detail::DCS(std::nullopt, 0, 0, std::nullopt, 'd', VTType::VT525, "GIDELETE", "Deletes an image.");
395+
constexpr inline auto GIONESHOT = detail::DCS(std::nullopt, 0, 0, std::nullopt, 's', VTType::VT525, "GIONESHOT", "Uploads and renders an unnamed image.");
396+
#endif
397+
389398
constexpr inline auto CaptureBufferCode = 314;
390399

391400
// clang-format on
392401

393402
inline auto const& functions() noexcept
394403
{
395404
static auto const funcs = []() constexpr
396-
{ // {{{
405+
{
406+
// clang-format off
397407
auto f = std::array {
398408
// C0
399409
EOT,
@@ -491,6 +501,12 @@ inline auto const& functions() noexcept
491501
XTVERSION,
492502

493503
// DCS
504+
#if defined(GOOD_IMAGE_PROTOCOL)
505+
GIUPLOAD,
506+
GIRENDER,
507+
GIDELETE,
508+
GIONESHOT,
509+
#endif
494510
STP,
495511
DECRQSS,
496512
DECSIXEL,
@@ -524,12 +540,13 @@ inline auto const& functions() noexcept
524540
NOTIFY,
525541
DUMPSTATE,
526542
};
543+
// clang-format off
527544
crispy::sort(
528545
f,
529546
[](FunctionDefinition const& a, FunctionDefinition const& b) constexpr { return compare(a, b); });
530547
return f;
531548
}
532-
(); // }}}
549+
();
533550

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

src/terminal/Image.cpp

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

153-
void ImagePool::link(string const& _name, shared_ptr<Image const> _imageRef)
153+
void ImagePool::link(string _name, shared_ptr<Image const> _imageRef)
154154
{
155-
imageNameToImageCache_.emplace(_name, move(_imageRef));
155+
imageNameToImageCache_.emplace(move(_name), move(_imageRef));
156156
}
157157

158158
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 format_to(ctx.out(), "RGB");
302303
case terminal::ImageFormat::RGBA: return format_to(ctx.out(), "RGBA");
304+
case terminal::ImageFormat::PNG: return format_to(ctx.out(), "PNG");
303305
}
304306
return format_to(ctx.out(), "{}", unsigned(value));
305307
}

0 commit comments

Comments
 (0)