1 // -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; -*-
2 // (c) 2016 Henner Zeller <h.zeller@acm.org>
3 //
4 // This program is free software; you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation version 2.
7 //
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with this program.  If not, see <http://gnu.org/licenses/gpl-2.0.txt>
15 
16 #include "unicode-block-canvas.h"
17 
18 #include <math.h>
19 #include <stdio.h>
20 #include <stdlib.h>
21 #include <string.h>
22 #include <sys/time.h>
23 #include <unistd.h>
24 
25 #define SCREEN_CURSOR_UP_FORMAT    "\033[%dA"  // Move cursor up given lines.
26 #define SCREEN_CURSOR_DN_FORMAT    "\033[%dB"  // Move cursor down given lines.
27 #define SCREEN_CURSOR_RIGHT_FORMAT "\033[%dC"  // Move cursor right given cols
28 
29 #define PIXEL_BLOCK_CHARACTER_LEN strlen("\u2584")  // blocks are 3 bytes UTF8
30 
31 // 24 bit color setting
32 #define PIXEL_SET_FG_COLOR24     "38;2;"
33 #define PIXEL_SET_BG_COLOR24     "48;2;"
34 
35 // 8 bit color setting
36 #define PIXEL_SET_FG_COLOR8     "38;5;"
37 #define PIXEL_SET_BG_COLOR8     "48;5;"
38 
39 
40 #define PIXEL_SET_COLOR_LEN     strlen(PIXEL_SET_FG_COLOR24)  // all the same
41 
42 // Maximum length of the color value sequence
43 #define ESCAPE_COLOR_MAX_LEN    strlen("rrr;ggg;bbb")
44 
45 // We reset the terminal at the end of a line
46 #define SCREEN_END_OF_LINE      "\033[0m\n"
47 #define SCREEN_END_OF_LINE_LEN  strlen(SCREEN_END_OF_LINE)
48 
49 namespace timg {
50 enum BlockChoice : uint8_t {
51     kBackground,
52     kTopLeft, kTopRight,
53     kBotLeft, kBotRight,
54     kLeftBar, kTopLeftBotRight,
55 
56     kLowerBlock,   // Depending on user choice, one of these is used.
57     kUpperBlock,
58 };
59 
60 // Half block rendering:
61 // Each character on the screen is divided in a top pixel and bottom pixel.
62 // We use a block character to fill one half with the foreground color,
63 // the other half is shown as background color.
64 // Some fonts display the top block worse than the bottom block, so use the
65 // bottom block by default, but allow to choose.
66 // Two pixels one stone. Or something.
67 //
68 // Quarter block rendering: similar, but more choices, which means we have
69 // to distribute foreground/background color as averages of the 'real' color.
70 static constexpr const char *kBlockGlyphs[9] = {
71     /*[kBackground] = */      " ",  // space
72     /*[kTopLeft] = */         "▘",  // U+2598 Quadrant upper left
73     /*[kTopRight] = */        "▝",  // U+259D Quadrant upper right
74     /*[kBotLeft] = */         "▖",  // U+2596 Quadrant lower left
75     /*[kBotRight] = */        "▗",  // U+2597 Quadrant lower right
76     /*[kLeftBar] = */         "▌",  // U+258C Left half block
77     /*[kTopLeftBotRight] = */ "▚",  // U+259A Quadrant upper left & lower right
78 
79     /*[kLowerBlock] = */      "▄",  // U+2584 Lower half block
80     /*[kUpperBlock] = */      "▀",  // U+2580 Upper half block
81 };
82 
UnicodeBlockCanvas(BufferedWriteSequencer * ws,bool use_quarter,bool use_upper_half_block,bool use_256_color)83 UnicodeBlockCanvas::UnicodeBlockCanvas(BufferedWriteSequencer *ws,
84                                        bool use_quarter,
85                                        bool use_upper_half_block,
86                                        bool use_256_color)
87     : TerminalCanvas(ws),
88       use_quarter_blocks_(use_quarter),
89       use_upper_half_block_(use_upper_half_block),
90       use_256_color_(use_256_color) {
91 }
92 
~UnicodeBlockCanvas()93 UnicodeBlockCanvas::~UnicodeBlockCanvas() {
94     free(backing_buffer_);
95     free(empty_line_);
96 }
97 
98 static char *int_append_with_semicolon(char *buf, uint8_t val);
AnsiSetFG()99 template <int colorbits> static inline const char *AnsiSetFG() {
100     return colorbits == 8 ? PIXEL_SET_FG_COLOR8 : PIXEL_SET_FG_COLOR24;
101 }
AnsiSetBG()102 template <int colorbits> static inline const char *AnsiSetBG() {
103     return colorbits == 8 ? PIXEL_SET_BG_COLOR8 : PIXEL_SET_BG_COLOR24;
104 }
AnsiWriteColor(char * buf,rgba_t color)105 template <int colorbits> static char *AnsiWriteColor(char *buf, rgba_t color) {
106     static_assert(colorbits == 8 || colorbits == 24, "unsupported color bits");
107     if (colorbits == 8)
108         return int_append_with_semicolon(buf, color.As256TermColor());
109 
110     buf = int_append_with_semicolon(buf, color.r);
111     buf = int_append_with_semicolon(buf, color.g);
112     return int_append_with_semicolon(buf, color.b);
113 }
114 
str_append(char * pos,const char * value,size_t len)115 static inline char *str_append(char *pos, const char *value, size_t len) {
116     memcpy(pos, value, len);
117     return pos + len;
118 }
119 
120 // Compare pixels of top and bottom row with backing store (see StoreBacking())
121 template <int N>
EqualToBacking(const rgba_t * top,const rgba_t * bottom,const rgba_t * backing)122 inline bool EqualToBacking(const rgba_t *top, const rgba_t *bottom,
123                            const rgba_t *backing) {
124     if (N == 1)
125         return *top == backing[0] && *bottom == backing[1];
126     return *top == backing[0] && *(top+1) == backing[1] &&
127         *bottom == backing[2] && *(bottom+1) == backing[3];
128 }
129 
130 // Store pixels of top and bottom row into backing store.
131 template <int N>
StoreBacking(rgba_t * backing,const rgba_t * top,const rgba_t * bottom)132 inline void StoreBacking(rgba_t *backing,
133                          const rgba_t *top, const rgba_t *bottom) {
134     if (N == 1) {
135         backing[0] = *top; backing[1] = *bottom;
136     } else {
137         backing[0] = top[0]; backing[1] = top[1];
138         backing[2] = bottom[0]; backing[3] = bottom[1];
139     }
140 }
141 
is_transparent(rgba_t c)142 inline bool is_transparent(rgba_t c) {  return c.a < 0x60; }
143 
144 struct UnicodeBlockCanvas::GlyphPick {
145     rgba_t fg;
146     rgba_t bg;
147     BlockChoice block;
148 };
149 
150 template <int N>
FindBestGlyph(const rgba_t * top,const rgba_t * bottom) const151 UnicodeBlockCanvas::GlyphPick UnicodeBlockCanvas::FindBestGlyph(
152     const rgba_t *top,
153     const rgba_t *bottom) const {
154     if (N == 1) {
155         if (*top == *bottom ||
156             (is_transparent(*top) && is_transparent(*bottom))) {
157             return { *top, *bottom, kBackground };
158         }
159         if (use_upper_half_block_)
160             return { *top, *bottom, kUpperBlock };
161         return { *bottom, *top, kLowerBlock };
162     }
163     // N == 2
164     const LinearColor tl(top[0]);
165     const LinearColor tr(top[1]);
166     const LinearColor bl(bottom[0]);
167     const LinearColor br(bottom[1]);
168 
169     // If we're all transparent at the top and/or bottom, the choices
170     // we can make for foreground and background are limited.
171     // Even though this adds branches, special casing is worthile.
172     if (is_transparent(top[0]) && is_transparent(top[1]) &&
173         is_transparent(bottom[0]) && is_transparent(bottom[1])) {
174         return { bottom[0], top[0], kBackground };
175     }
176     if (is_transparent(top[0]) && is_transparent(top[1])) {
177         return { linear_average({bl, br}).repack(), top[0], kLowerBlock };
178     }
179     if (is_transparent(bottom[0]) && is_transparent(bottom[1])) {
180         return { linear_average({tl, tr}).repack(), bottom[0], kUpperBlock };
181     }
182 
183     struct Result {
184         LinearColor fg, bg;
185         BlockChoice block = kBackground;
186     } best;
187     float best_distance = 1e12;
188     for (int b = 0; b < 8; ++b) {
189         float d;  // Sum of color distance for each sub-block to average color
190         LinearColor fg, bg;
191         // We can't fix all the blocks that the user tries to work around
192         // with TIMG_USE_UPPER_BLOCK. But fix the half-blocks at least.
193         const BlockChoice block = (BlockChoice)
194             (b < 7 ? b : (use_upper_half_block_ ? kUpperBlock : kLowerBlock));
195         switch (block) {
196         case kBackground:      d = avd(&bg, {tl, tr, bl, br}); fg = bg;   break;
197         case kTopLeft:         d = avd(&bg, {tr, bl, br});     fg = tl;   break;
198         case kTopRight:        d = avd(&bg, {tl, bl, br});     fg = tr;   break;
199         case kBotLeft:         d = avd(&bg, {tl, tr, br});     fg = bl;   break;
200         case kBotRight:        d = avd(&bg, {tl, tr, bl});     fg = br;   break;
201         case kLeftBar:         d = avd(&bg, {tr, br})+avd(&fg, {tl, bl}); break;
202         case kTopLeftBotRight: d = avd(&bg, {tr, bl})+avd(&fg, {tl, br}); break;
203         case kLowerBlock:      d = avd(&bg, {tl, tr})+avd(&fg, {bl, br}); break;
204         case kUpperBlock:      d = avd(&bg, {bl, br})+avd(&fg, {tl, tr}); break;
205         }
206         if (d < best_distance) {
207             best = { fg, bg, block };
208             if (d < 1) break;   // Essentially zero.
209             best_distance = d;
210         }
211     }
212     return { best.fg.repack(), best.bg.repack(), best.block };
213 }
214 
215 // Append two rows of pixels at once.
216 template <int N, int colorbits>   // Advancing N x-pixels per char
AppendDoubleRow(char * pos,int indent,int width,const rgba_t * tline,const rgba_t * bline,bool emit_diff,int * y_skip)217 char *UnicodeBlockCanvas::AppendDoubleRow(char *pos, int indent, int width,
218                                       const rgba_t *tline,
219                                       const rgba_t *bline,
220                                       bool emit_diff,
221                                       int *y_skip) {
222     static constexpr char kStartEscape[] = "\033[";
223     GlyphPick last = {};
224     rgba_t last_foreground = {};
225     bool last_fg_unknown = true;
226     bool last_bg_unknown = true;
227     int x_skip = indent;
228     const char *start = pos;
229     for (int x=0; x < width; x+=N, prev_content_it_+=2*N, tline+=N, bline+=N) {
230         if (emit_diff && EqualToBacking<N>(tline, bline, prev_content_it_)) {
231             ++x_skip;
232             continue;
233         }
234 
235         if (*y_skip) {  // Emit cursor down or newlines, whatever is shorter
236             if (*y_skip <= 4) {
237                 memset(pos, '\n', *y_skip);
238                 pos += *y_skip;
239             } else {
240                 pos += sprintf(pos, SCREEN_CURSOR_DN_FORMAT, *y_skip);
241             }
242             *y_skip = 0;
243         }
244 
245         if (x_skip > 0) {
246             pos += sprintf(pos, SCREEN_CURSOR_RIGHT_FORMAT, x_skip);
247             x_skip = 0;
248         }
249 
250         const GlyphPick pick = FindBestGlyph<N>(tline, bline);
251 
252         bool color_emitted = false;
253 
254         // Foreground. Only consider if we're not having background.
255         if (pick.block != kBackground &&
256             (last_fg_unknown || pick.fg != last_foreground)) {
257             // Appending prefix. At this point, it can only be kStartEscape
258             pos = str_append(pos, kStartEscape, strlen(kStartEscape));
259             pos = str_append(pos, AnsiSetFG<colorbits>(), PIXEL_SET_COLOR_LEN);
260             pos = AnsiWriteColor<colorbits>(pos, pick.fg);
261             color_emitted = true;
262             last_foreground = pick.fg;
263             last_fg_unknown = false;
264         }
265 
266         // Background
267         if (last_bg_unknown || pick.bg != last.bg) {
268             if (!color_emitted) {
269                 pos = str_append(pos, kStartEscape, strlen(kStartEscape));
270             }
271             if (is_transparent(pick.bg)) {
272                 // This is best effort and only happens with -b none
273                 pos = str_append(pos, "49;", 3);  // Reset background color
274             } else {
275                 pos = str_append(pos, AnsiSetBG<colorbits>(),
276                                  PIXEL_SET_COLOR_LEN);
277                 pos = AnsiWriteColor<colorbits>(pos, pick.bg);
278             }
279             color_emitted = true;
280             last_bg_unknown = false;
281         }
282 
283         if (color_emitted) {
284             *(pos-1) = 'm';   // overwrite semicolon with finish ESC seq.
285         }
286         if (pick.block == kBackground) {
287             *pos++ = ' ';  // Simple background 'block'. One character.
288         } else {
289             pos = str_append(pos, kBlockGlyphs[pick.block],
290                              PIXEL_BLOCK_CHARACTER_LEN);
291         }
292         last = pick;
293         StoreBacking<N>(prev_content_it_, tline, bline);
294     }
295 
296     if (pos == start) {  // Nothing emitted for whole line
297         (*y_skip)++;
298     } else {
299         pos = str_append(pos, SCREEN_END_OF_LINE, SCREEN_END_OF_LINE_LEN);
300     }
301 
302     return pos;
303 }
304 
Send(int x,int dy,const Framebuffer & framebuffer,SeqType seq_type,Duration end_of_frame)305 void UnicodeBlockCanvas::Send(int x, int dy,
306                               const Framebuffer &framebuffer,
307                               SeqType seq_type, Duration end_of_frame) {
308     const int width = framebuffer.width();
309     const int height = framebuffer.height();
310     char *const start_buffer = RequestBuffers(width, height);
311     char *pos = start_buffer;
312 
313     if (dy < 0) MoveCursorDY((dy - 1) / 2);
314 
315     pos = AppendPrefixToBuffer(pos);
316 
317     if (use_quarter_blocks_) x /= 2;  // That is in character cell units.
318 
319     const char *before_image_emission = pos;
320 
321     const rgba_t *const pixels = framebuffer.begin();
322     const rgba_t *top_row, *bottom_row;
323 
324     // If we just got requested to move back where we started the last image,
325     // we just need to emit pixels that changed.
326     prev_content_it_ = backing_buffer_;
327     const bool emit_difference = (x == last_x_indent_) &&
328         (last_framebuffer_height_ > 0) && abs(dy) == last_framebuffer_height_;
329 
330     // We are always writing two lines at once with one character, which
331     // requires to leave an empty line if the height of the framebuffer is odd.
332     // We want to make sure that this empty line is written in natural terminal
333     // background color to match the chosen terminal color.
334     // Depending on if we use the upper or lower half block character to show
335     // pixels, we might need to shift displaying by one pixel to make sure
336     // the empty line matches up with the background part of that character.
337     // This it the row_offset we calculate here.
338     const bool needs_empty_line = (height % 2 != 0);
339     const bool top_optional_blank = !use_upper_half_block_;
340     const int row_offset = (needs_empty_line && top_optional_blank) ? -1 : 0;
341 
342     int y_skip = 0;
343     for (int y = 0; y < height; y+=2) {
344         const int row = y + row_offset;
345         top_row = row < 0 ? empty_line_ : &pixels[width*row];
346         bottom_row = (row+1) >= height ? empty_line_ : &pixels[width*(row+1)];
347 
348         if (use_256_color_) {
349             if (use_quarter_blocks_) {
350                 pos = AppendDoubleRow<2, 8>(pos, x, width, top_row, bottom_row,
351                                             emit_difference, &y_skip);
352             } else {
353                 pos = AppendDoubleRow<1, 8>(pos, x, width, top_row, bottom_row,
354                                             emit_difference, &y_skip);
355             }
356         } else {
357             if (use_quarter_blocks_) {
358                 pos = AppendDoubleRow<2, 24>(pos, x, width, top_row, bottom_row,
359                                              emit_difference, &y_skip);
360             } else {
361                 pos = AppendDoubleRow<1, 24>(pos, x, width, top_row, bottom_row,
362                                              emit_difference, &y_skip);
363             }
364         }
365     }
366     last_framebuffer_height_ = height;
367     last_x_indent_ = x;
368     if (before_image_emission == pos) {
369         // Don't even emit cursor up/dn jump, but make sure to return buffer.
370         write_sequencer_->WriteBuffer(start_buffer, 0, seq_type, end_of_frame);
371         return;
372     }
373 
374     if (y_skip) {
375         pos += sprintf(pos, SCREEN_CURSOR_DN_FORMAT, y_skip);
376     }
377     write_sequencer_->WriteBuffer(start_buffer, pos - start_buffer,
378                                   seq_type, end_of_frame);
379 }
380 
RequestBuffers(int width,int height)381 char *UnicodeBlockCanvas::RequestBuffers(int width, int height) {
382     // Pixels will be variable size depending on if we need to change colors
383     // between two adjacent pixels. This is the maximum size they can be.
384     static const int max_pixel_size = strlen("\033[")
385         + PIXEL_SET_COLOR_LEN + ESCAPE_COLOR_MAX_LEN
386         + 1 /* ; */
387         + PIXEL_SET_COLOR_LEN + ESCAPE_COLOR_MAX_LEN
388         + 1 /* m */
389         + PIXEL_BLOCK_CHARACTER_LEN;
390     // Few extra space for number printed in the format.
391     static const int opt_cursor_up = strlen(SCREEN_CURSOR_UP_FORMAT) + 3;
392     static const int opt_cursor_right = strlen(SCREEN_CURSOR_RIGHT_FORMAT) + 3;
393     const int vertical_characters = (height+1) / 2;   // two pixels, one glyph
394     const size_t content_size = opt_cursor_up     // Jump up
395         + vertical_characters
396         * (opt_cursor_right            // Horizontal jump
397            + width * max_pixel_size    // pixels in one row
398            + SCREEN_END_OF_LINE_LEN);  // Finishing a line.
399 
400     // Depending on even/odd situation, we might need one extra row.
401     const size_t new_backing = width * (height+1) * sizeof(rgba_t);
402     if (new_backing > backing_buffer_size_) {
403         backing_buffer_ =(rgba_t*)realloc(backing_buffer_, new_backing);
404         backing_buffer_size_ = new_backing;
405     }
406 
407     const size_t new_empty = width * sizeof(rgba_t);
408     if (new_empty > empty_line_size_) {
409         empty_line_ = (rgba_t*)realloc(empty_line_, new_empty);
410         empty_line_size_ = new_empty;
411         memset(empty_line_, 0x00, empty_line_size_);
412     }
413     return write_sequencer_->RequestBuffer(content_size);
414 }
415 
416 // Converting the colors requires fast uint8 -> ASCII decimal digits with
417 // appended semicolon. There are probably faster ways (send a pull request
418 // if you know one), but this is a good start.
419 // Approach is to specify exactly how many digits to memcpy(), which helps the
420 // compiler create good instructions.
421 
422 // Make sure we're 4 aligned so that we can quickly access chunks of 4 bytes.
423 // While at it, let's go further and align it to 64 byte cache lines.
424 struct digit_convert {
425     char data[1025];
426 };
427 static constexpr digit_convert convert_lookup __attribute__ ((aligned(64))) = {
428     "0;  1;  2;  3;  4;  5;  6;  7;  8;  9;  10; 11; 12; 13; 14; 15; "
429     "16; 17; 18; 19; 20; 21; 22; 23; 24; 25; 26; 27; 28; 29; 30; 31; "
430     "32; 33; 34; 35; 36; 37; 38; 39; 40; 41; 42; 43; 44; 45; 46; 47; "
431     "48; 49; 50; 51; 52; 53; 54; 55; 56; 57; 58; 59; 60; 61; 62; 63; "
432     "64; 65; 66; 67; 68; 69; 70; 71; 72; 73; 74; 75; 76; 77; 78; 79; "
433     "80; 81; 82; 83; 84; 85; 86; 87; 88; 89; 90; 91; 92; 93; 94; 95; "
434     "96; 97; 98; 99; 100;101;102;103;104;105;106;107;108;109;110;111;"
435     "112;113;114;115;116;117;118;119;120;121;122;123;124;125;126;127;"
436     "128;129;130;131;132;133;134;135;136;137;138;139;140;141;142;143;"
437     "144;145;146;147;148;149;150;151;152;153;154;155;156;157;158;159;"
438     "160;161;162;163;164;165;166;167;168;169;170;171;172;173;174;175;"
439     "176;177;178;179;180;181;182;183;184;185;186;187;188;189;190;191;"
440     "192;193;194;195;196;197;198;199;200;201;202;203;204;205;206;207;"
441     "208;209;210;211;212;213;214;215;216;217;218;219;220;221;222;223;"
442     "224;225;226;227;228;229;230;231;232;233;234;235;236;237;238;239;"
443     "240;241;242;243;244;245;246;247;248;249;250;251;252;253;254;255;"
444 };
445 
446 // Append decimal representation plus semicolon of given "value" to "buffer".
447 // Does not \0-terminate. Might write one byte beyond number.
int_append_with_semicolon(char * buffer,uint8_t value)448 static char *int_append_with_semicolon(char *buffer, uint8_t value) {
449     // We cheat a little here: for the beauty of initizliaing the above array
450     // with a block of text, we manually aligned the data array to 4 to
451     // be able to interpret it as uint-array generating fast accesses like
452     //    mov eax, DWORD PTR convert_lookup[0+rax*4]
453     // (only slightly invokong undefined behavior with this type punning :) )
454     const uint32_t *const four_bytes = (const uint32_t*) convert_lookup.data;
455     if (value >= 100) {
456         memcpy(buffer, &four_bytes[value], 4);
457         return buffer + 4;
458     }
459     if (value >= 10) {
460         memcpy(buffer, &four_bytes[value], 4); // copy 4 cheaper than 3
461         return buffer + 3;
462     }
463     memcpy(buffer, &four_bytes[value], 2);
464     return buffer + 2;
465 }
466 
467 }  // namespace timg
468