#include "sixel.h" #include #include #define LOG_MODULE "sixel" #define LOG_ENABLE_DBG 0 #include "log.h" #include "render.h" #include "sixel-hls.h" #include "util.h" #include "xmalloc.h" static size_t count; void sixel_fini(struct terminal *term) { free(term->sixel.palette); } static uint32_t color_with_alpha(const struct terminal *term, uint32_t color) { uint16_t alpha = color == term->colors.bg ? term->colors.alpha : 0xffff; return (alpha / 256u) << 24 | color; } void sixel_init(struct terminal *term) { assert(term->sixel.image.data == NULL); assert(term->sixel.palette_size <= SIXEL_MAX_COLORS); term->sixel.state = SIXEL_DECSIXEL; term->sixel.pos = (struct coord){0, 0}; term->sixel.color_idx = 0; term->sixel.max_col = 0; term->sixel.param = 0; term->sixel.param_idx = 0; memset(term->sixel.params, 0, sizeof(term->sixel.params)); term->sixel.image.data = xmalloc(1 * 6 * sizeof(term->sixel.image.data[0])); term->sixel.image.width = 1; term->sixel.image.height = 6; term->sixel.image.autosize = true; if (term->sixel.palette == NULL) { term->sixel.palette = xcalloc( term->sixel.palette_size, sizeof(term->sixel.palette[0])); } for (size_t i = 0; i < 1 * 6; i++) term->sixel.image.data[i] = color_with_alpha(term, term->colors.bg); count = 0; /* TODO: default palette */ } void sixel_destroy(struct sixel *sixel) { pixman_image_unref(sixel->pix); free(sixel->data); sixel->pix = NULL; sixel->data = NULL; } void sixel_destroy_all(struct terminal *term) { tll_foreach(term->normal.sixel_images, it) sixel_destroy(&it->item); tll_foreach(term->alt.sixel_images, it) sixel_destroy(&it->item); tll_free(term->normal.sixel_images); tll_free(term->alt.sixel_images); } static void sixel_erase(struct terminal *term, struct sixel *sixel) { for (int i = 0; i < sixel->rows; i++) { int r = (sixel->pos.row + i) & (term->grid->num_rows - 1); struct row *row = term->grid->rows[r]; if (row == NULL) { /* A resize/reflow may cause row to now be unallocated */ continue; } row->dirty = true; for (int c = 0; c < term->grid->num_cols; c++) row->cells[c].attrs.clean = 0; } sixel_destroy(sixel); } /* * Calculates the scrollback relative row number, given an absolute row number. * * The scrollback relative row number 0 is the *first*, and *oldest* * row in the scrollback history (and thus the *first* row to be * scrolled out). Thus, a higher number means further *down* in the * scrollback, with the *highest* number being at the bottom of the * screen, where new input appears. */ static int rebase_row(const struct terminal *term, int abs_row) { int scrollback_start = term->grid->offset + term->rows; int rebased_row = abs_row - scrollback_start + term->grid->num_rows; rebased_row &= term->grid->num_rows - 1; return rebased_row; } /* * Verify the sixels are sorted correctly. * * The sixels are sorted on their *end* row, in descending order. This * invariant means the most recent sixels appear first in the list. */ static void verify_list_order(const struct terminal *term) { #if defined(_DEBUG) int prev_row = INT_MAX; int prev_col = -1; int prev_col_count = 0; /* To aid debugging */ size_t idx = 0; tll_foreach(term->grid->sixel_images, it) { int row = rebase_row(term, it->item.pos.row + it->item.rows - 1); int col = it->item.pos.col; int col_count = it->item.cols; assert(row <= prev_row); if (row == prev_row) { /* Allowed to be on the same row only if their columns * don't overlap */ assert(col + col_count <= prev_col || prev_col + prev_col_count <= col); } prev_row = row; prev_col = col; prev_col_count = col_count; idx++; } #endif } /* * Verifies there aren't any sixels that cross the scrollback * wrap-around. This invariant means a sixel's absolute row numbers * are strictly increasing. */ static void verify_no_wraparound_crossover(const struct terminal *term) { #if defined(_DEBUG) tll_foreach(term->grid->sixel_images, it) { const struct sixel *six = &it->item; assert(six->pos.row >= 0); assert(six->pos.row < term->grid->num_rows); int end = (six->pos.row + six->rows - 1) & (term->grid->num_rows - 1); assert(end >= six->pos.row); } #endif } /* * Verify there aren't any sixels that cross the scrollback end. This * invariant means a sixel's rebased row numbers are strictly * increasing. */ static void verify_scrollback_consistency(const struct terminal *term) { #if defined(_DEBUG) tll_foreach(term->grid->sixel_images, it) { const struct sixel *six = &it->item; int last_row = -1; for (int i = 0; i < six->rows; i++) { int row_no = rebase_row(term, six->pos.row + i); if (last_row != -1) assert(last_row < row_no); last_row = row_no; } } #endif } /* * Verifies no sixel overlap with any other sixels. */ static void verify_no_overlap(const struct terminal *term) { #if defined(_DEBUG) tll_foreach(term->grid->sixel_images, it) { const struct sixel *six1 = &it->item; pixman_region32_t rect1; pixman_region32_init_rect( &rect1, six1->pos.col, six1->pos.row, six1->cols, six1->rows); tll_foreach(term->grid->sixel_images, it2) { const struct sixel *six2 = &it2->item; if (six1 == six2) continue; pixman_region32_t rect2; pixman_region32_init_rect( &rect2, six2->pos.col, six2->pos.row, six2->cols, six2->rows); pixman_region32_t intersection; pixman_region32_init(&intersection); pixman_region32_intersect(&intersection, &rect1, &rect2); assert(!pixman_region32_not_empty(&intersection)); pixman_region32_fini(&intersection); pixman_region32_fini(&rect2); } pixman_region32_fini(&rect1); } #endif } static void verify_sixels(const struct terminal *term) { verify_no_wraparound_crossover(term); verify_scrollback_consistency(term); verify_no_overlap(term); verify_list_order(term); } static void sixel_insert(struct terminal *term, struct sixel sixel) { int end_row = rebase_row(term, sixel.pos.row + sixel.rows - 1); tll_foreach(term->grid->sixel_images, it) { if (rebase_row(term, it->item.pos.row + it->item.rows - 1) < end_row) { tll_insert_before(term->grid->sixel_images, it, sixel); goto out; } } tll_push_back(term->grid->sixel_images, sixel); out: #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG LOG_DBG("sixel list after insertion:"); tll_foreach(term->grid->sixel_images, it) { LOG_DBG(" rows=%d+%d", it->item.pos.row, it->item.rows); } #endif verify_sixels(term); } void sixel_scroll_up(struct terminal *term, int rows) { if (likely(tll_length(term->grid->sixel_images) == 0)) return; tll_rforeach(term->grid->sixel_images, it) { struct sixel *six = &it->item; int six_start = rebase_row(term, six->pos.row); if (six_start < rows) { sixel_erase(term, six); tll_remove(term->grid->sixel_images, it); } else { /* * Unfortunately, we cannot break here. * * The sixels are sorted on their *end* row. This means * there may be a sixel with a top row that will be * scrolled out *anywhere* in the list (think of a huuuuge * sixel that covers the entire scrollback) */ //break; } } verify_sixels(term); } void sixel_scroll_down(struct terminal *term, int rows) { if (likely(tll_length(term->grid->sixel_images) == 0)) return; assert(term->grid->num_rows >= rows); tll_foreach(term->grid->sixel_images, it) { struct sixel *six = &it->item; int six_end = rebase_row(term, six->pos.row + six->rows - 1); if (six_end >= term->grid->num_rows - rows) { sixel_erase(term, six); tll_remove(term->grid->sixel_images, it); } else break; } verify_sixels(term); } static void sixel_overwrite(struct terminal *term, struct sixel *six, int row, int col, int height, int width) { pixman_region32_t six_rect; pixman_region32_init_rect( &six_rect, six->pos.col * term->cell_width, six->pos.row * term->cell_height, six->width, six->height); pixman_region32_t overwrite_rect; pixman_region32_init_rect( &overwrite_rect, col * term->cell_width, row * term->cell_height, width * term->cell_width, height * term->cell_height); #if defined(_DEBUG) pixman_region32_t intersection; pixman_region32_init(&intersection); pixman_region32_intersect(&intersection, &six_rect, &overwrite_rect); assert(pixman_region32_not_empty(&intersection)); pixman_region32_fini(&intersection); #endif pixman_region32_t diff; pixman_region32_init(&diff); pixman_region32_subtract(&diff, &six_rect, &overwrite_rect); pixman_region32_fini(&six_rect); pixman_region32_fini(&overwrite_rect); int n_rects = -1; pixman_box32_t *boxes = pixman_region32_rectangles(&diff, &n_rects); for (int i = 0; i < n_rects; i++) { LOG_DBG("box #%d: x1=%d, y1=%d, x2=%d, y2=%d", i, boxes[i].x1, boxes[i].y1, boxes[i].x2, boxes[i].y2); assert(boxes[i].x1 % term->cell_width == 0); assert(boxes[i].y1 % term->cell_height == 0); /* New image's position, in cells */ const int new_col = boxes[i].x1 / term->cell_width; const int new_row = boxes[i].y1 / term->cell_height; assert(new_row < term->grid->num_rows); /* New image's width and height, in pixels */ const int new_width = boxes[i].x2 - boxes[i].x1; const int new_height = boxes[i].y2 - boxes[i].y1; uint32_t *new_data = xmalloc(new_width * new_height * sizeof(uint32_t)); const uint32_t *old_data = six->data; /* Pixel offsets into old image backing memory */ const int x_ofs = boxes[i].x1 - six->pos.col * term->cell_width; const int y_ofs = boxes[i].y1 - six->pos.row * term->cell_height; /* Copy image data, one row at a time */ for (size_t j = 0; j < new_height; j++) { memcpy( &new_data[(0 + j) * new_width], &old_data[(y_ofs + j) * six->width + x_ofs], new_width * sizeof(uint32_t)); } pixman_image_t *new_pix = pixman_image_create_bits_no_clear( PIXMAN_a8r8g8b8, new_width, new_height, new_data, new_width * sizeof(uint32_t)); struct sixel new_six = { .data = new_data, .pix = new_pix, .width = new_width, .height = new_height, .pos = {.col = new_col, .row = new_row}, .cols = (new_width + term->cell_width - 1) / term->cell_width, .rows = (new_height + term->cell_height - 1) / term->cell_height, }; #if defined(_DEBUG) /* Assert we don't cross the scrollback wrap-around */ const int new_end = new_six.pos.row + new_six.rows - 1; assert(new_end < term->grid->num_rows); #endif sixel_insert(term, new_six); } pixman_region32_fini(&diff); } /* Row numbers are absolute */ static void _sixel_overwrite_by_rectangle( struct terminal *term, int row, int col, int height, int width) { verify_sixels(term); #if defined(_DEBUG) pixman_region32_t overwrite_rect; pixman_region32_init_rect(&overwrite_rect, col, row, width, height); #endif const int start = row; const int end = row + height - 1; /* We should never generate scrollback wrapping sixels */ assert(end < term->grid->num_rows); const int scrollback_rel_start = rebase_row(term, start); bool UNUSED would_have_breaked = false; tll_foreach(term->grid->sixel_images, it) { struct sixel *six = &it->item; const int six_start = six->pos.row; const int six_end = (six_start + six->rows - 1); const int six_scrollback_rel_end = rebase_row(term, six_end); /* We should never generate scrollback wrapping sixels */ assert(six_end < term->grid->num_rows); if (six_scrollback_rel_end < scrollback_rel_start) { /* All remaining sixels are *before* our rectangle */ would_have_breaked = true; break; } #if defined(_DEBUG) pixman_region32_t six_rect; pixman_region32_init_rect(&six_rect, six->pos.col, six->pos.row, six->cols, six->rows); pixman_region32_t intersection; pixman_region32_init(&intersection); pixman_region32_intersect(&intersection, &six_rect, &overwrite_rect); const bool collides = pixman_region32_not_empty(&intersection); #else const bool UNUSED collides = false; #endif if ((start <= six_start && end >= six_start) || /* Crosses sixel start boundary */ (start <= six_end && end >= six_end) || /* Crosses sixel end boundary */ (start >= six_start && end <= six_end)) /* Fully within sixel range */ { const int col_start = six->pos.col; const int col_end = six->pos.col + six->cols - 1; if ((col <= col_start && col + width - 1 >= col_start) || (col <= col_end && col + width - 1 >= col_end) || (col >= col_start && col + width - 1 <= col_end)) { assert(!would_have_breaked); struct sixel to_be_erased = *six; tll_remove(term->grid->sixel_images, it); sixel_overwrite(term, &to_be_erased, start, col, height, width); sixel_erase(term, &to_be_erased); } else assert(!collides); } else assert(!collides); #if defined(_DEBUG) pixman_region32_fini(&intersection); pixman_region32_fini(&six_rect); #endif } #if defined(_DEBUG) pixman_region32_fini(&overwrite_rect); #endif } void sixel_overwrite_by_rectangle( struct terminal *term, int row, int col, int height, int width) { if (likely(tll_length(term->grid->sixel_images) == 0)) return; const int start = (term->grid->offset + row) & (term->grid->num_rows - 1); const int end = (start + height - 1) & (term->grid->num_rows - 1); const bool wraps = end < start; if (wraps) { int rows_to_wrap_around = term->grid->num_rows - start; assert(height - rows_to_wrap_around > 0); _sixel_overwrite_by_rectangle(term, start, col, rows_to_wrap_around, width); _sixel_overwrite_by_rectangle(term, 0, col, height - rows_to_wrap_around, width); } else _sixel_overwrite_by_rectangle(term, start, col, height, width); } /* Row numbers are relative to grid offset */ void sixel_overwrite_by_row(struct terminal *term, int _row, int col, int width) { assert(col >= 0); assert(_row >= 0); assert(_row < term->rows); assert(col >= 0); assert(col < term->grid->num_cols); if (likely(tll_length(term->grid->sixel_images) == 0)) return; if (col + width > term->grid->num_cols) width = term->grid->num_cols - col; const int row = (term->grid->offset + _row) & (term->grid->num_rows - 1); const int scrollback_rel_row = rebase_row(term, row); tll_foreach(term->grid->sixel_images, it) { struct sixel *six = &it->item; const int six_start = six->pos.row; const int six_end = (six_start + six->rows - 1) & (term->grid->num_rows - 1); /* We should never generate scrollback wrapping sixels */ assert(six_end >= six_start); const int six_scrollback_rel_end = rebase_row(term, six_end); if (six_scrollback_rel_end < scrollback_rel_row) { /* All remaining sixels are *before* "our" row */ break; } if (row >= six_start && row <= six_end) { const int col_start = six->pos.col; const int col_end = six->pos.col + six->cols - 1; if ((col <= col_start && col + width - 1 >= col_start) || (col <= col_end && col + width - 1 >= col_end) || (col >= col_start && col + width - 1 <= col_end)) { struct sixel to_be_erased = *six; tll_remove(term->grid->sixel_images, it); sixel_overwrite(term, &to_be_erased, row, col, 1, width); sixel_erase(term, &to_be_erased); } } } } void sixel_overwrite_at_cursor(struct terminal *term, int width) { sixel_overwrite_by_row( term, term->grid->cursor.point.row, term->grid->cursor.point.col, width); } void sixel_cell_size_changed(struct terminal *term) { struct grid *g = term->grid; term->grid = &term->normal; tll_foreach(term->normal.sixel_images, it) { struct sixel *six = &it->item; six->rows = (six->height + term->cell_height - 1) / term->cell_height; six->cols = (six->width + term->cell_width - 1) / term->cell_width; } term->grid = &term->alt; tll_foreach(term->alt.sixel_images, it) { struct sixel *six = &it->item; six->rows = (six->height + term->cell_height - 1) / term->cell_height; six->cols = (six->width + term->cell_width - 1) / term->cell_width; } term->grid = g; } void sixel_reflow(struct terminal *term) { struct grid *g = term->grid; for (size_t i = 0; i < 2; i++) { struct grid *grid = i == 0 ? &term->normal : &term->alt; term->grid = grid; /* Need the “real” list to be empty from the beginning */ tll(struct sixel) copy = tll_init(); tll_foreach(grid->sixel_images, it) tll_push_back(copy, it->item); tll_free(grid->sixel_images); tll_rforeach(copy, it) { struct sixel *six = &it->item; int start = six->pos.row; int end = (start + six->rows - 1) & (grid->num_rows - 1); if (end < start) { /* Crosses scrollback wrap-around */ /* TODO: split image */ sixel_destroy(six); continue; } if (six->rows > grid->num_rows) { /* Image too large */ /* TODO: keep bottom part? */ sixel_destroy(six); continue; } /* Drop sixels that now cross the current scrollback end * border. This is similar to a sixel that have been * scrolled out */ /* TODO: should be possible to optimize this */ bool sixel_destroyed = false; int last_row = -1; for (int j = 0; j < six->rows; j++) { int row_no = rebase_row(term, six->pos.row + j); if (last_row != -1 && last_row >= row_no) { sixel_destroy(six); sixel_destroyed = true; break; } last_row = row_no; } if (sixel_destroyed) { LOG_WARN("destroyed sixel that now crossed history"); continue; } /* Sixels that didn’t overlap may now do so, which isn’t * allowed of course */ _sixel_overwrite_by_rectangle( term, six->pos.row, six->pos.col, six->rows, six->cols); sixel_insert(term, it->item); } tll_free(copy); } term->grid = g; } void sixel_unhook(struct terminal *term) { int pixel_row_idx = 0; int pixel_rows_left = term->sixel.image.height; const int stride = term->sixel.image.width * sizeof(uint32_t); /* * Need to 'remember' current cursor column. * * If we split up the sixel (to avoid scrollback wrap-around), we * will emit a carriage-return (after several linefeeds), which * will reset the cursor column to 0. If we use _that_ column for * the subsequent image parts, the image will look sheared. */ const int start_col = term->grid->cursor.point.col; /* We do not allow sixels to cross the scrollback wrap-around, as * this makes intersection calculations much more complicated */ while (pixel_rows_left > 0) { const struct coord *cursor = &term->grid->cursor.point; const int cur_row = (term->grid->offset + cursor->row) & (term->grid->num_rows - 1); const int rows_avail = term->grid->num_rows - cur_row; const int pixel_rows_avail = rows_avail * term->cell_height; const int width = term->sixel.image.width; const int height = min(pixel_rows_left, pixel_rows_avail); uint32_t *img_data; if (pixel_row_idx == 0) img_data = term->sixel.image.data; else { img_data = xmalloc(height * stride); memcpy( img_data, &((uint8_t *)term->sixel.image.data)[pixel_row_idx * stride], height * stride); } struct sixel image = { .data = img_data, .width = width, .height = height, .rows = (height + term->cell_height - 1) / term->cell_height, .cols = (width + term->cell_width - 1) / term->cell_width, .pos = (struct coord){start_col, cur_row}, }; assert(image.rows < term->grid->num_rows); assert(image.pos.row + image.rows - 1 < term->grid->num_rows); LOG_DBG("generating %dx%d pixman image at %d-%d", image.width, image.height, image.pos.row, image.pos.row + image.rows); image.pix = pixman_image_create_bits_no_clear( PIXMAN_a8r8g8b8, image.width, image.height, img_data, stride); /* Allocate space *first*, then insert */ for (size_t i = 0; i < image.rows; i++) term_linefeed(term); term_carriage_return(term); _sixel_overwrite_by_rectangle( term, image.pos.row, image.pos.col, image.rows, image.cols); sixel_insert(term, image); pixel_row_idx += height; pixel_rows_left -= height; } term->sixel.image.data = NULL; term->sixel.image.width = 0; term->sixel.image.height = 0; term->sixel.max_col = 0; term->sixel.pos = (struct coord){0, 0}; LOG_DBG("you now have %zu sixels in current grid", tll_length(term->grid->sixel_images)); render_refresh(term); } static unsigned max_width(const struct terminal *term) { /* foot extension - treat 0 to mean current terminal size */ return term->sixel.max_width == 0 ? term->cols * term->cell_width : term->sixel.max_width; } static unsigned max_height(const struct terminal *term) { /* foot extension - treat 0 to mean current terminal size */ return term->sixel.max_height == 0 ? term->rows * term->cell_height : term->sixel.max_height; } static bool resize(struct terminal *term, int new_width, int new_height) { if (!term->sixel.image.autosize) return false; LOG_DBG("resizing image: %dx%d -> %dx%d", term->sixel.image.width, term->sixel.image.height, new_width, new_height); uint32_t *old_data = term->sixel.image.data; const int old_width = term->sixel.image.width; const int old_height = term->sixel.image.height; int alloc_new_width = new_width; int alloc_new_height = (new_height + 6 - 1) / 6 * 6; assert(alloc_new_height >= new_height); assert(alloc_new_height - new_height < 6); assert(new_width >= old_width); assert(new_height >= old_height); uint32_t *new_data = NULL; if (new_width == old_width) { /* Width (and thus stride) is the same, so we can simply * re-alloc the existing buffer */ new_data = realloc(old_data, alloc_new_width * alloc_new_height * sizeof(uint32_t)); if (new_data == NULL) { LOG_ERRNO("failed to reallocate sixel image buffer"); return false; } assert(new_height > old_height); } else { /* Width (and thus stride) change - need to allocate a new buffer */ assert(new_width > old_width); new_data = xmalloc(alloc_new_width * alloc_new_height * sizeof(uint32_t)); /* Copy old rows, and initialize new columns to background color */ for (int r = 0; r < old_height; r++) { memcpy(&new_data[r * new_width], &old_data[r * old_width], old_width * sizeof(uint32_t)); for (int c = old_width; c < new_width; c++) new_data[r * new_width + c] = color_with_alpha(term, term->colors.bg); } free(old_data); } /* Initialize new rows to background color */ for (int r = old_height; r < new_height; r++) { for (int c = 0; c < new_width; c++) new_data[r * new_width + c] = color_with_alpha(term, term->colors.bg); } assert(new_data != NULL); term->sixel.image.data = new_data; term->sixel.image.width = new_width; term->sixel.image.height = new_height; return true; } static void sixel_add(struct terminal *term, uint32_t color, uint8_t sixel) { //LOG_DBG("adding sixel %02hhx using color 0x%06x", sixel, color); if (term->sixel.pos.col >= max_width(term) || term->sixel.pos.row * 6 + 5 >= max_height(term)) { return; } if (term->sixel.pos.col >= term->sixel.image.width || term->sixel.pos.row * 6 + 5 >= (term->sixel.image.height + 6 - 1) / 6 * 6) { int width = max( term->sixel.image.width, max(term->sixel.max_col, term->sixel.pos.col + 1)); int height = max( term->sixel.image.height, (term->sixel.pos.row + 1) * 6); if (!resize(term, width, height)) return; } for (int i = 0; i < 6; i++, sixel >>= 1) { if (sixel & 1) { size_t pixel_row = term->sixel.pos.row * 6 + i; size_t stride = term->sixel.image.width; size_t idx = pixel_row * stride + term->sixel.pos.col; term->sixel.image.data[idx] = color_with_alpha(term, color); } } assert(sixel == 0); term->sixel.pos.col++; } static void decsixel(struct terminal *term, uint8_t c) { switch (c) { case '"': term->sixel.state = SIXEL_DECGRA; term->sixel.param = 0; term->sixel.param_idx = 0; break; case '!': term->sixel.state = SIXEL_DECGRI; term->sixel.param = 0; term->sixel.param_idx = 0; break; case '#': term->sixel.state = SIXEL_DECGCI; term->sixel.color_idx = 0; term->sixel.param = 0; term->sixel.param_idx = 0; break; case '$': if (term->sixel.pos.col > term->sixel.max_col) term->sixel.max_col = term->sixel.pos.col; term->sixel.pos.col = 0; break; case '-': if (term->sixel.pos.col > term->sixel.max_col) term->sixel.max_col = term->sixel.pos.col; term->sixel.pos.row++; term->sixel.pos.col = 0; break; case '?': case '@': case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': case '[': case '\\': case ']': case '^': case '_': case '`': case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': case '{': case '|': case '}': case '~': sixel_add(term, term->sixel.palette[term->sixel.color_idx], c - 63); break; case ' ': case '\n': case '\r': break; default: LOG_WARN("invalid sixel character: '%c' at idx=%zu", c, count); break; } } static void decgra(struct terminal *term, uint8_t c) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': term->sixel.param *= 10; term->sixel.param += c - '0'; break; case ';': if (term->sixel.param_idx < ALEN(term->sixel.params)) term->sixel.params[term->sixel.param_idx++] = term->sixel.param; term->sixel.param = 0; break; default: { if (term->sixel.param_idx < ALEN(term->sixel.params)) term->sixel.params[term->sixel.param_idx++] = term->sixel.param; int nparams = term->sixel.param_idx; unsigned pan = nparams > 0 ? term->sixel.params[0] : 0; unsigned pad = nparams > 1 ? term->sixel.params[1] : 0; unsigned ph = nparams > 2 ? term->sixel.params[2] : 0; unsigned pv = nparams > 3 ? term->sixel.params[3] : 0; pan = pan > 0 ? pan : 1; pad = pad > 0 ? pad : 1; LOG_DBG("pan=%u, pad=%u (aspect ratio = %u), size=%ux%u", pan, pad, pan / pad, ph, pv); if (ph >= term->sixel.image.height && pv >= term->sixel.image.width && ph <= max_height(term) && pv <= max_width(term)) { if (resize(term, ph, pv)) term->sixel.image.autosize = false; } term->sixel.state = SIXEL_DECSIXEL; sixel_put(term, c); break; } } } static void decgri(struct terminal *term, uint8_t c) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': term->sixel.param *= 10; term->sixel.param += c - '0'; break; default: //LOG_DBG("repeating '%c' %u times", c, term->sixel.param); for (unsigned i = 0; i < term->sixel.param; i++) decsixel(term, c); term->sixel.state = SIXEL_DECSIXEL; break; } } static void decgci(struct terminal *term, uint8_t c) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': term->sixel.param *= 10; term->sixel.param += c - '0'; break; case ';': if (term->sixel.param_idx < ALEN(term->sixel.params)) term->sixel.params[term->sixel.param_idx++] = term->sixel.param; term->sixel.param = 0; break; default: { if (term->sixel.param_idx < ALEN(term->sixel.params)) term->sixel.params[term->sixel.param_idx++] = term->sixel.param; int nparams = term->sixel.param_idx; if (nparams > 0) term->sixel.color_idx = min(term->sixel.params[0], term->sixel.palette_size - 1); if (nparams > 4) { unsigned format = term->sixel.params[1]; unsigned c1 = term->sixel.params[2]; unsigned c2 = term->sixel.params[3]; unsigned c3 = term->sixel.params[4]; switch (format) { case 1: { /* HLS */ uint32_t rgb = hls_to_rgb(c1, c2, c3); LOG_DBG("setting palette #%d = HLS %hhu/%hhu/%hhu (0x%06x)", term->sixel.color_idx, c1, c2, c3, rgb); term->sixel.palette[term->sixel.color_idx] = rgb; break; } case 2: { /* RGB */ uint8_t r = 255 * c1 / 100; uint8_t g = 255 * c2 / 100; uint8_t b = 255 * c3 / 100; LOG_DBG("setting palette #%d = RGB %hhu/%hhu/%hhu", term->sixel.color_idx, r, g, b); term->sixel.palette[term->sixel.color_idx] = r << 16 | g << 8 | b; break; } } } term->sixel.state = SIXEL_DECSIXEL; sixel_put(term, c); break; } } } void sixel_put(struct terminal *term, uint8_t c) { switch (term->sixel.state) { case SIXEL_DECSIXEL: decsixel(term, c); break; case SIXEL_DECGRA: decgra(term, c); break; case SIXEL_DECGRI: decgri(term, c); break; case SIXEL_DECGCI: decgci(term, c); break; } count++; } void sixel_colors_report_current(struct terminal *term) { char reply[24]; snprintf(reply, sizeof(reply), "\033[?1;0;%uS", term->sixel.palette_size); term_to_slave(term, reply, strlen(reply)); LOG_DBG("query response for current color count: %u", term->sixel.palette_size); } void sixel_colors_reset(struct terminal *term) { LOG_DBG("sixel palette size reset to %u", SIXEL_MAX_COLORS); free(term->sixel.palette); term->sixel.palette = NULL; term->sixel.palette_size = SIXEL_MAX_COLORS; sixel_colors_report_current(term); } void sixel_colors_set(struct terminal *term, unsigned count) { unsigned new_palette_size = min(max(2, count), SIXEL_MAX_COLORS); LOG_DBG("sixel palette size set to %u", new_palette_size); free(term->sixel.palette); term->sixel.palette = NULL; term->sixel.palette_size = new_palette_size; sixel_colors_report_current(term); } void sixel_colors_report_max(struct terminal *term) { char reply[24]; snprintf(reply, sizeof(reply), "\033[?1;0;%uS", SIXEL_MAX_COLORS); term_to_slave(term, reply, strlen(reply)); LOG_DBG("query response for max color count: %u", SIXEL_MAX_COLORS); } void sixel_geometry_report_current(struct terminal *term) { char reply[64]; snprintf(reply, sizeof(reply), "\033[?2;0;%u;%uS", max_width(term), max_height(term)); term_to_slave(term, reply, strlen(reply)); LOG_DBG("query response for current sixel geometry: %ux%u", max_width(term), max_height(term)); } void sixel_geometry_reset(struct terminal *term) { LOG_DBG("sixel geometry reset to %ux%u", max_width(term), max_height(term)); term->sixel.max_width = 0; term->sixel.max_height = 0; sixel_geometry_report_current(term); } void sixel_geometry_set(struct terminal *term, unsigned width, unsigned height) { LOG_DBG("sixel geometry set to %ux%u", width, height); term->sixel.max_width = width; term->sixel.max_height = height; sixel_geometry_report_current(term); } void sixel_geometry_report_max(struct terminal *term) { unsigned max_width = term->cols * term->cell_width; unsigned max_height = term->rows * term->cell_height; char reply[64]; snprintf(reply, sizeof(reply), "\033[?2;0;%u;%uS", max_width, max_height); term_to_slave(term, reply, strlen(reply)); LOG_DBG("query response for max sixel geometry: %ux%u", max_width, max_height); }