forked from dnkl/foot
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1622 lines
50 KiB
1622 lines
50 KiB
#include "sixel.h" |
|
|
|
#include <string.h> |
|
#include <limits.h> |
|
|
|
#define LOG_MODULE "sixel" |
|
#define LOG_ENABLE_DBG 0 |
|
#include "log.h" |
|
#include "debug.h" |
|
#include "render.h" |
|
#include "hsl.h" |
|
#include "util.h" |
|
#include "xmalloc.h" |
|
#include "xsnprintf.h" |
|
|
|
static size_t count; |
|
|
|
void |
|
sixel_fini(struct terminal *term) |
|
{ |
|
free(term->sixel.image.data); |
|
free(term->sixel.private_palette); |
|
free(term->sixel.shared_palette); |
|
} |
|
|
|
void |
|
sixel_init(struct terminal *term, int p1, int p2, int p3) |
|
{ |
|
/* |
|
* P1: pixel aspect ratio - unimplemented |
|
* 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); |
|
|
|
term->sixel.state = SIXEL_DECSIXEL; |
|
term->sixel.pos = (struct coord){0, 0}; |
|
term->sixel.max_non_empty_row_no = -1; |
|
term->sixel.row_byte_ofs = 0; |
|
term->sixel.color_idx = 0; |
|
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 = xmalloc(1 * 6 * sizeof(term->sixel.image.data[0])); |
|
term->sixel.image.width = 1; |
|
term->sixel.image.height = 6; |
|
|
|
/* TODO: default palette */ |
|
|
|
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])); |
|
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])); |
|
} else { |
|
/* Shared palette - do *not* reset palette for new sixels */ |
|
} |
|
|
|
term->sixel.palette = term->sixel.shared_palette; |
|
} |
|
|
|
term->sixel.default_bg = term->sixel.transparent_bg |
|
? 0x00000000u |
|
: 0xffu << 24 | (term->vt.attrs.have_bg |
|
? term->vt.attrs.bg |
|
: term->colors.bg); |
|
|
|
for (size_t i = 0; i < 1 * 6; i++) |
|
term->sixel.image.data[i] = term->sixel.default_bg; |
|
|
|
count = 0; |
|
} |
|
|
|
void |
|
sixel_destroy(struct sixel *sixel) |
|
{ |
|
if (sixel->pix != NULL) |
|
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 = sixel->pos.col; c < min(sixel->cols, term->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; |
|
|
|
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 = rebase_row(term, 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 = 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; |
|
} |
|
} |
|
|
|
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 = 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; |
|
} |
|
|
|
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); |
|
|
|
const int six_ofs_x = six->pos.col * term->cell_width; |
|
const int six_ofs_y = six->pos.row * term->cell_height; |
|
const int img_ofs_x = col * term->cell_width; |
|
const int img_ofs_y = row * term->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->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->width > img_ofs_x + img_width |
|
? min( |
|
six_ofs_x + six->width, |
|
(box->x2 + term->cell_width - 1) / term->cell_width * term->cell_width) |
|
: box->x2; |
|
int bounding_y = six_ofs_y + six->height > img_ofs_y + img_height |
|
? min( |
|
six_ofs_y + six->height, |
|
(box->y2 + term->cell_height - 1) / term->cell_height * term->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( |
|
PIXMAN_a8r8g8b8, 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->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->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 * 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 cell_intersection; |
|
pixman_region32_init(&cell_intersection); |
|
pixman_region32_intersect(&cell_intersection, &six_rect, &overwrite_rect); |
|
xassert(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 % term->cell_width == 0); |
|
xassert(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; |
|
|
|
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->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, |
|
.opaque = six->opaque, |
|
}; |
|
|
|
#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 = 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 */ |
|
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_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 = 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 */ |
|
xassert(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, NULL, NULL); |
|
sixel_erase(term, &to_be_erased); |
|
} |
|
} |
|
} |
|
|
|
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) |
|
{ |
|
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, |
|
&it->item.pix, &it->item.opaque); |
|
|
|
if (it->item.data != pixman_image_get_data(it->item.pix)) { |
|
it->item.data = pixman_image_get_data(it->item.pix); |
|
it->item.width = pixman_image_get_width(it->item.pix); |
|
it->item.height = pixman_image_get_height(it->item.pix); |
|
it->item.cols = (it->item.width + term->cell_width - 1) / term->cell_width; |
|
it->item.rows = (it->item.height + term->cell_height - 1) / term->cell_height; |
|
} |
|
|
|
sixel_insert(term, it->item); |
|
} |
|
|
|
tll_free(copy); |
|
} |
|
|
|
term->grid = g; |
|
} |
|
|
|
void |
|
sixel_unhook(struct terminal *term) |
|
{ |
|
if (term->sixel.image.height > term->sixel.max_non_empty_row_no + 1) { |
|
LOG_DBG( |
|
"last row only partially filled, reducing image height: %d -> %d", |
|
term->sixel.image.height, term->sixel.max_non_empty_row_no + 1); |
|
term->sixel.image.height = term->sixel.max_non_empty_row_no + 1; |
|
} |
|
|
|
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 (+ optional newline at the end) */ |
|
const int rows_needed = |
|
(term->sixel.image.height + term->cell_height - 1) / term->cell_height + |
|
(term->sixel.cursor_right_of_graphics ? 0 : 1); |
|
|
|
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 = { |
|
.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}, |
|
.opaque = !term->sixel.transparent_bg, |
|
}; |
|
|
|
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.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); |
|
|
|
pixel_row_idx += height; |
|
pixel_rows_left -= height; |
|
rows_avail -= image.rows; |
|
|
|
/* 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; |
|
} |
|
|
|
if (do_scroll) { |
|
/* |
|
* Linefeed, *unless* we're on the very last row of |
|
* the final image (not just this chunk) and private |
|
* mode 8452 (leave cursor at the right of graphics) |
|
* is enabled. |
|
*/ |
|
if (term->sixel.cursor_right_of_graphics && |
|
rows_avail == 0 && |
|
i >= image.rows - 1) |
|
{ |
|
term_cursor_to( |
|
term, |
|
term->grid->cursor.point.row, |
|
min(image.pos.col + image.cols, term->cols - 1)); |
|
} else { |
|
term_linefeed(term); |
|
term_carriage_return(term); |
|
} |
|
} |
|
} |
|
|
|
_sixel_overwrite_by_rectangle( |
|
term, image.pos.row, image.pos.col, image.rows, image.cols, |
|
&image.pix, &image.opaque); |
|
|
|
if (image.data != pixman_image_get_data(image.pix)) { |
|
image.data = pixman_image_get_data(image.pix); |
|
image.width = pixman_image_get_width(image.pix); |
|
image.height = pixman_image_get_height(image.pix); |
|
image.cols = (image.width + term->cell_width - 1) / term->cell_width; |
|
image.rows = (image.height + term->cell_height - 1) / term->cell_height; |
|
} |
|
|
|
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.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_update_ascii_printer(term); |
|
render_refresh(term); |
|
} |
|
|
|
static bool |
|
resize_horizontally(struct terminal *term, int new_width) |
|
{ |
|
LOG_DBG("resizing image horizontally: %dx(%d) -> %dx(%d)", |
|
term->sixel.image.width, term->sixel.image.height, |
|
new_width, term->sixel.image.height); |
|
|
|
if (unlikely(new_width > term->sixel.max_width)) { |
|
LOG_WARN("maximum image dimensions reached"); |
|
return false; |
|
} |
|
|
|
uint32_t *old_data = term->sixel.image.data; |
|
const int old_width = term->sixel.image.width; |
|
const int height = term->sixel.image.height; |
|
|
|
int alloc_height = (height + 6 - 1) / 6 * 6; |
|
|
|
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.default_bg; |
|
|
|
/* Copy old rows, and initialize new columns to background color */ |
|
for (int r = 0; r < 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] = bg; |
|
} |
|
|
|
free(old_data); |
|
|
|
term->sixel.image.data = new_data; |
|
term->sixel.image.width = new_width; |
|
term->sixel.row_byte_ofs = term->sixel.pos.row * new_width; |
|
return true; |
|
} |
|
|
|
static bool |
|
resize_vertically(struct terminal *term, 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; |
|
|
|
int alloc_height = (new_height + 6 - 1) / 6 * 6; |
|
|
|
xassert(width > 0); |
|
xassert(new_height > 0); |
|
|
|
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; |
|
} |
|
|
|
uint32_t bg = term->sixel.default_bg; |
|
|
|
/* Initialize new rows to background color */ |
|
for (int r = old_height; r < new_height; r++) { |
|
for (int c = 0; c < width; c++) |
|
new_data[r * width + c] = bg; |
|
} |
|
|
|
term->sixel.image.data = new_data; |
|
term->sixel.image.height = new_height; |
|
return true; |
|
} |
|
|
|
static bool |
|
resize(struct terminal *term, int new_width, int new_height) |
|
{ |
|
LOG_DBG("resizing image: %dx%d -> %dx%d", |
|
term->sixel.image.width, term->sixel.image.height, |
|
new_width, new_height); |
|
|
|
if (new_width > term->sixel.max_width || |
|
new_height > term->sixel.max_height) |
|
{ |
|
LOG_WARN("maximum image dimensions reached"); |
|
return false; |
|
} |
|
|
|
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; |
|
xassert(alloc_new_height >= new_height); |
|
xassert(alloc_new_height - new_height < 6); |
|
|
|
uint32_t *new_data = NULL; |
|
uint32_t bg = term->sixel.default_bg; |
|
|
|
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; |
|
} |
|
|
|
xassert(new_height > old_height); |
|
|
|
} else { |
|
/* Width (and thus stride) change - need to allocate a new buffer */ |
|
xassert(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 < min(old_height, new_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] = 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] = bg; |
|
} |
|
|
|
xassert(new_data != NULL); |
|
term->sixel.image.data = new_data; |
|
term->sixel.image.width = new_width; |
|
term->sixel.image.height = new_height; |
|
term->sixel.row_byte_ofs = term->sixel.pos.row * new_width; |
|
|
|
return true; |
|
} |
|
|
|
static void |
|
sixel_add(struct terminal *term, int col, int width, uint32_t color, uint8_t sixel) |
|
{ |
|
xassert(term->sixel.pos.col < term->sixel.image.width); |
|
xassert(term->sixel.pos.row < term->sixel.image.height); |
|
|
|
size_t ofs = term->sixel.row_byte_ofs + col; |
|
uint32_t *data = &term->sixel.image.data[ofs]; |
|
|
|
int max_non_empty_row = -1; |
|
int row = term->sixel.pos.row; |
|
|
|
for (int i = 0; i < 6; i++, sixel >>= 1, data += width) { |
|
if (sixel & 1) { |
|
*data = color; |
|
max_non_empty_row = row + i; |
|
} |
|
} |
|
|
|
xassert(sixel == 0); |
|
|
|
term->sixel.max_non_empty_row_no = max( |
|
term->sixel.max_non_empty_row_no, |
|
max_non_empty_row); |
|
} |
|
|
|
static void |
|
sixel_add_many(struct terminal *term, uint8_t c, unsigned count) |
|
{ |
|
int col = term->sixel.pos.col; |
|
int width = term->sixel.image.width; |
|
|
|
if (unlikely(col + count - 1 >= width)) { |
|
width = col + count; |
|
if (unlikely(!resize_horizontally(term, width))) |
|
return; |
|
} |
|
|
|
uint32_t color = term->sixel.color; |
|
for (unsigned i = 0; i < count; i++, col++) |
|
sixel_add(term, col, width, color, c); |
|
|
|
term->sixel.pos.col = 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 (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; |
|
} |
|
break; |
|
|
|
case '-': |
|
term->sixel.pos.row += 6; |
|
term->sixel.pos.col = 0; |
|
term->sixel.row_byte_ofs += term->sixel.image.width * 6; |
|
|
|
if (term->sixel.pos.row >= term->sixel.image.height) { |
|
if (!resize_vertically(term, term->sixel.pos.row + 6)) |
|
term->sixel.pos.col = term->sixel.max_width + 1; |
|
} |
|
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_many(term, c - 63, 1); |
|
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 <= term->sixel.max_height && pv <= term->sixel.max_width) |
|
{ |
|
resize(term, ph, pv); |
|
|
|
/* This ensures the sixel’s final image size is *at least* |
|
* this large */ |
|
term->sixel.max_non_empty_row_no = pv - 1; |
|
} |
|
|
|
term->sixel.state = SIXEL_DECSIXEL; |
|
decsixel(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; |
|
|
|
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 '~': { |
|
unsigned count = term->sixel.param; |
|
if (likely(count > 0)) |
|
sixel_add_many(term, c - 63, count); |
|
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]; |
|
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 */ |
|
uint8_t r = 255 * min(c1, 100) / 100; |
|
uint8_t g = 255 * min(c2, 100) / 100; |
|
uint8_t b = 255 * min(c3, 100) / 100; |
|
|
|
LOG_DBG("setting palette #%d = RGB %hhu/%hhu/%hhu", |
|
term->sixel.color_idx, r, g, b); |
|
|
|
term->sixel.palette[term->sixel.color_idx] = |
|
0xffu << 24 | r << 16 | g << 8 | b; |
|
break; |
|
} |
|
} |
|
} else |
|
term->sixel.color = term->sixel.palette[term->sixel.color_idx]; |
|
|
|
term->sixel.state = SIXEL_DECSIXEL; |
|
decsixel(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]; |
|
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); |
|
}
|
|
|