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