Skip to content

Commit a4450f7

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

16 files changed

+957
-2
lines changed

TODO.md

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

97+
### Good Image Protocol
98+
99+
- [ ] 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)
100+
97101
### Usability Improvements
98102

99103
- ? 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

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,105 @@
1919

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

24+
#include <crispy/base64.h>
2325
#include <crispy/debuglog.h>
26+
#include <crispy/stdfs.h>
2427
#include <crispy/utils.h>
2528

2629
#include <fstream>
2730
#include <memory>
2831

32+
#define GOOD_IMAGE_PROTOCOL // TODO use cmake here instead
33+
2934
using std::bind;
3035
using std::cout;
36+
using std::ifstream;
3137
using std::make_unique;
3238
using std::ofstream;
3339
using std::string;
3440
using std::string_view;
3541
using std::unique_ptr;
42+
using std::vector;
3643

3744
using namespace std::string_literals;
45+
using namespace std::string_view_literals;
3846

3947
namespace CLI = crispy::cli;
4048

4149
namespace contour {
4250

51+
#if defined(GOOD_IMAGE_PROTOCOL) // {{{
52+
terminal::ImageAlignment parseImageAlignment(string_view _text)
53+
{
54+
(void) _text;
55+
return terminal::ImageAlignment::TopStart; // TODO
56+
}
57+
58+
terminal::ImageResize parseImageResize(string_view _text)
59+
{
60+
(void) _text;
61+
return terminal::ImageResize::NoResize; // TODO
62+
}
63+
64+
terminal::Coordinate parsePosition(string_view _text)
65+
{
66+
(void) _text;
67+
return {}; // TODO
68+
}
69+
70+
// TODO: chunkedFileReader(path) to return iterator over spans of data chunks.
71+
std::vector<uint8_t> readFile(FileSystem::path const& _path)
72+
{
73+
auto ifs = ifstream(_path.string());
74+
if (!ifs.good())
75+
return {};
76+
77+
auto const size = FileSystem::file_size(_path);
78+
auto text = vector<uint8_t>();
79+
text.resize(size);
80+
ifs.read((char*) &text[0], size);
81+
return text;
82+
}
83+
84+
void displayImage(terminal::ImageResize _resizePolicy,
85+
terminal::ImageAlignment _alignmentPolicy,
86+
crispy::Size _screenSize,
87+
string_view _fileName)
88+
{
89+
auto constexpr ST = "\033\\"sv;
90+
91+
cout << fmt::format("{}f={},c={},l={},a={},z={};",
92+
"\033Ps"sv, // GIONESHOT
93+
'0', // image format: 0 = auto detect
94+
_screenSize.width,
95+
_screenSize.height,
96+
int(_alignmentPolicy),
97+
int(_resizePolicy)
98+
);
99+
100+
#if 1
101+
auto const data = readFile(_fileName);// TODO: incremental buffered read
102+
auto encoderState = crispy::base64::EncoderState{};
103+
104+
vector<char> buf;
105+
auto const writer = [&](string_view _data) { for (auto ch: _data) buf.push_back(ch); };
106+
auto const flush = [&]() { cout.write(buf.data(), buf.size()); buf.clear(); };
107+
108+
for (uint8_t const byte: data)
109+
{
110+
crispy::base64::encode(byte, encoderState, writer);
111+
if (buf.size() >= 4096)
112+
flush();
113+
}
114+
flush();
115+
#endif
116+
117+
cout << ST;
118+
}
119+
#endif // }}}
120+
43121
ContourApp::ContourApp() :
44122
App("contour", "Contour Terminal Emulator", CONTOUR_VERSION_STRING)
45123
{
@@ -135,6 +213,28 @@ int ContourApp::profileAction()
135213
return EXIT_SUCCESS;
136214
}
137215

216+
#if defined(GOOD_IMAGE_PROTOCOL)
217+
crispy::Size parseSize(string_view _text)
218+
{
219+
(void) _text;
220+
return crispy::Size{};//TODO
221+
}
222+
223+
int ContourApp::imageAction()
224+
{
225+
auto const resizePolicy = parseImageResize(parameters().get<string>("contour.image.resize"));
226+
auto const alignmentPolicy = parseImageAlignment(parameters().get<string>("contour.image.align"));
227+
auto const size = parseSize(parameters().get<string>("contour.image.size"));
228+
auto const fileName = parameters().verbatim.front();
229+
// TODO: how do we wanna handle more than one verbatim arg (image)?
230+
// => report error and EXIT_FAILURE as only one verbatim arg is allowed.
231+
// FIXME: What if parameter `size` is given as `_size` instead, it should cause an
232+
// invalid-argument error above already!
233+
displayImage(resizePolicy, alignmentPolicy, size, fileName);
234+
return EXIT_SUCCESS;
235+
}
236+
#endif
237+
138238
crispy::cli::Command ContourApp::parameterDefinition() const
139239
{
140240
return CLI::Command{
@@ -199,6 +299,37 @@ crispy::cli::Command ContourApp::parameterDefinition() const
199299
}
200300
}
201301
},
302+
#if defined(GOOD_IMAGE_PROTOCOL)
303+
CLI::Command{
304+
"image",
305+
"Sends an image to the terminal emulator for display.",
306+
CLI::OptionList{
307+
CLI::Option{"resize", CLI::Value{"fit"s},
308+
"Sets the image resize policy.\n"
309+
"Policies available are:\n"
310+
" - no (no resize),\n"
311+
" - fit (resize to fit),\n"
312+
" - fill (resize to fill),\n"
313+
" - stretch (stretch to fill)."
314+
},
315+
CLI::Option{"align", CLI::Value{"center"s},
316+
"Sets the image alignment policy.\n"
317+
"Possible policies are: TopLeft, TopCenter, TopRight, MiddleLeft, MiddleCenter, MiddleRight, BottomLeft, BottomCenter, BottomRight."
318+
},
319+
CLI::Option{"size", CLI::Value{""s},
320+
"Sets the amount of columns and rows to place the image onto. "
321+
"The top-left of the this area is the current cursor position, "
322+
"and it will be scrolled automatically if not enough rows are present."
323+
}
324+
},
325+
CLI::CommandList{},
326+
CLI::CommandSelect::Explicit,
327+
CLI::Verbatim{
328+
"IMAGE_FILE",
329+
"Path to image to be displayed. Image formats supported are at least PNG, JPG."
330+
}
331+
},
332+
#endif
202333
CLI::Command{
203334
"capture",
204335
"Captures the screen buffer of the currently running terminal.",

src/contour/ContourApp.h

Lines changed: 1 addition & 0 deletions
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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,6 +1073,50 @@ void TerminalWidget::updateMinimumSize()
10731073
auto const minSize = gridMetrics().cellSize * MinimumGridSize;
10741074
setMinimumSize(minSize.width, minSize.height);
10751075
}
1076+
1077+
optional<terminal::Image> TerminalWidget::decodeImage(crispy::span<uint8_t> _imageData)
1078+
{
1079+
QImage image;
1080+
image.loadFromData(_imageData.begin(), _imageData.size());
1081+
1082+
qDebug() << "decodeImage()" << image.format();
1083+
if (image.hasAlphaChannel() && image.format() != QImage::Format_ARGB32)
1084+
image = image.convertToFormat(QImage::Format_ARGB32);
1085+
else
1086+
image = image.convertToFormat(QImage::Format_RGB888);
1087+
qDebug() << "|> decodeImage()" << image.format()
1088+
<< image.sizeInBytes()
1089+
<< image.size()
1090+
;
1091+
1092+
static terminal::Image::Id nextImageId = 0;
1093+
1094+
terminal::Image::Data pixels;
1095+
auto* p = &pixels[0];
1096+
pixels.resize(image.bytesPerLine() * image.height());
1097+
for (int i = 0; i < image.height(); ++i)
1098+
{
1099+
memcpy(p, image.constScanLine(i), image.bytesPerLine());
1100+
p += image.bytesPerLine();
1101+
}
1102+
1103+
terminal::ImageFormat format = terminal::ImageFormat::RGBA;
1104+
switch (image.format())
1105+
{
1106+
case QImage::Format_RGBA8888:
1107+
format = terminal::ImageFormat::RGBA;
1108+
break;
1109+
case QImage::Format_RGB888:
1110+
format = terminal::ImageFormat::RGB;
1111+
break;
1112+
default:
1113+
return nullopt;
1114+
}
1115+
crispy::Size size{image.width(), image.height()};
1116+
1117+
auto img = terminal::Image(nextImageId++, format, std::move(pixels), size);
1118+
return {std::move(img)};
1119+
}
10761120
// }}}
10771121

