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