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