1 /*
2  * Sixel mpv output device implementation based on ffmpeg libavdevice implementation
3  * by Hayaki Saito
4  * https://github.com/saitoha/FFmpeg-SIXEL/blob/sixel/libavdevice/sixel.c
5  *
6  * Copyright (c) 2014 Hayaki Saito
7  *
8  * This file is part of mpv.
9  *
10  * mpv is free software; you can redistribute it and/or
11  * modify it under the terms of the GNU Lesser General Public
12  * License as published by the Free Software Foundation; either
13  * version 2.1 of the License, or (at your option) any later version.
14  *
15  * mpv is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU Lesser General Public License for more details.
19  *
20  * You should have received a copy of the GNU Lesser General Public
21  * License along with mpv.  If not, see <http://www.gnu.org/licenses/>.
22  */
23 
24 #include <stdio.h>
25 #include <stdlib.h>
26 
27 #include <libswscale/swscale.h>
28 #include <sixel.h>
29 
30 #include "config.h"
31 #include "options/m_config.h"
32 #include "osdep/terminal.h"
33 #include "sub/osd.h"
34 #include "vo.h"
35 #include "video/sws_utils.h"
36 #include "video/mp_image.h"
37 
38 #define IMGFMT IMGFMT_RGB24
39 
40 #define TERMINAL_FALLBACK_COLS      80
41 #define TERMINAL_FALLBACK_ROWS      25
42 #define TERMINAL_FALLBACK_PX_WIDTH  320
43 #define TERMINAL_FALLBACK_PX_HEIGHT 240
44 
45 #define ESC_HIDE_CURSOR             "\033[?25l"
46 #define ESC_RESTORE_CURSOR          "\033[?25h"
47 #define ESC_CLEAR_SCREEN            "\033[2J"
48 #define ESC_GOTOXY                  "\033[%d;%df"
49 #define ESC_USE_GLOBAL_COLOR_REG    "\033[?1070l"
50 
51 struct priv {
52 
53     // User specified options
54     int opt_diffuse;
55     int opt_width;
56     int opt_height;
57     int opt_reqcolors;
58     int opt_fixedpal;
59     int opt_threshold;
60     int opt_top;
61     int opt_left;
62     int opt_pad_y;
63     int opt_pad_x;
64     int opt_rows;
65     int opt_cols;
66     int opt_clear;
67 
68     // Internal data
69     sixel_output_t *output;
70     sixel_dither_t *dither;
71     sixel_dither_t *testdither;
72     uint8_t        *buffer;
73     bool            skip_frame_draw;
74 
75     int left, top;  // image origin cell (1 based)
76     int width, height;  // actual image px size - always reflects dst_rect.
77     int num_cols, num_rows;  // terminal size in cells
78     int canvas_ok;  // whether canvas vo->dwidth and vo->dheight are positive
79 
80     int previous_histgram_colors;
81 
82     struct mp_rect src_rect;
83     struct mp_rect dst_rect;
84     struct mp_osd_res osd;
85     struct mp_image *frame;
86     struct mp_sws_context *sws;
87 };
88 
89 static const unsigned int depth = 3;
90 
detect_scene_change(struct vo * vo)91 static int detect_scene_change(struct vo* vo)
92 {
93     struct priv* priv = vo->priv;
94     int previous_histgram_colors = priv->previous_histgram_colors;
95     int histgram_colors = 0;
96 
97     // If threshold is set negative, then every frame must be a scene change
98     if (priv->dither == NULL || priv->opt_threshold < 0)
99         return 1;
100 
101     histgram_colors = sixel_dither_get_num_of_histogram_colors(priv->testdither);
102 
103     int color_difference_count = previous_histgram_colors - histgram_colors;
104     color_difference_count = (color_difference_count > 0) ?  // abs value
105                               color_difference_count : -color_difference_count;
106 
107     if (100 * color_difference_count >
108         priv->opt_threshold * previous_histgram_colors)
109     {
110         priv->previous_histgram_colors = histgram_colors; // update history
111         return 1;
112     } else {
113         return 0;
114     }
115 
116 }
117 
dealloc_dithers_and_buffers(struct vo * vo)118 static void dealloc_dithers_and_buffers(struct vo* vo)
119 {
120     struct priv* priv = vo->priv;
121 
122     if (priv->buffer) {
123         talloc_free(priv->buffer);
124         priv->buffer = NULL;
125     }
126 
127     if (priv->frame) {
128         talloc_free(priv->frame);
129         priv->frame = NULL;
130     }
131 
132     if (priv->dither) {
133         sixel_dither_unref(priv->dither);
134         priv->dither = NULL;
135     }
136 
137     if (priv->testdither) {
138         sixel_dither_unref(priv->testdither);
139         priv->testdither = NULL;
140     }
141 }
142 
prepare_static_palette(struct vo * vo)143 static SIXELSTATUS prepare_static_palette(struct vo* vo)
144 {
145     struct priv* priv = vo->priv;
146 
147     if (!priv->dither) {
148         priv->dither = sixel_dither_get(BUILTIN_XTERM256);
149         if (priv->dither == NULL)
150             return SIXEL_FALSE;
151 
152         sixel_dither_set_diffusion_type(priv->dither, priv->opt_diffuse);
153     }
154 
155     sixel_dither_set_body_only(priv->dither, 0);
156     return SIXEL_OK;
157 }
158 
prepare_dynamic_palette(struct vo * vo)159 static SIXELSTATUS prepare_dynamic_palette(struct vo *vo)
160 {
161     SIXELSTATUS status = SIXEL_FALSE;
162     struct priv *priv = vo->priv;
163 
164     /* create histgram and construct color palette
165      * with median cut algorithm. */
166     status = sixel_dither_initialize(priv->testdither, priv->buffer,
167                                      priv->width, priv->height,
168                                      SIXEL_PIXELFORMAT_RGB888,
169                                      LARGE_NORM, REP_CENTER_BOX,
170                                      QUALITY_LOW);
171     if (SIXEL_FAILED(status))
172         return status;
173 
174     if (detect_scene_change(vo)) {
175         if (priv->dither) {
176             sixel_dither_unref(priv->dither);
177             priv->dither = NULL;
178         }
179 
180         priv->dither = priv->testdither;
181         status = sixel_dither_new(&priv->testdither, priv->opt_reqcolors, NULL);
182 
183         if (SIXEL_FAILED(status))
184             return status;
185 
186         sixel_dither_set_diffusion_type(priv->dither, priv->opt_diffuse);
187     } else {
188         if (priv->dither == NULL)
189             return SIXEL_FALSE;
190     }
191 
192     sixel_dither_set_body_only(priv->dither, 0);
193     return status;
194 }
195 
update_canvas_dimensions(struct vo * vo)196 static void update_canvas_dimensions(struct vo *vo)
197 {
198     // this function sets the vo canvas size in pixels vo->dwidth, vo->dheight,
199     // and the number of rows and columns available in priv->num_rows/cols
200     struct priv *priv   = vo->priv;
201     int num_rows        = TERMINAL_FALLBACK_ROWS;
202     int num_cols        = TERMINAL_FALLBACK_COLS;
203     int total_px_width  = 0;
204     int total_px_height = 0;
205 
206     terminal_get_size2(&num_rows, &num_cols, &total_px_width, &total_px_height);
207 
208     // If the user has specified rows/cols use them for further calculations
209     num_rows = (priv->opt_rows > 0) ? priv->opt_rows : num_rows;
210     num_cols = (priv->opt_cols > 0) ? priv->opt_cols : num_cols;
211 
212     // If the pad value is set in between 0 and width/2 - 1, then we
213     // subtract from the detected width. Otherwise, we assume that the width
214     // output must be a integer multiple of num_cols and accordingly set
215     // total_width to be an integer multiple of num_cols. So in case the padding
216     // added by terminal is less than the number of cells in that axis, then rounding
217     // down will take care of correcting the detected width and remove padding.
218     if (priv->opt_width > 0) {
219         // option - set by the user, hard truth
220         total_px_width = priv->opt_width;
221     } else {
222         if (total_px_width <= 0) {
223                 // ioctl failed to read terminal width
224                 total_px_width = TERMINAL_FALLBACK_PX_WIDTH;
225         } else {
226             if (priv->opt_pad_x >= 0 && priv->opt_pad_x < total_px_width / 2) {
227                 // explicit padding set by the user
228                 total_px_width -= (2 * priv->opt_pad_x);
229             } else {
230                 // rounded "auto padding"
231                 total_px_width = total_px_width / num_cols * num_cols;
232             }
233         }
234     }
235 
236     if (priv->opt_height > 0) {
237         total_px_height = priv->opt_height;
238     } else {
239         if (total_px_height <= 0) {
240             total_px_height = TERMINAL_FALLBACK_PX_HEIGHT;
241         } else {
242             if (priv->opt_pad_y >= 0 && priv->opt_pad_y < total_px_height / 2) {
243                 total_px_height -= (2 * priv->opt_pad_y);
244             } else {
245                 total_px_height = total_px_height / num_rows * num_rows;
246             }
247         }
248     }
249 
250     // use n-1 rows for height
251     // The last row can't be used for encoding image, because after sixel encode
252     // the terminal moves the cursor to next line below the image, causing the
253     // last line to be empty instead of displaying image data.
254     // TODO: Confirm if the output height must be a multiple of 6, if not, remove
255     // the / 6 * 6 part which is setting the height to be a multiple of 6.
256     vo->dheight = total_px_height * (num_rows - 1) / num_rows / 6 * 6;
257     vo->dwidth  = total_px_width;
258 
259     priv->num_rows = num_rows;
260     priv->num_cols = num_cols;
261 
262     priv->canvas_ok = vo->dwidth > 0 && vo->dheight > 0;
263 }
264 
set_sixel_output_parameters(struct vo * vo)265 static void set_sixel_output_parameters(struct vo *vo)
266 {
267     // This function sets output scaled size in priv->width, priv->height
268     // and the scaling rectangles in pixels priv->src_rect, priv->dst_rect
269     // as well as image positioning in cells priv->top, priv->left.
270     struct priv *priv = vo->priv;
271 
272     vo_get_src_dst_rects(vo, &priv->src_rect, &priv->dst_rect, &priv->osd);
273 
274     // priv->width and priv->height are the width and height of dst_rect
275     // and they are not changed anywhere else outside this function.
276     // It is the sixel image output dimension which is output by libsixel.
277     priv->width  = priv->dst_rect.x1 - priv->dst_rect.x0;
278     priv->height = priv->dst_rect.y1 - priv->dst_rect.y0;
279 
280     // top/left values must be greater than 1. If it is set, then
281     // the image will be rendered from there and no further centering is done.
282     priv->top  = (priv->opt_top  > 0) ?  priv->opt_top :
283                   priv->num_rows * priv->dst_rect.y0 / vo->dheight + 1;
284     priv->left = (priv->opt_left > 0) ?  priv->opt_left :
285                   priv->num_cols * priv->dst_rect.x0 / vo->dwidth  + 1;
286 }
287 
update_sixel_swscaler(struct vo * vo,struct mp_image_params * params)288 static int update_sixel_swscaler(struct vo *vo, struct mp_image_params *params)
289 {
290     struct priv *priv = vo->priv;
291 
292     priv->sws->src = *params;
293     priv->sws->src.w = mp_rect_w(priv->src_rect);
294     priv->sws->src.h = mp_rect_h(priv->src_rect);
295     priv->sws->dst = (struct mp_image_params) {
296         .imgfmt = IMGFMT,
297         .w = priv->width,
298         .h = priv->height,
299         .p_w = 1,
300         .p_h = 1,
301     };
302 
303     dealloc_dithers_and_buffers(vo);
304 
305     priv->frame = mp_image_alloc(IMGFMT, priv->width, priv->height);
306     if (!priv->frame)
307         return -1;
308 
309     if (mp_sws_reinit(priv->sws) < 0)
310         return -1;
311 
312     // create testdither only if dynamic palette mode is set
313     if (!priv->opt_fixedpal) {
314         SIXELSTATUS status = sixel_dither_new(&priv->testdither,
315                                               priv->opt_reqcolors, NULL);
316         if (SIXEL_FAILED(status)) {
317             MP_ERR(vo, "update_sixel_swscaler: Failed to create new dither: %s\n",
318                    sixel_helper_format_error(status));
319             return -1;
320         }
321     }
322 
323     priv->buffer =
324         talloc_array(NULL, uint8_t, depth * priv->width * priv->height);
325 
326     return 0;
327 }
328 
reconfig(struct vo * vo,struct mp_image_params * params)329 static int reconfig(struct vo *vo, struct mp_image_params *params)
330 {
331     struct priv *priv = vo->priv;
332     int ret = 0;
333     update_canvas_dimensions(vo);
334     if (priv->canvas_ok) {  // if too small - succeed but skip the rendering
335         set_sixel_output_parameters(vo);
336         ret = update_sixel_swscaler(vo, params);
337     }
338 
339     printf(ESC_CLEAR_SCREEN);
340     vo->want_redraw = true;
341 
342     return ret;
343 }
344 
draw_frame(struct vo * vo,struct vo_frame * frame)345 static void draw_frame(struct vo *vo, struct vo_frame *frame)
346 {
347     struct priv *priv = vo->priv;
348     SIXELSTATUS status;
349     struct mp_image *mpi = NULL;
350 
351     int  prev_rows   = priv->num_rows;
352     int  prev_cols   = priv->num_cols;
353     int  prev_height = vo->dheight;
354     int  prev_width  = vo->dwidth;
355     bool resized     = false;
356     update_canvas_dimensions(vo);
357     if (!priv->canvas_ok)
358         return;
359 
360     if (prev_rows != priv->num_rows || prev_cols != priv->num_cols ||
361         prev_width != vo->dwidth || prev_height != vo->dheight)
362     {
363         set_sixel_output_parameters(vo);
364         // Not checking for vo->config_ok because draw_frame is never called
365         // with a failed reconfig.
366         update_sixel_swscaler(vo, vo->params);
367 
368         printf(ESC_CLEAR_SCREEN);
369         resized = true;
370     }
371 
372     if (frame->repeat && !frame->redraw && !resized) {
373         // Frame is repeated, and no need to update OSD either
374         priv->skip_frame_draw = true;
375         return;
376     } else {
377         // Either frame is new, or OSD has to be redrawn
378         priv->skip_frame_draw = false;
379     }
380 
381     // Normal case where we have to draw the frame and the image is not NULL
382     if (frame->current) {
383         mpi = mp_image_new_ref(frame->current);
384         struct mp_rect src_rc = priv->src_rect;
385         src_rc.x0 = MP_ALIGN_DOWN(src_rc.x0, mpi->fmt.align_x);
386         src_rc.y0 = MP_ALIGN_DOWN(src_rc.y0, mpi->fmt.align_y);
387         mp_image_crop_rc(mpi, src_rc);
388 
389         // scale/pan to our dest rect
390         mp_sws_scale(priv->sws, priv->frame, mpi);
391     } else {
392         // Image is NULL, so need to clear image and draw OSD
393         mp_image_clear(priv->frame, 0, 0, priv->width, priv->height);
394     }
395 
396     struct mp_osd_res dim = {
397         .w = priv->width,
398         .h = priv->height
399     };
400     osd_draw_on_image(vo->osd, dim, mpi ? mpi->pts : 0, 0, priv->frame);
401 
402     // Copy from mpv to RGB format as required by libsixel
403     memcpy_pic(priv->buffer, priv->frame->planes[0], priv->width * depth,
404                priv->height, priv->width * depth, priv->frame->stride[0]);
405 
406     // Even if either of these prepare palette functions fail, on re-running them
407     // they should try to re-initialize the dithers, so it shouldn't dereference
408     // any NULL pointers. flip_page also has a check to make sure dither is not
409     // NULL before drawing, so failure in these functions should still be okay.
410     if (priv->opt_fixedpal) {
411         status = prepare_static_palette(vo);
412     } else {
413         status = prepare_dynamic_palette(vo);
414     }
415 
416     if (SIXEL_FAILED(status)) {
417         MP_WARN(vo, "draw_frame: prepare_palette returned error: %s\n",
418                 sixel_helper_format_error(status));
419     }
420 
421     if (mpi)
422         talloc_free(mpi);
423 }
424 
sixel_write(char * data,int size,void * priv)425 static int sixel_write(char *data, int size, void *priv)
426 {
427     return fwrite(data, 1, size, (FILE *)priv);
428 }
429 
flip_page(struct vo * vo)430 static void flip_page(struct vo *vo)
431 {
432     struct priv* priv = vo->priv;
433     if (!priv->canvas_ok)
434         return;
435 
436     // If frame is repeated and no update required, then we skip encoding
437     if (priv->skip_frame_draw)
438         return;
439 
440     // Make sure that image and dither are valid before drawing
441     if (priv->buffer == NULL || priv->dither == NULL)
442         return;
443 
444     // Go to the offset row and column, then display the image
445     printf(ESC_GOTOXY, priv->top, priv->left);
446     sixel_encode(priv->buffer, priv->width, priv->height,
447                  depth, priv->dither, priv->output);
448     fflush(stdout);
449 }
450 
preinit(struct vo * vo)451 static int preinit(struct vo *vo)
452 {
453     struct priv *priv = vo->priv;
454     SIXELSTATUS status = SIXEL_FALSE;
455     FILE* sixel_output_file = stdout;
456 
457     // Parse opts set by CLI or conf
458     priv->sws = mp_sws_alloc(vo);
459     priv->sws->log = vo->log;
460     mp_sws_enable_cmdline_opts(priv->sws, vo->global);
461 
462     status = sixel_output_new(&priv->output, sixel_write, sixel_output_file, NULL);
463     if (SIXEL_FAILED(status)) {
464         MP_ERR(vo, "preinit: Failed to create output file: %s\n",
465                sixel_helper_format_error(status));
466         return -1;
467     }
468 
469     sixel_output_set_encode_policy(priv->output, SIXEL_ENCODEPOLICY_FAST);
470 
471     printf(ESC_HIDE_CURSOR);
472 
473     /* don't use private color registers for each frame. */
474     printf(ESC_USE_GLOBAL_COLOR_REG);
475 
476     priv->dither = NULL;
477 
478     // create testdither only if dynamic palette mode is set
479     if (!priv->opt_fixedpal) {
480         status = sixel_dither_new(&priv->testdither, priv->opt_reqcolors, NULL);
481         if (SIXEL_FAILED(status)) {
482             MP_ERR(vo, "preinit: Failed to create new dither: %s\n",
483                    sixel_helper_format_error(status));
484             return -1;
485         }
486     }
487 
488     priv->previous_histgram_colors = 0;
489 
490     return 0;
491 }
492 
query_format(struct vo * vo,int format)493 static int query_format(struct vo *vo, int format)
494 {
495     return format == IMGFMT;
496 }
497 
control(struct vo * vo,uint32_t request,void * data)498 static int control(struct vo *vo, uint32_t request, void *data)
499 {
500     if (request == VOCTRL_SET_PANSCAN)
501         return (vo->config_ok && !reconfig(vo, vo->params)) ? VO_TRUE : VO_FALSE;
502     return VO_NOTIMPL;
503 }
504 
505 
uninit(struct vo * vo)506 static void uninit(struct vo *vo)
507 {
508     struct priv *priv = vo->priv;
509 
510     printf(ESC_RESTORE_CURSOR);
511 
512     if (priv->opt_clear) {
513         printf(ESC_CLEAR_SCREEN);
514         printf(ESC_GOTOXY, 1, 1);
515     }
516     fflush(stdout);
517 
518     if (priv->output) {
519         sixel_output_unref(priv->output);
520         priv->output = NULL;
521     }
522 
523     dealloc_dithers_and_buffers(vo);
524 }
525 
526 #define OPT_BASE_STRUCT struct priv
527 
528 const struct vo_driver video_out_sixel = {
529     .name = "sixel",
530     .description = "terminal graphics using sixels",
531     .preinit = preinit,
532     .query_format = query_format,
533     .reconfig = reconfig,
534     .control = control,
535     .draw_frame = draw_frame,
536     .flip_page = flip_page,
537     .uninit = uninit,
538     .priv_size = sizeof(struct priv),
539     .priv_defaults = &(const struct priv) {
540         .opt_diffuse = DIFFUSE_AUTO,
541         .opt_width = 0,
542         .opt_height = 0,
543         .opt_reqcolors = 256,
544         .opt_threshold = -1,
545         .opt_fixedpal = 1,
546         .opt_top = 0,
547         .opt_left = 0,
548         .opt_pad_y = -1,
549         .opt_pad_x = -1,
550         .opt_rows = 0,
551         .opt_cols = 0,
552         .opt_clear = 1,
553     },
554     .options = (const m_option_t[]) {
555         {"dither", OPT_CHOICE(opt_diffuse,
556             {"auto", DIFFUSE_AUTO},
557             {"none", DIFFUSE_NONE},
558             {"atkinson", DIFFUSE_ATKINSON},
559             {"fs", DIFFUSE_FS},
560             {"jajuni", DIFFUSE_JAJUNI},
561             {"stucki", DIFFUSE_STUCKI},
562             {"burkes", DIFFUSE_BURKES},
563             {"arithmetic", DIFFUSE_A_DITHER},
564             {"xor", DIFFUSE_X_DITHER})},
565         {"width", OPT_INT(opt_width)},
566         {"height", OPT_INT(opt_height)},
567         {"reqcolors", OPT_INT(opt_reqcolors)},
568         {"fixedpalette", OPT_FLAG(opt_fixedpal)},
569         {"threshold", OPT_INT(opt_threshold)},
570         {"top", OPT_INT(opt_top)},
571         {"left", OPT_INT(opt_left)},
572         {"pad-y", OPT_INT(opt_pad_y)},
573         {"pad-x", OPT_INT(opt_pad_x)},
574         {"rows", OPT_INT(opt_rows)},
575         {"cols", OPT_INT(opt_cols)},
576         {"exit-clear", OPT_FLAG(opt_clear), },
577         {0}
578     },
579     .options_prefix = "vo-sixel",
580 };
581