1 // Copyright 2008-present Contributors to the OpenImageIO project.
2 // SPDX-License-Identifier: BSD-3-Clause
3 // https://github.com/OpenImageIO/oiio/blob/master/LICENSE.md
4
5 #include <cstdio>
6
7 #include <OpenImageIO/filesystem.h>
8 #include <OpenImageIO/imagebuf.h>
9 #include <OpenImageIO/imagebufalgo.h>
10 #include <OpenImageIO/imageio.h>
11 #include <OpenImageIO/simd.h>
12 #include <OpenImageIO/sysutil.h>
13
14 OIIO_PLUGIN_NAMESPACE_BEGIN
15
16 namespace term_pvt {
17
18
19 class TermOutput : public ImageOutput {
20 public:
TermOutput()21 TermOutput() { init(); }
~TermOutput()22 virtual ~TermOutput() { close(); }
format_name() const23 virtual const char* format_name() const { return "term"; }
24 virtual bool open(const std::string& name, const ImageSpec& spec,
25 OpenMode mode = Create);
26 virtual int supports(string_view feature) const;
27 virtual bool write_scanline(int y, int z, TypeDesc format, const void* data,
28 stride_t xstride);
29 virtual bool write_tile(int x, int y, int z, TypeDesc format,
30 const void* data, stride_t xstride,
31 stride_t ystride, stride_t zstride);
32 virtual bool close();
33
34 private:
35 ImageBuf m_buf;
36 std::string m_method;
37 bool m_fit = true; // automatically fit to window size
38
init()39 void init() { m_buf.clear(); }
40
41 // Actually output the stored buffer to the console
42 bool output();
43 };
44
45
46
47 int
supports(string_view feature) const48 TermOutput::supports(string_view feature) const
49 {
50 return feature == "tiles" || feature == "alpha"
51 || feature == "random_access" || feature == "rewrite"
52 || feature == "procedural";
53 }
54
55
56
57 bool
open(const std::string & name,const ImageSpec & spec,OpenMode mode)58 TermOutput::open(const std::string& name, const ImageSpec& spec, OpenMode mode)
59 {
60 if (mode != Create) {
61 error("%s does not support subimages or MIP levels", format_name());
62 return false;
63 }
64
65 if (spec.nchannels != 3 && spec.nchannels != 4) {
66 error("%s does not support %d-channel images\n", format_name(),
67 m_spec.nchannels);
68 return false;
69 }
70
71 m_spec = spec;
72
73 // Retrieve config hints giving special instructions
74 m_method = Strutil::lower(m_spec["term:method"].get());
75 m_fit = m_spec["term:fit"].get<int>(1);
76
77 // Store temp buffer in HALF format
78 ImageSpec spec2 = m_spec;
79 spec2.set_format(TypeDesc::HALF);
80 m_buf.reset(spec2);
81 ImageBufAlgo::zero(m_buf);
82
83 return true;
84 }
85
86
87
88 bool
write_scanline(int y,int z,TypeDesc format,const void * data,stride_t xstride)89 TermOutput::write_scanline(int y, int z, TypeDesc format, const void* data,
90 stride_t xstride)
91 {
92 if (y > m_spec.height) {
93 error("Attempt to write too many scanlines to terminal");
94 close();
95 return false;
96 }
97 ROI roi(m_spec.x, m_spec.x + m_spec.width, y, y + 1, z, z + 1, 0,
98 m_spec.nchannels);
99 return m_buf.set_pixels(roi, format, data, xstride);
100 }
101
102
103
104 bool
write_tile(int x,int y,int z,TypeDesc format,const void * data,stride_t xstride,stride_t ystride,stride_t zstride)105 TermOutput::write_tile(int x, int y, int z, TypeDesc format, const void* data,
106 stride_t xstride, stride_t ystride, stride_t zstride)
107 {
108 ROI roi(x, std::min(x + m_spec.tile_width, m_spec.x + m_spec.width), y,
109 std::min(y + m_spec.tile_height, m_spec.y + m_spec.height), z,
110 std::min(z + m_spec.tile_depth, m_spec.z + m_spec.depth), 0,
111 m_spec.nchannels);
112 return m_buf.set_pixels(roi, format, data, xstride, ystride, zstride);
113 }
114
115
116
117 bool
close()118 TermOutput::close()
119 {
120 if (!m_buf.initialized())
121 return true; // already closed
122
123 output();
124
125 init(); // clear everything
126 return true;
127 }
128
129
130 bool
output()131 TermOutput::output()
132 {
133 // Color convert in place to sRGB, or it won't look right
134 std::string cspace = m_buf.spec()["oiio:colorspace"].get();
135 ImageBufAlgo::colorconvert(m_buf, m_buf, cspace, "sRGB");
136
137 string_view TERM(Sysutil::getenv("TERM"));
138 string_view TERM_PROGRAM(Sysutil::getenv("TERM_PROGRAM"));
139 string_view TERM_PROGRAM_VERSION(Sysutil::getenv("TERM_PROGRAM_VERSION"));
140 Sysutil::Term term;
141
142 string_view method(m_method);
143 if (method.empty()) {
144 if (TERM_PROGRAM == "iTerm.app"
145 && Strutil::from_string<float>(TERM_PROGRAM_VERSION) >= 2.9) {
146 method = "iterm2";
147 } else if (TERM == "xterm" || TERM == "xterm-256color") {
148 method = "24bit";
149 } else {
150 method = "256color";
151 }
152 }
153
154 // Try to figure out how big an image we can display
155 int w = m_buf.spec().width;
156 int h = m_buf.spec().height;
157 // iTerm2 is special, see bellow
158 int maxw = (method == "iterm2") ? Sysutil::terminal_columns() * 16
159 : Sysutil::terminal_columns();
160 float yscale = (method == "iterm2" || method == "24bit") ? 1.0f : 0.5f;
161 // Resize the image as needed
162 if (w > maxw && m_fit) {
163 ROI newsize(0, maxw, 0, int(std::round(yscale * float(maxw) / w * h)));
164 m_buf = ImageBufAlgo::resize(m_buf, /*filter=*/nullptr, newsize);
165 w = newsize.width();
166 h = newsize.height();
167 }
168
169 if (method == "iterm2") {
170 // iTerm2.app can display entire images in the window, if you use a
171 // special escape sequence that lets you transmit a base64-encoded
172 // image file, so we convert to just a simple PPM and do so.
173 std::ostringstream s;
174 s << "P3\n" << m_buf.spec().width << ' ' << m_buf.spec().height << "\n";
175 s << "255\n";
176 for (int y = m_buf.ybegin(), ye = m_buf.yend(); y < ye; y += 1) {
177 for (int x = m_buf.xbegin(), xe = m_buf.xend(); x < xe; ++x) {
178 unsigned char rgb[3];
179 m_buf.get_pixels(ROI(x, x + 1, y, y + 1, 0, 1, 0, 3),
180 TypeDesc::UINT8, &rgb);
181 s << int(rgb[0]) << ' ' << int(rgb[1]) << ' ' << int(rgb[2])
182 << '\n';
183 }
184 }
185 std::cout << "\033]"
186 << "1337;"
187 << "File=inline=1"
188 << ";width=auto"
189 << ":" << Strutil::base64_encode(s.str()) << '\007'
190 << std::endl;
191 return true;
192 }
193
194 if (method == "24bit") {
195 // Print two vertical pixels per character cell using the Unicode
196 // "upper half block" glyph U+2580, with fg color set to the 24 bit
197 // RGB value of the upper pixel, and bg color set to the 24-bit RGB
198 // value the lower pixel.
199 int z = m_buf.spec().z;
200 for (int y = m_buf.ybegin(), ye = m_buf.yend(); y < ye; y += 2) {
201 for (int x = m_buf.xbegin(), xe = m_buf.xend(); x < xe; ++x) {
202 unsigned char rgb[2][3];
203 m_buf.get_pixels(ROI(x, x + 1, y, y + 2, z, z + 1, 0, 3),
204 TypeDesc::UINT8, &rgb);
205 std::cout << term.ansi_fgcolor(rgb[0][0], rgb[0][1], rgb[0][2]);
206 std::cout << term.ansi_bgcolor(rgb[1][0], rgb[1][1], rgb[1][2])
207 << "\u2580";
208 }
209 std::cout << term.ansi("default") << "\n";
210 }
211 return true;
212 }
213
214 if (method == "24bit-space") {
215 // Print as space, with bg color set to the 24-bit RGB value of each
216 // pixel.
217 int z = m_buf.spec().z;
218 for (int y = m_buf.ybegin(), ye = m_buf.yend(); y < ye; ++y) {
219 for (int x = m_buf.xbegin(), xe = m_buf.xend(); x < xe; ++x) {
220 unsigned char rgb[3];
221 m_buf.get_pixels(ROI(x, x + 1, y, y + 1, z, z + 1, 0, 3),
222 TypeDesc::UINT8, &rgb);
223 std::cout << term.ansi_bgcolor(rgb[0], rgb[1], rgb[2]) << " ";
224 }
225 std::cout << term.ansi("default") << "\n";
226 }
227 return true;
228 }
229
230 if (method == "dither") {
231 // Print as space, with bg color set to the 6x6x6 RGB value of each
232 // pixels. Try to make it better with horizontal dithering. But...
233 // it still looks bad. Room for future improvement?
234 int z = m_buf.spec().z;
235 for (int y = m_buf.ybegin(), ye = m_buf.yend(); y < ye; ++y) {
236 simd::vfloat4 leftover(0.0f);
237 for (int x = m_buf.xbegin(), xe = m_buf.xend(); x < xe; ++x) {
238 simd::vfloat4 rgborig;
239 m_buf.get_pixels(ROI(x, x + 1, y, y + 1, z, z + 1, 0, 3),
240 TypeDesc::FLOAT, &rgborig);
241 rgborig += leftover;
242 simd::vfloat4 rgb = 5.0f * rgborig;
243 simd::vint4 rgbi;
244 OIIO_MAYBE_UNUSED simd::vfloat4 frac = floorfrac(rgb, &rgbi);
245 leftover = rgborig - 0.2f * simd::vfloat4(rgbi);
246 rgbi = clamp(rgbi, simd::vint4(0), simd::vint4(5));
247 std::cout << "\033[48;5;"
248 << (0x10 + 36 * rgbi[0] + 6 * rgbi[1] + rgbi[2])
249 << "m ";
250 }
251 std::cout << term.ansi("default") << "\n";
252 }
253 return true;
254 }
255 {
256 // Print as space, with bg color set to the 6x6x6 RGB value of each
257 // pixels. This looks awful!
258 int z = m_buf.spec().z;
259 for (int y = m_buf.ybegin(), ye = m_buf.yend(); y < ye; ++y) {
260 for (int x = m_buf.xbegin(), xe = m_buf.xend(); x < xe; ++x) {
261 simd::vfloat4 rgborig;
262 m_buf.get_pixels(ROI(x, x + 1, y, y + 1, z, z + 1, 0, 3),
263 TypeDesc::FLOAT, &rgborig);
264 simd::vfloat4 rgb = 5.0f * rgborig;
265 simd::vint4 rgbi;
266 OIIO_MAYBE_UNUSED simd::vfloat4 frac = floorfrac(rgb, &rgbi);
267 rgbi = clamp(rgbi, simd::vint4(0), simd::vint4(5));
268 std::cout << "\033[48;5;"
269 << (0x10 + 36 * rgbi[0] + 6 * rgbi[1] + rgbi[2])
270 << "m ";
271 }
272 std::cout << term.ansi("default") << "\n";
273 }
274 return true;
275 }
276
277 return false;
278 }
279
280
281 } // namespace term_pvt
282
283
284 OIIO_PLUGIN_EXPORTS_BEGIN
285
286 OIIO_EXPORT ImageOutput*
term_output_imageio_create()287 term_output_imageio_create()
288 {
289 return new term_pvt::TermOutput;
290 }
291
292 OIIO_EXPORT int term_imageio_version = OIIO_PLUGIN_VERSION;
293
294 OIIO_EXPORT const char*
term_imageio_library_version()295 term_imageio_library_version()
296 {
297 return nullptr;
298 }
299
300 OIIO_EXPORT const char* term_output_extensions[] = { "term", nullptr };
301
302 OIIO_PLUGIN_EXPORTS_END
303
304 OIIO_PLUGIN_NAMESPACE_END
305