Skip to content

Commit a5d308a

Browse files
[terminal] WIP: Good Image Protocol (PoC)
1 parent dcbe1ef commit a5d308a

17 files changed

+959
-3
lines changed

TODO.md

+4
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@
9393
- [ ] contour: provide `--mono` (or alike) CLI flag to "just" provide a QOpenGLWindow for best performance,
9494
lacking UI features as compromise.
9595

96+
### Good Image Protocol
97+
98+
- [ ] Make sure Screen::Image does not need to know about the underlying image format. (only the frontend needs to know about the actual format in use, so it can *render* the pixmaps)
99+
96100
### Usability Improvements
97101

98102
- ? Images: copy action should uxe U+FFFC (object replacement) on grid cells that contain an image for text-based clipboard action

src/contour/ContourApp.cpp

+131
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@
1919

2020
#include <terminal/Capabilities.h>
2121
#include <terminal/Parser.h>
22+
#include <terminal/Image.h>
2223

2324
#include <crispy/StackTrace.h>
25+
#include <crispy/base64.h>
2426
#include <crispy/debuglog.h>
27+
#include <crispy/stdfs.h>
2528
#include <crispy/utils.h>
2629

2730
#include <iostream>
@@ -35,16 +38,21 @@
3538
#include <sys/ioctl.h>
3639
#endif
3740

41+
#define GOOD_IMAGE_PROTOCOL // TODO use cmake here instead
42+
3843
using std::bind;
3944
using std::cerr;
4045
using std::cout;
46+
using std::ifstream;
4147
using std::make_unique;
4248
using std::ofstream;
4349
using std::string;
4450
using std::string_view;
4551
using std::unique_ptr;
52+
using std::vector;
4653

4754
using namespace std::string_literals;
55+
using namespace std::string_view_literals;
4856

4957
namespace CLI = crispy::cli;
5058

@@ -202,6 +210,76 @@ namespace // {{{ helper
202210
#endif
203211
} // }}}
204212

213+
#if defined(GOOD_IMAGE_PROTOCOL) // {{{
214+
terminal::ImageAlignment parseImageAlignment(string_view _text)
215+
{
216+
(void) _text;
217+
return terminal::ImageAlignment::TopStart; // TODO
218+
}
219+
220+
terminal::ImageResize parseImageResize(string_view _text)
221+
{
222+
(void) _text;
223+
return terminal::ImageResize::NoResize; // TODO
224+
}
225+
226+
terminal::Coordinate parsePosition(string_view _text)
227+
{
228+
(void) _text;
229+
return {}; // TODO
230+
}
231+
232+
// TODO: chunkedFileReader(path) to return iterator over spans of data chunks.
233+
std::vector<uint8_t> readFile(FileSystem::path const& _path)
234+
{
235+
auto ifs = ifstream(_path.string());
236+
if (!ifs.good())
237+
return {};
238+
239+
auto const size = FileSystem::file_size(_path);
240+
auto text = vector<uint8_t>();
241+
text.resize(size);
242+
ifs.read((char*) &text[0], size);
243+
return text;
244+
}
245+
246+
void displayImage(terminal::ImageResize _resizePolicy,
247+
terminal::ImageAlignment _alignmentPolicy,
248+
crispy::Size _screenSize,
249+
string_view _fileName)
250+
{
251+
auto constexpr ST = "\033\\"sv;
252+
253+
cout << fmt::format("{}f={},c={},l={},a={},z={};",
254+
"\033Ps"sv, // GIONESHOT
255+
'0', // image format: 0 = auto detect
256+
_screenSize.width,
257+
_screenSize.height,
258+
int(_alignmentPolicy),
259+
int(_resizePolicy)
260+
);
261+
262+
#if 1
263+
auto const data = readFile(_fileName);// TODO: incremental buffered read
264+
auto encoderState = crispy::base64::EncoderState{};
265+
266+
vector<char> buf;
267+
auto const writer = [&](string_view _data) { for (auto ch: _data) buf.push_back(ch); };
268+
auto const flush = [&]() { cout.write(buf.data(), buf.size()); buf.clear(); };
269+
270+
for (uint8_t const byte: data)
271+
{
272+
crispy::base64::encode(byte, encoderState, writer);
273+
if (buf.size() >= 4096)
274+
flush();
275+
}
276+
flush();
277+
#endif
278+
279+
cout << ST;
280+
}
281+
#endif // }}}
282+
205283
ContourApp::ContourApp() :
206284
App("contour", "Contour Terminal Emulator", CONTOUR_VERSION_STRING)
207285
{
@@ -301,6 +379,28 @@ int ContourApp::profileAction()
301379
return EXIT_SUCCESS;
302380
}
303381

