gvplugin_vt.c 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. /// \file
  2. /// \brief Device that renders using ANSI terminal colors
  3. #include <assert.h>
  4. #include <gvc/gvplugin.h>
  5. #include <gvc/gvplugin_device.h>
  6. #include <limits.h>
  7. #include <stddef.h>
  8. #include <gvc/gvio.h>
  9. /// an ANSI color
  10. typedef struct {
  11. unsigned value;
  12. unsigned red;
  13. unsigned green;
  14. unsigned blue;
  15. } color_t;
  16. /// ANSI 3-bit colors
  17. static const color_t COLORS[] = {
  18. {0, 0x00, 0x00, 0x00}, ///< black
  19. {1, 0xff, 0x00, 0x00}, ///< red
  20. {2, 0x00, 0xff, 0x00}, ///< green
  21. {3, 0xff, 0xff, 0x00}, ///< yellow
  22. {4, 0x00, 0x00, 0xff}, ///< blue
  23. {5, 0xff, 0x00, 0xff}, ///< magenta
  24. {6, 0x00, 0xff, 0xff}, ///< cyan
  25. {7, 0xff, 0xff, 0xff}, ///< white
  26. };
  27. /// a metric of “closeness” to a given color
  28. static unsigned distance(const color_t base, unsigned red, unsigned green,
  29. unsigned blue) {
  30. unsigned diff = 0;
  31. diff += red > base.red ? red - base.red : base.red - red;
  32. diff += green > base.green ? green - base.green : base.green - green;
  33. diff += blue > base.blue ? blue - base.blue : base.blue - blue;
  34. return diff;
  35. }
  36. /// find closest ANSI color
  37. static unsigned get_color(unsigned red, unsigned green, unsigned blue) {
  38. unsigned winner = 0;
  39. unsigned diff = UINT_MAX;
  40. for (size_t i = 0; i < sizeof(COLORS) / sizeof(COLORS[0]); ++i) {
  41. unsigned d = distance(COLORS[i], red, green, blue);
  42. if (d < diff) {
  43. diff = d;
  44. winner = COLORS[i].value;
  45. }
  46. }
  47. return winner;
  48. }
  49. // number of bytes per pixel
  50. static const unsigned BPP = 4;
  51. static void process(GVJ_t *job, int color_depth) {
  52. unsigned char *data = (unsigned char *)job->imagedata;
  53. assert(color_depth == 3 || color_depth == 24);
  54. for (unsigned y = 0; y < job->height; y += 2) {
  55. for (unsigned x = 0; x < job->width; ++x) {
  56. {
  57. // extract the upper pixel
  58. unsigned offset = y * job->width * BPP + x * BPP;
  59. unsigned red = data[offset + 2];
  60. unsigned green = data[offset + 1];
  61. unsigned blue = data[offset];
  62. // use this to select a foreground color
  63. if (color_depth == 3) {
  64. unsigned fg = get_color(red, green, blue);
  65. gvprintf(job, "\033[3%um", fg);
  66. } else {
  67. assert(color_depth == 24);
  68. gvprintf(job, "\033[38;2;%u;%u;%um", red, green, blue);
  69. }
  70. }
  71. {
  72. // extract the lower pixel
  73. unsigned red = 0;
  74. unsigned green = 0;
  75. unsigned blue = 0;
  76. if (y + 1 < job->height) {
  77. unsigned offset = (y + 1) * job->width * BPP + x * BPP;
  78. red = data[offset + 2];
  79. green = data[offset + 1];
  80. blue = data[offset];
  81. }
  82. // use this to select a background color
  83. if (color_depth == 3) {
  84. unsigned bg = get_color(red, green, blue);
  85. gvprintf(job, "\033[4%um", bg);
  86. } else {
  87. assert(color_depth == 24);
  88. gvprintf(job, "\033[48;2;%u;%u;%um", red, green, blue);
  89. }
  90. }
  91. // print unicode “upper half block” to effectively do two rows of
  92. // pixels per one terminal row
  93. gvprintf(job, "▀\033[0m");
  94. }
  95. gvprintf(job, "\n");
  96. }
  97. }
  98. static void process3(GVJ_t *job) { process(job, 3); }
  99. static void process24(GVJ_t *job) { process(job, 24); }
  100. /// convert an RGB color to grayscale
  101. static unsigned rgb_to_grayscale(unsigned red, unsigned green, unsigned blue) {
  102. /// use “perceptual” scaling,
  103. /// https://en.wikipedia.org/wiki/Grayscale#Colorimetric_(perceptual_luminance-preserving)_conversion_to_grayscale
  104. const double r_linear = red / 255.0;
  105. const double g_linear = green / 255.0;
  106. const double b_linear = blue / 255.0;
  107. const double y_linear =
  108. 0.2126 * r_linear + 0.7152 * g_linear + 0.0722 * b_linear;
  109. return (unsigned)(y_linear * 255.999);
  110. }
  111. /// draw a y_stride×x_stride-pixels-per-character monochrome image
  112. ///
  113. /// @param job GVC job to operate on
  114. /// @param y_stride How many Y pixels fit in a character
  115. /// @param x_stride How many X pixels fit in a character
  116. /// @param tiles In-order list of characters for each representation
  117. static void processNup(GVJ_t *job, unsigned y_stride, unsigned x_stride,
  118. const char **tiles) {
  119. assert(y_stride > 0);
  120. assert(x_stride > 0);
  121. assert(tiles != NULL);
  122. for (unsigned i = 0; i < y_stride; ++i) {
  123. for (unsigned j = 0; j < x_stride; ++j) {
  124. assert(tiles[i * x_stride + j] != NULL && "missing or not enough tiles");
  125. }
  126. }
  127. unsigned char *data = (unsigned char *)job->imagedata;
  128. for (unsigned y = 0; y < job->height; y += y_stride) {
  129. for (unsigned x = 0; x < job->width; x += x_stride) {
  130. unsigned index = 0;
  131. for (unsigned y_offset = 0;
  132. y + y_offset < job->height && y_offset < y_stride; ++y_offset) {
  133. for (unsigned x_offset = 0;
  134. x + x_offset < job->width && x_offset < x_stride; ++x_offset) {
  135. const unsigned offset =
  136. (y + y_offset) * job->width * BPP + (x + x_offset) * BPP;
  137. const unsigned red = data[offset + 2];
  138. const unsigned green = data[offset + 1];
  139. const unsigned blue = data[offset];
  140. const unsigned gray = rgb_to_grayscale(red, green, blue);
  141. // The [0, 256) grayscale measurement can be quantized into 16
  142. // 16-stride buckets. I.e. [0, 16) as bucket 1, [16, 32) as bucket 2,
  143. // … Drawing a threshold at 240, and considering only the last bucket
  144. // to be white when converting to monochrome empirically seems to
  145. // generate reasonable results.
  146. const unsigned pixel = gray >= 240;
  147. index |= pixel << (y_offset * x_stride + x_offset);
  148. }
  149. }
  150. gvputs(job, tiles[index]);
  151. }
  152. gvputc(job, '\n');
  153. }
  154. }
  155. /// draw a 4-pixels-per-character monochrome image
  156. static void process4up(GVJ_t *job) {
  157. // block characters from the “Amstrad CPC character set”
  158. const char *tiles[] = {" ", "▘", "▝", "▀", "▖", "▍", "▞", "▛",
  159. "▗", "▚", "▐", "▜", "▃", "▙", "▟", "█"};
  160. const unsigned y_stride = 2;
  161. const unsigned x_stride = 2;
  162. assert(sizeof(tiles) / sizeof(tiles[0]) == 1 << (y_stride * x_stride));
  163. processNup(job, y_stride, x_stride, tiles);
  164. }
  165. /// draw a 6-pixels-per-character monochrome image
  166. static void process6up(GVJ_t *job) {
  167. // the “Teletext G1 Block Mosaics Set”
  168. const char *tiles[] = {" ", "🬀", "🬁", "🬂", "🬃", "🬄", "🬅", "🬆", "🬇", "🬈", "🬉",
  169. "🬊", "🬋", "🬌", "🬍", "🬎", "🬏", "🬐", "🬑", "🬒", "🬓", "▌",
  170. "🬔", "🬕", "🬖", "🬗", "🬘", "🬙", "🬚", "🬛", "🬜", "🬝", "🬞",
  171. "🬟", "🬠", "🬡", "🬢", "🬣", "🬤", "🬥", "🬦", "🬧", "▐", "🬨",
  172. "🬩", "🬪", "🬫", "🬬", "🬭", "🬮", "🬯", "🬰", "🬱", "🬲", "🬳",
  173. "🬴", "🬵", "🬶", "🬷", "🬸", "🬹", "🬺", "🬻", "█"};
  174. const unsigned y_stride = 3;
  175. const unsigned x_stride = 2;
  176. assert(sizeof(tiles) / sizeof(tiles[0]) == 1 << (y_stride * x_stride));
  177. processNup(job, y_stride, x_stride, tiles);
  178. }
  179. /// draw a 8-pixels-per-character monochrome image
  180. static void process8up(GVJ_t *job) {
  181. // the Unicode “Braille Patterns” block
  182. const char *tiles[] = {
  183. " ", "⠁", "⠈", "⠉", "⠂", "⠃", "⠊", "⠋", "⠐", "⠑", "⠘", "⠙", "⠒", "⠓", "⠚",
  184. "⠛", "⠄", "⠅", "⠌", "⠍", "⠆", "⠇", "⠎", "⠏", "⠔", "⠕", "⠜", "⠝", "⠖", "⠗",
  185. "⠞", "⠟", "⠠", "⠡", "⠨", "⠩", "⠢", "⠣", "⠪", "⠫", "⠰", "⠱", "⠸", "⠹", "⠲",
  186. "⠳", "⠺", "⠻", "⠤", "⠥", "⠬", "⠭", "⠦", "⠧", "⠮", "⠯", "⠴", "⠵", "⠼", "⠽",
  187. "⠶", "⠷", "⠾", "⠿", "⡀", "⡁", "⡈", "⡉", "⡂", "⡃", "⡊", "⡋", "⡐", "⡑", "⡘",
  188. "⡙", "⡒", "⡓", "⡚", "⡛", "⡄", "⡅", "⡌", "⡍", "⡆", "⡇", "⡎", "⡏", "⡔", "⡕",
  189. "⡜", "⡝", "⡖", "⡗", "⡞", "⡟", "⡠", "⡡", "⡨", "⡩", "⡢", "⡣", "⡪", "⡫", "⡰",
  190. "⡱", "⡸", "⡹", "⡲", "⡳", "⡺", "⡻", "⡤", "⡥", "⡬", "⡭", "⡦", "⡧", "⡮", "⡯",
  191. "⡴", "⡵", "⡼", "⡽", "⡶", "⡷", "⡾", "⡿", "⢀", "⢁", "⢈", "⢉", "⢂", "⢃", "⢊",
  192. "⢋", "⢐", "⢑", "⢘", "⢙", "⢒", "⢓", "⢚", "⢛", "⢄", "⢅", "⢌", "⢍", "⢆", "⢇",
  193. "⢎", "⢏", "⢔", "⢕", "⢜", "⢝", "⢖", "⢗", "⢞", "⢟", "⢠", "⢡", "⢨", "⢩", "⢢",
  194. "⢣", "⢪", "⢫", "⢰", "⢱", "⢸", "⢹", "⢲", "⢳", "⢺", "⢻", "⢤", "⢥", "⢬", "⢭",
  195. "⢦", "⢧", "⢮", "⢯", "⢴", "⢵", "⢼", "⢽", "⢶", "⢷", "⢾", "⢿", "⣀", "⣁", "⣈",
  196. "⣉", "⣂", "⣃", "⣊", "⣋", "⣐", "⣑", "⣘", "⣙", "⣒", "⣓", "⣚", "⣛", "⣄", "⣅",
  197. "⣌", "⣍", "⣆", "⣇", "⣎", "⣏", "⣔", "⣕", "⣜", "⣝", "⣖", "⣗", "⣞", "⣟", "⣠",
  198. "⣡", "⣨", "⣩", "⣢", "⣣", "⣪", "⣫", "⣰", "⣱", "⣸", "⣹", "⣲", "⣳", "⣺", "⣻",
  199. "⣤", "⣥", "⣬", "⣭", "⣦", "⣧", "⣮", "⣯", "⣴", "⣵", "⣼", "⣽", "⣶", "⣷", "⣾",
  200. "⣿"};
  201. const unsigned y_stride = 4;
  202. const unsigned x_stride = 2;
  203. assert(sizeof(tiles) / sizeof(tiles[0]) == 1 << (y_stride * x_stride));
  204. processNup(job, y_stride, x_stride, tiles);
  205. }
  206. static gvdevice_engine_t engine3 = {
  207. .format = process3,
  208. };
  209. static gvdevice_engine_t engine24 = {
  210. .format = process24,
  211. };
  212. static gvdevice_engine_t engine4up = {
  213. .format = process4up,
  214. };
  215. static gvdevice_engine_t engine6up = {
  216. .format = process6up,
  217. };
  218. static gvdevice_engine_t engine8up = {
  219. .format = process8up,
  220. };
  221. static gvdevice_features_t device_features = {
  222. .default_dpi = {96, 96},
  223. };
  224. static gvplugin_installed_t device_types[] = {
  225. {8, "vt:cairo", 0, &engine3, &device_features},
  226. {1 << 24, "vt-24bit:cairo", 0, &engine24, &device_features},
  227. {4, "vt-4up:cairo", 0, &engine4up, &device_features},
  228. {6, "vt-6up:cairo", 0, &engine6up, &device_features},
  229. {7, "vt-8up:cairo", 0, &engine8up, &device_features},
  230. {0},
  231. };
  232. static gvplugin_api_t apis[] = {
  233. {API_device, device_types},
  234. {(api_t)0, 0},
  235. };
  236. #ifdef GVDLL
  237. #define GVPLUGIN_VT_API __declspec(dllexport)
  238. #else
  239. #define GVPLUGIN_VT_API
  240. #endif
  241. GVPLUGIN_VT_API gvplugin_library_t gvplugin_vt_LTX_library = {"vt", apis};