10781122
} // namespace contour

src/contour/opengl/TerminalWidget.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ class TerminalWidget :
6161
static QSurfaceFormat surfaceFormat();
6262
QSize minimumSizeHint() const override;
6363
QSize sizeHint() const override;
64+
65+
std::optional<terminal::Image> decodeImage(crispy::span<uint8_t> _imageData);
66+
67+
int pointsToPixels(text::font_size _fontSize) const noexcept;
68+
6469
void initializeGL() override;
6570
void resizeGL(int _width, int _height) override;
6671
void paintGL() override;

src/terminal/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
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)
@@ -31,6 +32,7 @@ set(terminal_HEADERS
3132
Functions.h
3233
Image.h
3334
InputGenerator.h
35+
MessageParser.h
3436
Parser.h
3537
Process.h
3638
pty/Pty.h
@@ -58,6 +60,7 @@ set(terminal_SOURCES
5860
Functions.cpp
5961
Image.cpp
6062
InputGenerator.cpp
63+
MessageParser.cpp
6164
Parser.cpp
6265
Process.cpp
6366
RenderBuffer.cpp
@@ -98,6 +101,9 @@ endif()
98101
if(LIBTERMINAL_LOG_TRACE)
99102
target_compile_definitions(terminal PRIVATE LIBTERMINAL_LOG_TRACE=1)
100103
endif()
104+
if(GOOD_IMAGE_PROTOCOL)
105+
target_compile_definitions(terminal PUBLIC GOOD_IMAGE_PROTOCOL=1)
106+
endif()
101107

102108
if(LIBTERMINAL_IMAGES)
103109
target_compile_definitions(terminal PUBLIC LIBTERMINAL_IMAGES=1)
@@ -120,6 +126,7 @@ if(LIBTERMINAL_TESTING)
120126
Selector_test.cpp
121127
Functions_test.cpp
122128
Grid_test.cpp
129+
MessageParser_test.cpp
123130
Parser_test.cpp
124131
Screen_test.cpp
125132
Terminal_test.cpp

src/terminal/Functions.h

Lines changed: 15 additions & 0 deletions
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,

0 commit comments

Comments
 (0)