382+
#if defined(GOOD_IMAGE_PROTOCOL)
383+
crispy::Size parseSize(string_view _text)
384+
{
385+
(void) _text;
386+
return crispy::Size{};//TODO
387+
}
388+
389+
int ContourApp::imageAction()
390+
{
391+
auto const resizePolicy = parseImageResize(parameters().get<string>("contour.image.resize"));
392+
auto const alignmentPolicy = parseImageAlignment(parameters().get<string>("contour.image.align"));
393+
auto const size = parseSize(parameters().get<string>("contour.image.size"));
394+
auto const fileName = parameters().verbatim.front();
395+
// TODO: how do we wanna handle more than one verbatim arg (image)?
396+
// => report error and EXIT_FAILURE as only one verbatim arg is allowed.
397+
// FIXME: What if parameter `size` is given as `_size` instead, it should cause an
398+
// invalid-argument error above already!
399+
displayImage(resizePolicy, alignmentPolicy, size, fileName);
400+
return EXIT_SUCCESS;
401+
}
402+
#endif
403+
304404
crispy::cli::Command ContourApp::parameterDefinition() const
305405
{
306406
return CLI::Command{
@@ -365,6 +465,37 @@ crispy::cli::Command ContourApp::parameterDefinition() const
365465
}
366466
}
367467
},
468+
#if defined(GOOD_IMAGE_PROTOCOL)
469+
CLI::Command{
470+
"image",
471+
"Sends an image to the terminal emulator for display.",
472+
CLI::OptionList{
473+
CLI::Option{"resize", CLI::Value{"fit"s},
474+
"Sets the image resize policy.\n"
475+
"Policies available are:\n"
476+
" - no (no resize),\n"
477+
" - fit (resize to fit),\n"
478+
" - fill (resize to fill),\n"
479+
" - stretch (stretch to fill)."
480+
},
481+
CLI::Option{"align", CLI::Value{"center"s},
482+
"Sets the image alignment policy.\n"
483+
"Possible policies are: TopLeft, TopCenter, TopRight, MiddleLeft, MiddleCenter, MiddleRight, BottomLeft, BottomCenter, BottomRight."
484+
},
485+
CLI::Option{"size", CLI::Value{""s},
486+
"Sets the amount of columns and rows to place the image onto. "
487+
"The top-left of the this area is the current cursor position, "
488+
"and it will be scrolled automatically if not enough rows are present."
489+
}
490+
},
491+
CLI::CommandList{},
492+
CLI::CommandSelect::Explicit,
493+
CLI::Verbatim{
494+
"IMAGE_FILE",
495+
"Path to image to be displayed. Image formats supported are at least PNG, JPG."
496+
}
497+
},
498+
#endif
368499
CLI::Command{
369500
"capture",
370501
"Captures the screen buffer of the currently running terminal.",

src/contour/ContourApp.h

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class ContourApp : public crispy::App
3535
int terminfoAction();
3636
int configAction();
3737
int integrationAction();
38+
int imageAction();
3839
};
3940

4041
}

src/contour/opengl/TerminalWidget.cpp

+44
Original file line numberDiff line numberDiff line change
@@ -1117,6 +1117,50 @@ void TerminalWidget::updateMinimumSize()
11171117
};
11181118
setMinimumSize(minSize.width.as<int>(), minSize.height.as<int>());
11191119
}
1120+
1121+
optional<terminal::Image> TerminalWidget::decodeImage(crispy::span<uint8_t> _imageData)
1122+
{
1123+
QImage image;
1124+
image.loadFromData(_imageData.begin(), _imageData.size());
1125+
1126+
qDebug() << "decodeImage()" << image.format();
1127+
if (image.hasAlphaChannel() && image.format() != QImage::Format_ARGB32)
1128+
image = image.convertToFormat(QImage::Format_ARGB32);
1129+
else
1130+
image = image.convertToFormat(QImage::Format_RGB888);
1131+
qDebug() << "|> decodeImage()" << image.format()
1132+
<< image.sizeInBytes()
1133+
<< image.size()
1134+
;
1135+
1136+
static terminal::Image::Id nextImageId = 0;
1137+
1138+
terminal::Image::Data pixels;
1139+
auto* p = &pixels[0];
1140+
pixels.resize(image.bytesPerLine() * image.height());
1141+
for (int i = 0; i < image.height(); ++i)
1142+
{
1143+
memcpy(p, image.constScanLine(i), image.bytesPerLine());
1144+
p += image.bytesPerLine();
1145+
}
1146+
1147+
terminal::ImageFormat format = terminal::ImageFormat::RGBA;
1148+
switch (image.format())
1149+
{
1150+
case QImage::Format_RGBA8888:
1151+
format = terminal::ImageFormat::RGBA;
1152+
break;
1153+
case QImage::Format_RGB888:
1154+
format = terminal::ImageFormat::RGB;
1155+
break;
1156+
default:
1157+
return nullopt;
1158+
}
1159+
auto const size = ImageSize{Width(image.width()), Height(image.height())};
1160+
1161+
auto img = terminal::Image(nextImageId++, format, std::move(pixels), size);
1162+
return {std::move(img)};
1163+
}
11201164
// }}}
11211165

11221166
} // namespace contour

src/contour/opengl/TerminalWidget.h

+6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <contour/helper.h>
2121

