#include "sixel.h" #include #include #define LOG_MODULE "sixel" #define LOG_ENABLE_DBG 0 #include "log.h" #include "debug.h" #include "grid.h" #include "hsl.h" #include "render.h" #include "srgb.h" #include "util.h" #include "xmalloc.h" #include "xsnprintf.h" static size_t count; static void sixel_put_generic(struct terminal *term, uint8_t c); static void sixel_put_ar_11(struct terminal *term, uint8_t c); static uint32_t color_decode_srgb(const struct terminal *term, uint16_t r, uint16_t g, uint16_t b) { if (term->sixel.linear_blending) { if (term->sixel.use_10bit) { r = srgb_decode_8_to_16(r) >> 6; g = srgb_decode_8_to_16(g) >> 6; b = srgb_decode_8_to_16(b) >> 6; } else { r = srgb_decode_8_to_8(r); g = srgb_decode_8_to_8(g); b = srgb_decode_8_to_8(b); } } else { if (term->sixel.use_10bit) { r <<= 2; g <<= 2; b <<= 2; } } uint32_t color; if (term->sixel.use_10bit) { if (PIXMAN_FORMAT_TYPE(term->sixel.pixman_fmt) == PIXMAN_TYPE_ARGB) color = 0x3u << 30 | r << 20 | g << 10 | b; else color = 0x3u << 30 | b << 20 | g << 10 | r; } else color = 0xffu << 24 | r << 16 | g << 8 | b; return color; } void sixel_fini(struct terminal *term) { free(term->sixel.image.data); free(term->sixel.private_palette); free(term->sixel.shared_palette); } sixel_put sixel_init(struct terminal *term, int p1, int p2, int p3) { /* * P1: pixel aspect ratio * - 0,1 - 2:1 * - 2 - 5:1 * - 3,4 - 3:1 * - 5,6 - 2:1 * - 7,8,9 - 1:1 * * P2: background color mode * - 0|2: empty pixels use current background color * - 1: empty pixels remain at their current color (i.e. transparent) * P3: horizontal grid size - ignored */ xassert(term->sixel.image.data == NULL); xassert(term->sixel.palette_size <= SIXEL_MAX_COLORS); /* Default aspect ratio is 2:1 */ const int pad = 1; const int pan = (p1 == 2) ? 5 : (p1 == 3 || p1 == 4) ? 3 : (p1 == 7 || p1 == 8 || p1 == 9) ? 1 : 2; LOG_DBG("initializing sixel with " "p1=%d (pan=%d, pad=%d, aspect-ratio=%d:%d), " "p2=%d (transparent=%s), " "p3=%d (ignored)", p1, pan, pad, pan, pad, p2, p2 == 1 ? "yes" : "no", p3); term->sixel.state = SIXEL_DECSIXEL; term->sixel.pos = (struct coord){0, 0}; term->sixel.color_idx = 0; term->sixel.pan = pan; term->sixel.pad = pad; term->sixel.param = 0; term->sixel.param_idx = 0; memset(term->sixel.params, 0, sizeof(term->sixel.params)); term->sixel.transparent_bg = p2 == 1; term->sixel.image.data = NULL; term->sixel.image.p = NULL; term->sixel.image.width = 0; term->sixel.image.height = 0; term->sixel.image.alloc_height = 0; term->sixel.image.bottom_pixel = 0; term->sixel.linear_blending = wayl_do_linear_blending(term->wl, term->conf); term->sixel.pixman_fmt = PIXMAN_a8r8g8b8; /* * Use higher-precision sixel surfaces if we're using * higher-precision window surfaces. * * This is to a) get more accurate colors when doing gamma-correct * blending, and b) use the same pixman format as the main * surfaces, for (hopefully) better performance. * * For now, don't support 16-bit surfaces (too much sixel logic * that assumes 32-bit pixels). */ if (shm_chain_bit_depth(term->render.chains.grid) >= SHM_BITS_10) { if (term->wl->shm_have_argb2101010) { term->sixel.use_10bit = true; term->sixel.pixman_fmt = PIXMAN_a2r10g10b10; } else if (term->wl->shm_have_abgr2101010) { term->sixel.use_10bit = true; term->sixel.pixman_fmt = PIXMAN_a2b10g10r10; } } const size_t active_palette_entries = min( ALEN(term->conf->colors_dark.sixel), term->sixel.palette_size); if (term->sixel.use_private_palette) { xassert(term->sixel.private_palette == NULL); term->sixel.private_palette = xcalloc( term->sixel.palette_size, sizeof(term->sixel.private_palette[0])); memcpy( term->sixel.private_palette, term->conf->colors_dark.sixel, active_palette_entries * sizeof(term->sixel.private_palette[0])); if (term->sixel.linear_blending || term->sixel.use_10bit) { for (size_t i = 0; i < active_palette_entries; i++) { uint8_t r = (term->sixel.private_palette[i] >> 16) & 0xff; uint8_t g = (term->sixel.private_palette[i] >> 8) & 0xff; uint8_t b = (term->sixel.private_palette[i] >> 0) & 0xff; term->sixel.private_palette[i] = color_decode_srgb(term, r, g, b); } } term->sixel.palette = term->sixel.private_palette; } else { if (term->sixel.shared_palette == NULL) { term->sixel.shared_palette = xcalloc( term->sixel.palette_size, sizeof(term->sixel.shared_palette[0])); memcpy( term->sixel.shared_palette, term->conf->colors_dark.sixel, active_palette_entries * sizeof(term->sixel.shared_palette[0])); if (term->sixel.linear_blending || term->sixel.use_10bit) { for (size_t i = 0; i < active_palette_entries; i++) { uint8_t r = (term->sixel.private_palette[i] >> 16) & 0xff; uint8_t g = (term->sixel.private_palette[i] >> 8) & 0xff; uint8_t b = (term->sixel.private_palette[i] >> 0) & 0xff; term->sixel.private_palette[i] = color_decode_srgb(term, r, g, b); } } } else { /* Shared palette - do *not* reset palette for new sixels */ } term->sixel.palette = term->sixel.shared_palette; } count = 0; return pan == 1 && pad == 1 ? &sixel_put_ar_11 : &sixel_put_generic; } static void sixel_invalidate_cache(struct sixel *sixel) { if (sixel->scaled.pix != NULL) pixman_image_unref(sixel->scaled.pix); free(sixel->scaled.data); sixel->scaled.pix = NULL; sixel->scaled.data = NULL; sixel->scaled.width = -1; sixel->scaled.height = -1; sixel->pix = NULL; sixel->width = -1; sixel->height = -1; } void sixel_destroy(struct sixel *sixel) { sixel_invalidate_cache(sixel); if (sixel->original.pix != NULL) pixman_image_unref(sixel->original.pix); free(sixel->original.data); sixel->original.pix = NULL; sixel->original.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 = sixel->pos.col; c < min(sixel->pos.col + sixel->cols, term->cols); c++) row->cells[c].attrs.clean = 0; } sixel_destroy(sixel); } /* * 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 UNUSED idx = 0; tll_foreach(term->grid->sixel_images, it) { int row = grid_row_abs_to_sb( term->grid, term->rows, it->item.pos.row + it->item.rows - 1); int col = it->item.pos.col; int col_count = it->item.cols; xassert(row <= prev_row); if (row == prev_row) { /* Allowed to be on the same row only if their columns * don't overlap */ xassert(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; xassert(six->pos.row >= 0); xassert(six->pos.row < term->grid->num_rows); int end = (six->pos.row + six->rows - 1) & (term->grid->num_rows - 1); xassert(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 = grid_row_abs_to_sb( term->grid, term->rows, six->pos.row + i); if (last_row != -1) xassert(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); xassert(!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 = grid_row_abs_to_sb( term->grid, term->rows, sixel.pos.row + sixel.rows - 1); tll_foreach(term->grid->sixel_images, it) { int rebased = grid_row_abs_to_sb( term->grid, term->rows, it->item.pos.row + it->item.rows - 1); if (rebased < 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 = grid_row_abs_to_sb(term->grid, term->rows, 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; } } term->bits_affecting_ascii_printer.sixels = tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); verify_sixels(term); } void sixel_scroll_down(struct terminal *term, int rows) { if (likely(tll_length(term->grid->sixel_images) == 0)) return; xassert(term->grid->num_rows >= rows); tll_foreach(term->grid->sixel_images, it) { struct sixel *six = &it->item; int six_end = grid_row_abs_to_sb( term->grid, term->rows, 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; } term->bits_affecting_ascii_printer.sixels = tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); verify_sixels(term); } static void blend_new_image_over_old(const struct terminal *term, const struct sixel *six, pixman_region32_t *six_rect, int row, int col, pixman_image_t **pix, bool *opaque) { xassert(pix != NULL); xassert(opaque != NULL); /* * TODO: handle images being emitted with different cell dimensions */ const int six_ofs_x = six->pos.col * six->cell_width; const int six_ofs_y = six->pos.row * six->cell_height; const int img_ofs_x = col * six->cell_width; const int img_ofs_y = row * six->cell_height; const int img_width = pixman_image_get_width(*pix); const int img_height = pixman_image_get_height(*pix); pixman_region32_t pix_rect; pixman_region32_init_rect( &pix_rect, img_ofs_x, img_ofs_y, img_width, img_height); /* Blend the intersection between the old and new images */ pixman_region32_t intersection; pixman_region32_init(&intersection); pixman_region32_intersect(&intersection, six_rect, &pix_rect); int n_rects = -1; pixman_box32_t *boxes = pixman_region32_rectangles( &intersection, &n_rects); if (n_rects == 0) goto out; xassert(n_rects == 1); pixman_box32_t *box = &boxes[0]; if (!*opaque) { /* * New image is transparent - blend on top of the old * sixel image. */ pixman_image_composite32( PIXMAN_OP_OVER_REVERSE, six->original.pix, NULL, *pix, box->x1 - six_ofs_x, box->y1 - six_ofs_y, 0, 0, box->x1 - img_ofs_x, box->y1 - img_ofs_y, box->x2 - box->x1, box->y2 - box->y1); } /* * Since the old image is split into sub-tiles on a * per-row basis, we need to enlarge the new image and * copy the old image if the old image extends beyond the * new image. * * The "bounding" coordinates are either the edges of the * old image, or the next cell boundary, whichever comes * first. */ int bounding_x = six_ofs_x + six->original.width > img_ofs_x + img_width ? min( six_ofs_x + six->original.width, (box->x2 + six->cell_width - 1) / six->cell_width * six->cell_width) : box->x2; int bounding_y = six_ofs_y + six->original.height > img_ofs_y + img_height ? min( six_ofs_y + six->original.height, (box->y2 + six->cell_height - 1) / six->cell_height * six->cell_height) : box->y2; /* The required size of the new image */ const int required_width = bounding_x - img_ofs_x; const int required_height = bounding_y - img_ofs_y; const int new_width = max(img_width, required_width); const int new_height = max(img_height, required_height); if (new_width <= img_width && new_height <= img_height) goto out; //LOG_INFO("enlarging: %dx%d -> %dx%d", img_width, img_height, new_width, new_height); if (!six->opaque) { /* Transparency is viral */ *opaque = false; } /* Create a new pixmap */ int stride = new_width * sizeof(uint32_t); uint32_t *new_data = xmalloc(stride * new_height); pixman_image_t *pix2 = pixman_image_create_bits_no_clear( term->sixel.pixman_fmt, new_width, new_height, new_data, stride); #if defined(_DEBUG) /* Fill new image with an easy-to-recognize color (green) */ for (size_t i = 0; i < new_width * new_height; i++) new_data[i] = 0xff00ff00; #endif /* Copy the new image, from its old pixmap, to the new pixmap */ pixman_image_composite32( PIXMAN_OP_SRC, *pix, NULL, pix2, 0, 0, 0, 0, 0, 0, img_width, img_height); /* Copy the bottom tile of the old sixel image into the new pixmap */ pixman_image_composite32( PIXMAN_OP_SRC, six->original.pix, NULL, pix2, box->x1 - six_ofs_x, box->y2 - six_ofs_y, 0, 0, box->x1 - img_ofs_x, box->y2 - img_ofs_y, bounding_x - box->x1, bounding_y - box->y2); /* Copy the right tile of the old sixel image into the new pixmap */ pixman_image_composite32( PIXMAN_OP_SRC, six->original.pix, NULL, pix2, box->x2 - six_ofs_x, box->y1 - six_ofs_y, 0, 0, box->x2 - img_ofs_x, box->y1 - img_ofs_y, bounding_x - box->x2, bounding_y - box->y1); /* * Ensure the newly allocated area is initialized. * * Some of it, or all, will have been initialized above, by the * bottom and right tiles from the old sixel image. However, there * may be areas in the new image that isn't covered by the old * image. These areas need to be made transparent. */ pixman_region32_t uninitialized; pixman_region32_init_rects( &uninitialized, (const pixman_box32_t []){ /* Extended image area on the right side */ {img_ofs_x + img_width, img_ofs_y, img_ofs_x + new_width, img_ofs_y + new_height}, /* Bottom */ {img_ofs_x, img_ofs_y + img_height, img_ofs_x + new_width, img_ofs_y + new_height}}, 2); /* Subtract the old sixel image, since the area(s) covered by the * old image has already been copied, and *must* not be * overwritten */ pixman_region32_t diff; pixman_region32_init(&diff); pixman_region32_subtract(&diff, &uninitialized, six_rect); if (pixman_region32_not_empty(&diff)) { pixman_image_t *src = pixman_image_create_solid_fill(&(pixman_color_t){0}); int count = -1; pixman_box32_t *rects = pixman_region32_rectangles(&diff, &count); for (int i = 0; i < count; i++) { pixman_image_composite32( PIXMAN_OP_SRC, src, NULL, pix2, 0, 0, 0, 0, rects[i].x1 - img_ofs_x, rects[i].y1 - img_ofs_y, rects[i].x2 - rects[i].x1, rects[i].y2 - rects[i].y1); } pixman_image_unref(src); *opaque = false; } pixman_region32_fini(&diff); pixman_region32_fini(&uninitialized); /* Use the new pixmap in place of the old one */ free(pixman_image_get_data(*pix)); pixman_image_unref(*pix); *pix = pix2; out: pixman_region32_fini(&intersection); pixman_region32_fini(&pix_rect); } static void sixel_overwrite(struct terminal *term, struct sixel *six, int row, int col, int height, int width, pixman_image_t **pix, bool *opaque) { pixman_region32_t six_rect; pixman_region32_init_rect( &six_rect, six->pos.col * six->cell_width, six->pos.row * six->cell_height, six->original.width, six->original.height); pixman_region32_t overwrite_rect; pixman_region32_init_rect( &overwrite_rect, col * six->cell_width, row * six->cell_height, width * six->cell_width, height * six->cell_height); #if defined(_DEBUG) pixman_region32_t cell_intersection; pixman_region32_init(&cell_intersection); pixman_region32_intersect(&cell_intersection, &six_rect, &overwrite_rect); xassert(!pixman_region32_not_empty(&six_rect) || pixman_region32_not_empty(&cell_intersection)); pixman_region32_fini(&cell_intersection); #endif if (pix != NULL) blend_new_image_over_old(term, six, &six_rect, row, col, pix, opaque); 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); xassert(boxes[i].x1 % six->cell_width == 0); xassert(boxes[i].y1 % six->cell_height == 0); /* New image's position, in cells */ const int new_col = boxes[i].x1 / six->cell_width; const int new_row = boxes[i].y1 / six->cell_height; xassert(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->original.data; /* Pixel offsets into old image backing memory */ const int x_ofs = boxes[i].x1 - six->pos.col * six->cell_width; const int y_ofs = boxes[i].y1 - six->pos.row * six->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->original.width + x_ofs], new_width * sizeof(uint32_t)); } pixman_image_t *new_pix = pixman_image_create_bits_no_clear( term->sixel.pixman_fmt, new_width, new_height, new_data, new_width * sizeof(uint32_t)); struct sixel new_six = { .pix = NULL, .width = -1, .height = -1, .pos = {.col = new_col, .row = new_row}, .cols = (new_width + six->cell_width - 1) / six->cell_width, .rows = (new_height + six->cell_height - 1) / six->cell_height, .opaque = six->opaque, .cell_width = six->cell_width, .cell_height = six->cell_height, .original = { .data = new_data, .pix = new_pix, .width = new_width, .height = new_height, }, .scaled = { .data = NULL, .pix = NULL, .width = -1, .height = -1, }, }; #if defined(_DEBUG) /* Assert we don't cross the scrollback wrap-around */ const int new_end = new_six.pos.row + new_six.rows - 1; xassert(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, pixman_image_t **pix, bool *opaque) { 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 */ xassert(end < term->grid->num_rows); const int scrollback_rel_start = grid_row_abs_to_sb( term->grid, term->rows, 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 = grid_row_abs_to_sb(term->grid, term->rows, six_end); /* We should never generate scrollback wrapping sixels */ xassert(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)) { xassert(!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, pix, opaque); sixel_erase(term, &to_be_erased); } else xassert(!collides); } else xassert(!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; xassert(height - rows_to_wrap_around > 0); _sixel_overwrite_by_rectangle(term, start, col, rows_to_wrap_around, width, NULL, NULL); _sixel_overwrite_by_rectangle(term, 0, col, height - rows_to_wrap_around, width, NULL, NULL); } else _sixel_overwrite_by_rectangle(term, start, col, height, width, NULL, NULL); term->bits_affecting_ascii_printer.sixels = tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); } /* Row numbers are relative to grid offset */ void sixel_overwrite_by_row(struct terminal *term, int _row, int col, int width) { xassert(col >= 0); xassert(_row >= 0); xassert(_row < term->rows); xassert(col >= 0); xassert(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 = grid_row_abs_to_sb(term->grid, term->rows, 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 */ xassert(six_end >= six_start); const int six_scrollback_rel_end = grid_row_abs_to_sb(term->grid, term->rows, 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, NULL, NULL); sixel_erase(term, &to_be_erased); } } } term->bits_affecting_ascii_printer.sixels = tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); } void sixel_overwrite_at_cursor(struct terminal *term, int width) { if (likely(tll_length(term->grid->sixel_images) == 0)) return; sixel_overwrite_by_row( term, term->grid->cursor.point.row, term->grid->cursor.point.col, width); } void sixel_cell_size_changed(struct terminal *term) { tll_foreach(term->normal.sixel_images, it) sixel_invalidate_cache(&it->item); tll_foreach(term->alt.sixel_images, it) sixel_invalidate_cache(&it->item); } void sixel_sync_cache(const struct terminal *term, struct sixel *six) { if (six->pix != NULL) { #if defined(_DEBUG) if (six->cell_width == term->cell_width && six->cell_height == term->cell_height) { xassert(six->pix == six->original.pix); xassert(six->width == six->original.width); xassert(six->height == six->original.height); xassert(six->scaled.data == NULL); xassert(six->scaled.pix == NULL); xassert(six->scaled.width < 0); xassert(six->scaled.height < 0); } else { xassert(six->pix == six->scaled.pix); xassert(six->width == six->scaled.width); xassert(six->height == six->scaled.height); xassert(six->scaled.data != NULL); xassert(six->scaled.pix != NULL); /* TODO: check ratio */ xassert(six->scaled.width >= 0); xassert(six->scaled.height >= 0); } #endif return; } /* Cache should be invalid */ xassert(six->scaled.data == NULL); xassert(six->scaled.pix == NULL); xassert(six->scaled.width < 0); xassert(six->scaled.height < 0); if (six->cell_width == term->cell_width && six->cell_height == term->cell_height) { six->pix = six->original.pix; six->width = six->original.width; six->height = six->original.height; } else { const double width_ratio = (double)term->cell_width / six->cell_width; const double height_ratio = (double)term->cell_height / six->cell_height; struct pixman_f_transform scale; pixman_f_transform_init_scale( &scale, 1. / width_ratio, 1. / height_ratio); struct pixman_transform _scale; pixman_transform_from_pixman_f_transform(&_scale, &scale); pixman_image_set_transform(six->original.pix, &_scale); pixman_image_set_filter(six->original.pix, PIXMAN_FILTER_BILINEAR, NULL, 0); int scaled_width = (double)six->original.width * width_ratio; int scaled_height = (double)six->original.height * height_ratio; int scaled_stride = scaled_width * sizeof(uint32_t); LOG_DBG("scaling sixel: %dx%d -> %dx%d", six->original.width, six->original.height, scaled_width, scaled_height); uint8_t *scaled_data = xmalloc(scaled_height * scaled_stride); pixman_image_t *scaled_pix = pixman_image_create_bits_no_clear( term->sixel.pixman_fmt, scaled_width, scaled_height, (uint32_t *)scaled_data, scaled_stride); pixman_image_composite32( PIXMAN_OP_SRC, six->original.pix, NULL, scaled_pix, 0, 0, 0, 0, 0, 0, scaled_width, scaled_height); pixman_image_set_transform(six->original.pix, NULL); six->scaled.data = scaled_data; six->scaled.pix = six->pix = scaled_pix; six->scaled.width = six->width = scaled_width; six->scaled.height = six->height = scaled_height; } } void sixel_reflow_grid(struct terminal *term, struct grid *grid) { /* Meh - the sixel functions we call use term->grid... */ struct grid *active_grid = term->grid; 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 = grid_row_abs_to_sb( term->grid, term->rows, 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, &it->item.original.pix, &it->item.opaque); if (it->item.original.data != pixman_image_get_data(it->item.original.pix)) { it->item.original.data = pixman_image_get_data(it->item.original.pix); it->item.original.width = pixman_image_get_width(it->item.original.pix); it->item.original.height = pixman_image_get_height(it->item.original.pix); it->item.cols = (it->item.original.width + it->item.cell_width - 1) / it->item.cell_width; it->item.rows = (it->item.original.height + it->item.cell_height - 1) / it->item.cell_height; sixel_invalidate_cache(&it->item); } sixel_insert(term, it->item); } tll_free(copy); term->grid = active_grid; } void sixel_reflow(struct terminal *term) { for (size_t i = 0; i < 2; i++) { struct grid *grid = i == 0 ? &term->normal : &term->alt; sixel_reflow_grid(term, grid); } } void sixel_unhook(struct terminal *term) { if (term->sixel.pos.row < term->sixel.image.height && term->sixel.pos.row + 6 * term->sixel.pan >= term->sixel.image.height) { /* * Handle case where image has had its size set by raster * attributes, and then one or more sixels were printed on the * last row of the RA area. * * In this case, the image height may not be a multiple of * 6*pan. But the printed sixels may still be outside the RA * area. In this case, using the size from the RA would * truncate the image. * * So, extend the image to a multiple of 6*pan. * * If this is a transparent image, the image may get trimmed * below (most likely back the size set by RA). */ term->sixel.image.height = term->sixel.image.alloc_height; } /* Strip trailing fully transparent rows, *unless* we *ended* with * a trailing GNL, in which case we do *not* want to strip all 6 * pixel rows */ if (term->sixel.pos.col > 0) { const int bits = sizeof(term->sixel.image.bottom_pixel) * 8; const int leading_zeroes = term->sixel.image.bottom_pixel == 0 ? bits : __builtin_clz(term->sixel.image.bottom_pixel); const int rows_to_trim = leading_zeroes + 6 - bits; LOG_DBG("bottom-pixel: 0x%02x, bits=%d, leading-zeroes=%d, " "rows-to-trim=%d*%d", term->sixel.image.bottom_pixel, bits, leading_zeroes, rows_to_trim, term->sixel.pan); /* * If the current graphical cursor position is at the last row * of the image, *and* the image is transparent (P2=1), trim * the entire image. * * If the image is not transparent, then we can't trim the RA * region (it is supposed to "erase", with the current * background color.) * * We *do* "trim" transparent rows from the graphical cursor * position, as this affects the positioning of the text * cursor. * * See https://raw.githubusercontent.com/hackerb9/vt340test/main/sixeltests/p2effect.sh */ if (term->sixel.pos.row + 6 * term->sixel.pan >= term->sixel.image.alloc_height) { LOG_DBG("trimming image"); const int trimmed_height = term->sixel.image.alloc_height - rows_to_trim * term->sixel.pan; if (term->sixel.transparent_bg) { /* Image is transparent - trim as much as possible */ term->sixel.image.height = trimmed_height; } else { /* Image is opaque. We can't trim anything "inside" the RA region */ if (trimmed_height > term->sixel.image.height) { /* There are non-empty pixels *outside* the RA region - trim up to that point */ term->sixel.image.height = trimmed_height; } } } else { LOG_DBG("only adjusting cursor position"); } term->sixel.pos.row += 6 * term->sixel.pan; term->sixel.pos.row -= rows_to_trim * term->sixel.pan; } int pixel_row_idx = 0; int pixel_rows_left = term->sixel.image.height; const int stride = term->sixel.image.width * sizeof(uint32_t); /* * When sixel scrolling is enabled (the default), sixels behave * pretty much like normal output; the sixel starts at the current * cursor position and the cursor is moved to a point after the * sixel. * * Furthermore, if the sixel reaches the bottom of the scrolling * region, the terminal content is scrolled. * * When scrolling is disabled, sixels always start at (0,0), the * cursor is not moved at all, and the terminal content never * scrolls. */ const bool do_scroll = term->sixel.scrolling; /* Number of rows we're allowed to use. * * When scrolling is enabled, we always allow the entire sixel to * be emitted. * * When disabled, only the number of screen rows may be used. */ int rows_avail = do_scroll ? (term->sixel.image.height + term->cell_height - 1) / term->cell_height : term->scroll_region.end; /* Initial sixel coordinates */ int start_row = do_scroll ? term->grid->cursor.point.row : 0; const int start_col = do_scroll ? term->grid->cursor.point.col : 0; /* Total number of rows needed by image */ const int rows_needed = (term->sixel.image.height + term->cell_height - 1) / term->cell_height; bool free_image_data = true; /* We do not allow sixels to cross the scrollback wrap-around, as * this makes intersection calculations much more complicated */ while (pixel_rows_left > 0 && rows_avail > 0 && rows_needed <= term->grid->num_rows) { const int cur_row = (term->grid->offset + start_row) & (term->grid->num_rows - 1); const int rows_left_until_wrap_around = term->grid->num_rows - cur_row; const int usable_rows = min(rows_avail, rows_left_until_wrap_around); const int pixel_rows_avail = usable_rows * 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 && height == pixel_rows_left) { /* Entire image will be emitted as a single chunk - reuse * the source buffer */ img_data = term->sixel.image.data; free_image_data = false; } else { xassert(free_image_data); img_data = xmalloc(height * stride); memcpy( img_data, &((uint8_t *)term->sixel.image.data)[pixel_row_idx * stride], height * stride); } struct sixel image = { .pix = NULL, .width = -1, .height = -1, .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}, .opaque = !term->sixel.transparent_bg, .cell_width = term->cell_width, .cell_height = term->cell_height, .original = { .data = img_data, .pix = NULL, .width = width, .height = height, }, .scaled = { .data = NULL, .pix = NULL, .width = -1, .height = -1, }, }; xassert(image.rows <= term->grid->num_rows); xassert(image.pos.row + image.rows - 1 < term->grid->num_rows); LOG_DBG("generating %s %dx%d pixman image at %d-%d", image.opaque ? "opaque" : "transparent", image.original.width, image.original.height, image.pos.row, image.pos.row + image.rows); image.original.pix = pixman_image_create_bits_no_clear( term->sixel.pixman_fmt, image.original.width, image.original.height, img_data, stride); pixel_row_idx += height; pixel_rows_left -= height; rows_avail -= image.rows; if (do_scroll) { /* * Linefeeds - always one less than the number of rows * occupied by the image. * * Unless this is *not* the last chunk. In that case, * linefeed past the chunk, so that the next chunk * "starts" at a "new" row. */ const int linefeed_count = rows_avail == 0 ? max(0, image.rows - 1) : image.rows; xassert(rows_avail == 0 || image.original.height % term->cell_height == 0); for (size_t i = 0; i < linefeed_count; i++) term_linefeed(term); /* Position text cursor if this is the last image chunk */ if (rows_avail == 0) { int row = term->grid->cursor.point.row; /* * Position the text cursor based on the text row * touched by the last sixel */ const int pixel_rows = pixel_rows_left > 0 ? image.original.height : term->sixel.pos.row; const int term_rows = (pixel_rows + term->cell_height - 1) / term->cell_height; xassert(term_rows <= image.rows); row -= (image.rows - term_rows); term_cursor_to( term, max(0, row), (term->sixel.cursor_right_of_graphics ? min(image.pos.col + image.cols, term->cols - 1) : image.pos.col)); } term->sixel.pos.row -= image.original.height; } /* Dirty touched cells, and scroll terminal content if necessary */ for (size_t i = 0; i < image.rows; i++) { struct row *row = term->grid->rows[cur_row + i]; row->dirty = true; for (int col = image.pos.col; col < min(image.pos.col + image.cols, term->cols); col++) { row->cells[col].attrs.clean = 0; } } _sixel_overwrite_by_rectangle( term, image.pos.row, image.pos.col, image.rows, image.cols, &image.original.pix, &image.opaque); if (image.original.data != pixman_image_get_data(image.original.pix)) { image.original.data = pixman_image_get_data(image.original.pix); image.original.width = pixman_image_get_width(image.original.pix); image.original.height = pixman_image_get_height(image.original.pix); image.cols = (image.original.width + image.cell_width - 1) / image.cell_width; image.rows = (image.original.height + image.cell_height - 1) / image.cell_height; sixel_invalidate_cache(&image); } sixel_insert(term, image); if (do_scroll) start_row = term->grid->cursor.point.row; else start_row -= image.rows; } if (free_image_data) free(term->sixel.image.data); term->sixel.image.data = NULL; term->sixel.image.p = NULL; term->sixel.image.width = 0; term->sixel.image.height = 0; term->sixel.pos = (struct coord){0, 0}; free(term->sixel.private_palette); term->sixel.private_palette = NULL; LOG_DBG("you now have %zu sixels in current grid", tll_length(term->grid->sixel_images)); term->bits_affecting_ascii_printer.sixels = tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); render_refresh(term); } static void ALWAYS_INLINE inline memset_u32(uint32_t *data, uint32_t value, size_t count) { static_assert(sizeof(wchar_t) == 4, "wchar_t is not 4 bytes"); wmemset((wchar_t *)data, (wchar_t)value, count); } static void resize_horizontally(struct terminal *term, int new_width_mutable) { if (unlikely(new_width_mutable > term->sixel.max_width)) { LOG_WARN("maximum image dimensions exceeded, truncating"); new_width_mutable = term->sixel.max_width; } if (unlikely(term->sixel.image.width >= new_width_mutable)) return; const int sixel_row_height = 6 * term->sixel.pan; uint32_t *old_data = term->sixel.image.data; const int old_width = term->sixel.image.width; const int new_width = new_width_mutable; int height; if (unlikely(term->sixel.image.height == 0)) { /* Lazy initialize height on first printed sixel */ xassert(old_width == 0); term->sixel.image.height = height = sixel_row_height; term->sixel.image.alloc_height = sixel_row_height; } else height = term->sixel.image.height; LOG_DBG("resizing image horizontally: %dx(%d) -> %dx(%d)", term->sixel.image.width, term->sixel.image.height, new_width, height); int alloc_height = (height + sixel_row_height - 1) / sixel_row_height * sixel_row_height; xassert(new_width >= old_width); xassert(new_width > 0); xassert(alloc_height > 0); /* Width (and thus stride) change - need to allocate a new buffer */ uint32_t *new_data = xmalloc(new_width * alloc_height * sizeof(uint32_t)); uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0]; /* Copy old rows, and initialize new columns to background color */ const uint32_t *end = &new_data[alloc_height * new_width]; for (uint32_t *n = new_data, *o = old_data; n < end; n += new_width, o += old_width) { memcpy(n, o, old_width * sizeof(uint32_t)); memset_u32(&n[old_width], bg, new_width - old_width); } free(old_data); term->sixel.image.data = new_data; term->sixel.image.width = new_width; const int ofs = term->sixel.pos.row * new_width + term->sixel.pos.col; term->sixel.image.p = &term->sixel.image.data[ofs]; } static bool resize_vertically(struct terminal *term, const int new_height) { LOG_DBG("resizing image vertically: (%d)x%d -> (%d)x%d", term->sixel.image.width, term->sixel.image.height, term->sixel.image.width, new_height); if (unlikely(new_height > term->sixel.max_height)) { LOG_WARN("maximum image dimensions reached"); return false; } uint32_t *old_data = term->sixel.image.data; const int width = term->sixel.image.width; const int old_height = term->sixel.image.height; const int sixel_row_height = 6 * term->sixel.pan; int alloc_height = (new_height + sixel_row_height - 1) / sixel_row_height * sixel_row_height; xassert(new_height > 0); if (unlikely(width == 0)) { xassert(term->sixel.image.data == NULL); term->sixel.image.height = new_height; term->sixel.image.alloc_height = alloc_height; return true; } uint32_t *new_data = realloc( old_data, width * alloc_height * sizeof(uint32_t)); if (new_data == NULL) { LOG_ERRNO("failed to reallocate sixel image buffer"); return false; } const uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0]; memset_u32(&new_data[old_height * width], bg, (alloc_height - old_height) * width); term->sixel.image.height = new_height; term->sixel.image.alloc_height = alloc_height; const int ofs = term->sixel.pos.row * term->sixel.image.width + term->sixel.pos.col; term->sixel.image.data = new_data; term->sixel.image.p = &term->sixel.image.data[ofs]; return true; } static bool resize(struct terminal *term, int new_width_mutable, int new_height_mutable) { LOG_DBG("resizing image: %dx%d -> %dx%d", term->sixel.image.width, term->sixel.image.height, new_width_mutable, new_height_mutable); if (unlikely(new_width_mutable > term->sixel.max_width)) { LOG_WARN("maximum image width exceeded, truncating"); new_width_mutable = term->sixel.max_width; } if (unlikely(new_height_mutable > term->sixel.max_height)) { LOG_WARN("maximum image height exceeded, truncating"); new_height_mutable = term->sixel.max_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; const int new_width = new_width_mutable; const int new_height = new_height_mutable; if (unlikely(old_width == new_width && old_height == new_height)) return true; const int sixel_row_height = 6 * term->sixel.pan; const int alloc_new_height = (new_height + sixel_row_height - 1) / sixel_row_height * sixel_row_height; xassert(alloc_new_height >= new_height); xassert(alloc_new_height - new_height < sixel_row_height); uint32_t *new_data = NULL; const uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0]; /* * If the image is resized horizontally, or if it's opaque, we * need to explicitly initialize the "new" pixels. * * When the image is *not* resized horizontally, we simply do a * realloc(). In this case, there's no need to manually copy the * old pixels. We do however need to initialize the new pixels * since realloc() returns uninitialized memory. * * When the image *is* resized horizontally, we need to allocate * new memory (when the width changes, the stride changes, and * thus we cannot simply realloc()) * * If the default background is transparent, the new pixels need * to be initialized to 0x0. We do this by using calloc(). * * If the default background is opaque, then we need to manually * initialize the new pixels. */ const bool initialize_bg = !term->sixel.transparent_bg || new_width == old_width; 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, new_width * alloc_new_height * sizeof(uint32_t)); if (new_data == NULL) { LOG_ERRNO("failed to reallocate sixel image buffer"); return false; } xassert(new_height > old_height); } else { /* Width (and thus stride) change - need to allocate a new buffer */ xassert(new_width > old_width); const size_t pixels = new_width * alloc_new_height; new_data = !initialize_bg ? xcalloc(pixels, sizeof(uint32_t)) : xmalloc(pixels * sizeof(uint32_t)); /* Copy old rows, and initialize new columns to background color */ const int row_copy_count = min(old_height, alloc_new_height); const uint32_t *end = &new_data[row_copy_count * new_width]; for (uint32_t *n = new_data, *o = old_data; n < end; n += new_width, o += old_width) { memcpy(n, o, old_width * sizeof(uint32_t)); memset_u32(&n[old_width], bg, new_width - old_width); } free(old_data); } if (initialize_bg) { memset_u32(&new_data[old_height * new_width], bg, (alloc_new_height - old_height) * new_width); } xassert(new_data != NULL); term->sixel.image.data = new_data; term->sixel.image.width = new_width; term->sixel.image.height = new_height; term->sixel.image.alloc_height = alloc_new_height; term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * new_width + term->sixel.pos.col]; return true; } static void sixel_add_generic(struct terminal *term, uint32_t *data, int stride, uint32_t color, uint8_t sixel) { const int pan = term->sixel.pan; for (int i = 0; i < 6; i++, sixel >>= 1) { if (sixel & 1) { for (int r = 0; r < pan; r++, data += stride) *data = color; } else data += stride * pan; } xassert(sixel == 0); } static void ALWAYS_INLINE inline sixel_add_ar_11(struct terminal *term, uint32_t *data, int stride, uint32_t color, uint8_t sixel) { xassert(term->sixel.pan == 1); if (sixel & 0x01) *data = color; data += stride; if (sixel & 0x02) *data = color; data += stride; if (sixel & 0x04) *data = color; data += stride; if (sixel & 0x08) *data = color; data += stride; if (sixel & 0x10) *data = color; data += stride; if (sixel & 0x20) *data = color; } static void sixel_add_many_generic(struct terminal *term, uint8_t c, unsigned count) { int col = term->sixel.pos.col; int width = term->sixel.image.width; count *= term->sixel.pad; if (unlikely(col + count - 1 >= width)) { resize_horizontally(term, col + count); width = term->sixel.image.width; count = min(count, max(width - col, 0)); if (unlikely(count == 0)) return; } uint32_t color = term->sixel.color; uint32_t *data = term->sixel.image.p; uint32_t *end = data + count; term->sixel.pos.col = col + count; term->sixel.image.p = end; term->sixel.image.bottom_pixel |= c; for (; data < end; data++) sixel_add_generic(term, data, width, color, c); } static void ALWAYS_INLINE inline sixel_add_one_ar_11(struct terminal *term, uint8_t c) { xassert(term->sixel.pan == 1); xassert(term->sixel.pad == 1); int col = term->sixel.pos.col; int width = term->sixel.image.width; if (unlikely(col >= width)) { resize_horizontally(term, col + count); width = term->sixel.image.width; count = min(count, max(width - col, 0)); if (unlikely(count == 0)) return; } uint32_t *data = term->sixel.image.p; term->sixel.pos.col += 1; term->sixel.image.p += 1; term->sixel.image.bottom_pixel |= c; sixel_add_ar_11(term, data, width, term->sixel.color, c); } static void sixel_add_many_ar_11(struct terminal *term, uint8_t c, unsigned count) { xassert(term->sixel.pan == 1); xassert(term->sixel.pad == 1); int col = term->sixel.pos.col; int width = term->sixel.image.width; if (unlikely(col + count - 1 >= width)) { resize_horizontally(term, col + count); width = term->sixel.image.width; count = min(count, max(width - col, 0)); if (unlikely(count == 0)) return; } uint32_t color = term->sixel.color; uint32_t *data = term->sixel.image.p; uint32_t *end = data + count; term->sixel.pos.col += count; term->sixel.image.p = end; term->sixel.image.bottom_pixel |= c; for (; data < end; data++) sixel_add_ar_11(term, data, width, color, c); } IGNORE_WARNING("-Wpedantic") static void decsixel_generic(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; term->sixel.repeat_count = 1; break; case '#': term->sixel.state = SIXEL_DECGCI; term->sixel.color_idx = 0; term->sixel.param = 0; term->sixel.param_idx = 0; break; case '$': if (likely(term->sixel.pos.col <= term->sixel.max_width)) { /* * We set, and keep, 'col' outside the image boundary when * we've reached the maximum image height, to avoid also * having to check the row vs image height in the common * path in sixel_add(). */ term->sixel.pos.col = 0; term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * term->sixel.image.width]; } break; case '-': /* GNL - Graphical New Line */ term->sixel.pos.row += 6 * term->sixel.pan; term->sixel.pos.col = 0; term->sixel.image.bottom_pixel = 0; term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * term->sixel.image.width]; if (term->sixel.pos.row >= term->sixel.image.alloc_height) { if (!resize_vertically(term, term->sixel.pos.row + 6 * term->sixel.pan)) term->sixel.pos.col = term->sixel.max_width + 1 * term->sixel.pad; } break; case '?' ... '~': sixel_add_many_generic(term, c - 63, 1); break; case ' ': case '\n': case '\r': break; default: LOG_WARN("invalid sixel character: '%c' at idx=%zu", c, count); break; } } UNIGNORE_WARNINGS static void decsixel_ar_11(struct terminal *term, uint8_t c) { if (likely(c >= '?' && c <= '~')) sixel_add_one_ar_11(term, c - 63); else decsixel_generic(term, c); } 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; if (likely(term->sixel.image.width == 0 && term->sixel.image.height == 0)) { term->sixel.pan = pan; term->sixel.pad = pad; } else { /* * Unsure what the VT340 does... * * We currently do *not* handle changing pan/pad in the * middle of a sixel, since that means resizing/stretching * the existing image. * * I'm *guessing* the VT340 simply changes the aspect * ratio of all subsequent sixels. But, given the design * of our implementation (the entire sixel is written to a * single pixman image), we can't easily do that. */ LOG_WARN("sixel: unsupported: pan/pad changed after printing sixels"); pan = term->sixel.pan; pad = term->sixel.pad; } pv *= pan; ph *= pad; LOG_DBG("pan=%u, pad=%u (aspect ratio = %d:%d), size=%ux%u", pan, pad, pan, pad, ph, pv); /* * RA really only acts as a rectangular erase - it fills the * specified area with the sixel background color[^1]. Nothing * else. It does *not* affect cursor positioning. * * This means that if the emitted sixel is *smaller* than the * RA, the text cursor will be placed "inside" the RA area. * * This means it would be more correct to view the RA area as * a *separate* sixel image, that is then overlaid with the * actual sixel. * * Still, RA _is_ a hint - the final image is _likely_ going * to be this large. And, treating RA as a separate image * prevents us from pre-allocating the final sixel image. * * So we don't. We use the RA as a hint, and pre-allocates the * backing image buffer. * * [^1]: i.e. it's a NOP if the sixel is transparent */ if (ph >= term->sixel.image.height && pv >= term->sixel.image.width && ph <= term->sixel.max_height && pv <= term->sixel.max_width) { /* * TODO: always resize to a multiple of 6*pan? * * We're effectively doing that already, except * sixel.image.height is set to ph, instead of the * allocated height (which is always a multiple of 6*pan). * * If the user wants to emit a sixel that isn't a multiple * of 6 pixels, the bottom sixel rows should all be empty, * and (assuming a transparent sixel), trimmed when the * final image is generated. */ resize(term, ph, pv); } term->sixel.state = SIXEL_DECSIXEL; /* Update DCS put handler, since pan/pad may have changed */ term->vt.dcs.put_handler = pan == 1 && pad == 1 ? &sixel_put_ar_11 : &sixel_put_generic; if (likely(pan == 1 && pad == 1)) decsixel_ar_11(term, c); else decsixel_generic(term, c); break; } } } IGNORE_WARNING("-Wpedantic") static void decgri_generic(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': { unsigned param = term->sixel.param; param *= 10; param += c - '0'; term->sixel.repeat_count = term->sixel.param = param; break; } case '?' ... '~': { unsigned count = term->sixel.repeat_count; if (unlikely(count == 0)) { count = 1; } sixel_add_many_generic(term, c - 63, count); term->sixel.state = SIXEL_DECSIXEL; break; } default: term->sixel.state = SIXEL_DECSIXEL; term->vt.dcs.put_handler(term, c); break; } } UNIGNORE_WARNINGS static void decgri_ar_11(struct terminal *term, uint8_t c) { if (likely(c >= '?' && c <= '~')) { unsigned count = term->sixel.repeat_count; if (unlikely(count == 0)) { count = 1; } sixel_add_many_ar_11(term, c - 63, count); term->sixel.state = SIXEL_DECSIXEL; } else decgri_generic(term, c); } 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]; int c1 = term->sixel.params[2]; int c2 = term->sixel.params[3]; int c3 = term->sixel.params[4]; switch (format) { case 1: { /* HLS */ int hue = min(c1, 360); int lum = min(c2, 100); int sat = min(c3, 100); /* * Sixel's HLS use the following primary color hues: * blue: 0° * red: 120° * green: 240° * * While "standard" HSL uses: * red: 0° * green: 120° * blue: 240° */ hue = (hue + 240) % 360; uint32_t rgb = hsl_to_rgb(hue, sat, lum); LOG_DBG("setting palette #%d = HLS %hhu/%hhu/%hhu (0x%06x)", term->sixel.color_idx, hue, lum, sat, rgb); term->sixel.palette[term->sixel.color_idx] = 0xffu << 24 | rgb; break; } case 2: { /* RGB */ uint16_t r = 255 * min(c1, 100) / 100; uint16_t g = 255 * min(c2, 100) / 100; uint16_t b = 255 * min(c3, 100) / 100; LOG_DBG("setting palette #%d = RGB %hu/%hu/%hu", term->sixel.color_idx, r, g, b); term->sixel.palette[term->sixel.color_idx] = color_decode_srgb(term, r, g, b); break; } } } else term->sixel.color = term->sixel.palette[term->sixel.color_idx]; term->sixel.state = SIXEL_DECSIXEL; if (likely(term->sixel.pan == 1 && term->sixel.pad == 1)) decsixel_ar_11(term, c); else decsixel_generic(term, c); break; } } } static void sixel_put_generic(struct terminal *term, uint8_t c) { switch (term->sixel.state) { case SIXEL_DECSIXEL: decsixel_generic(term, c); break; case SIXEL_DECGRA: decgra(term, c); break; case SIXEL_DECGRI: decgri_generic(term, c); break; case SIXEL_DECGCI: decgci(term, c); break; } count++; } static void sixel_put_ar_11(struct terminal *term, uint8_t c) { switch (term->sixel.state) { case SIXEL_DECSIXEL: decsixel_ar_11(term, c); break; case SIXEL_DECGRA: decgra(term, c); break; case SIXEL_DECGRI: decgri_ar_11(term, c); break; case SIXEL_DECGCI: decgci(term, c); break; } count++; } void sixel_colors_report_current(struct terminal *term) { char reply[24]; size_t n = xsnprintf(reply, sizeof(reply), "\033[?1;0;%uS", term->sixel.palette_size); term_to_slave(term, reply, n); 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.private_palette); free(term->sixel.shared_palette); term->sixel.private_palette = NULL; term->sixel.shared_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]; size_t n = xsnprintf(reply, sizeof(reply), "\033[?1;0;%uS", SIXEL_MAX_COLORS); term_to_slave(term, reply, n); LOG_DBG("query response for max color count: %u", SIXEL_MAX_COLORS); } void sixel_geometry_report_current(struct terminal *term) { char reply[64]; size_t n = xsnprintf(reply, sizeof(reply), "\033[?2;0;%u;%uS", min(term->cols * term->cell_width, term->sixel.max_width), min(term->rows * term->cell_height, term->sixel.max_height)); term_to_slave(term, reply, n); LOG_DBG("query response for current sixel geometry: %ux%u", term->sixel.max_width, term->sixel.max_height); } void sixel_geometry_reset(struct terminal *term) { LOG_DBG("sixel geometry reset to %ux%u", SIXEL_MAX_WIDTH, SIXEL_MAX_HEIGHT); term->sixel.max_width = SIXEL_MAX_WIDTH; term->sixel.max_height = SIXEL_MAX_HEIGHT; 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->sixel.max_width; unsigned max_height = term->sixel.max_height; char reply[64]; size_t n = xsnprintf(reply, sizeof(reply), "\033[?2;0;%u;%uS", max_width, max_height); term_to_slave(term, reply, n); LOG_DBG("query response for max sixel geometry: %ux%u", max_width, max_height); }