2222
#include <terminal/Color.h>
23+
#include <terminal/Image.h>
2324
#include <terminal/Metrics.h>
2425
#include <terminal/primitives.h>
2526
#include <terminal_renderer/Renderer.h>
@@ -66,6 +67,11 @@ class TerminalWidget :
6667
static QSurfaceFormat surfaceFormat();
6768
QSize minimumSizeHint() const override;
6869
QSize sizeHint() const override;
70+
71+
std::optional<terminal::Image> decodeImage(crispy::span<uint8_t> _imageData);
72+
73+
int pointsToPixels(text::font_size _fontSize) const noexcept;
74+
6975
void initializeGL() override;
7076
void resizeGL(int _width, int _height) override;
7177
void paintGL() override;

src/terminal/CMakeLists.txt

+7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ option(LIBTERMINAL_PASSIVE_RENDER_BUFFER_UPDATE "Updates the render buffer withi
1717
# Compile-time terminal features
1818
option(LIBTERMINAL_IMAGES "Enables image support [default: ON]" ON)
1919
option(LIBTERMINAL_HYPERLINKS "Enables hyperlink support [default: ON]" ON)
20+
option(GOOD_IMAGE_PROTOCOL "Enables building with Good Image Protocol support [default: OFF]" OFF)
2021

2122
if(MSVC)
2223
add_definitions(-DNOMINMAX)
@@ -33,6 +34,7 @@ set(terminal_HEADERS
3334
InputBinding.h
3435
InputGenerator.h
3536
MatchModes.h
37+
MessageParser.h
3638
Parser.h
3739
Process.h
3840
pty/Pty.h
@@ -63,6 +65,7 @@ set(terminal_SOURCES
6365
InputBinding.cpp
6466
InputGenerator.cpp
6567
MatchModes.cpp
68+
MessageParser.cpp
6669
Parser.cpp
6770
Process.cpp
6871
RenderBuffer.cpp
@@ -104,6 +107,9 @@ endif()
104107
if(LIBTERMINAL_LOG_TRACE)
105108
target_compile_definitions(terminal PRIVATE LIBTERMINAL_LOG_TRACE=1)
106109
endif()
110+
if(GOOD_IMAGE_PROTOCOL)
111+
target_compile_definitions(terminal PUBLIC GOOD_IMAGE_PROTOCOL=1)
112+
endif()
107113

108114
if(LIBTERMINAL_IMAGES)
109115
target_compile_definitions(terminal PUBLIC LIBTERMINAL_IMAGES=1)
@@ -126,6 +132,7 @@ if(LIBTERMINAL_TESTING)
126132
Selector_test.cpp
127133
Functions_test.cpp
128134
Grid_test.cpp
135+
MessageParser_test.cpp
129136
Parser_test.cpp
130137
Screen_test.cpp
131138
Terminal_test.cpp

src/terminal/Functions.h

+15
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,15 @@ constexpr inline auto RCOLORHIGHLIGHTBG = detail::OSC(117, "RCOLORHIGHLIGHTBG",
366366
constexpr inline auto NOTIFY = detail::OSC(777, "NOTIFY", "Send Notification.");
367367
constexpr inline auto DUMPSTATE = detail::OSC(888, "DUMPSTATE", "Dumps internal state to debug stream.");
368368

369+
// DCS: Good Image Protocol
370+
#if defined(GOOD_IMAGE_PROTOCOL)
371+
// TODO: use OSC instead of DCS?
372+
constexpr inline auto GIUPLOAD = detail::DCS(std::nullopt, 0, 0, std::nullopt, 'u', VTType::VT525, "GIUPLOAD", "Uploads an image.");
373+
constexpr inline auto GIRENDER = detail::DCS(std::nullopt, 0, 0, std::nullopt, 'r', VTType::VT525, "GIRENDER", "Renders an image.");
374+
constexpr inline auto GIDELETE = detail::DCS(std::nullopt, 0, 0, std::nullopt, 'd', VTType::VT525, "GIDELETE", "Deletes an image.");
375+
constexpr inline auto GIONESHOT = detail::DCS(std::nullopt, 0, 0, std::nullopt, 's', VTType::VT525, "GIONESHOT", "Uploads and renders an unnamed image.");
376+
#endif
377+
369378
inline auto const& functions() noexcept
370379
{
371380
static auto const funcs = []() constexpr { // {{{
@@ -465,6 +474,12 @@ inline auto const& functions() noexcept
465474
XTVERSION,
466475

467476
// DCS
477+
#if defined(GOOD_IMAGE_PROTOCOL)
478+
GIUPLOAD,
479+
GIRENDER,
480+
GIDELETE,
481+
GIONESHOT,
482+
#endif
468483
STP,
469484
DECRQSS,
470485
DECSIXEL,

src/terminal/Image.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ class Image {
7575
Id id_;
7676
ImageFormat format_;
7777
Data data_;
78-
crispy::Size size_;
78+
ImageSize size_;
7979
};
8080

8181
/// Image resize hints are used to properly fit/fill the area to place the image onto.

0 commit comments

Comments
 (0)