diff --git a/image-grid.c b/image-grid.c index c604e1ca3..8e3c97f8e 100644 --- a/image-grid.c +++ b/image-grid.c @@ -136,10 +136,10 @@ static struct image_backend image_backend_sixel = { sixel_select, NULL, sixel_draw_rect, - sixel_redraw_start, - sixel_flush_output, + sixel_start_draw, + sixel_flush_pending, NULL, - sixel_free_output + sixel_free_pending }; /* Kitty backend. */ diff --git a/image-sixel.c b/image-sixel.c index c82ea12fb..9c689cebc 100644 --- a/image-sixel.c +++ b/image-sixel.c @@ -40,6 +40,9 @@ #define SIXEL_HGRAM_SIZE \ (SIXEL_HGRAM_LEVELS * SIXEL_HGRAM_LEVELS * SIXEL_HGRAM_LEVELS) +/* Maximum number of bytes in the SIXEL cache. */ +#define SIXEL_CACHE_SIZE_LIMIT IMAGE_SIZE_LIMIT + /* Line of a SIXEL image. */ struct sixel_line { /* Number of pixels in this row. */ @@ -55,11 +58,7 @@ struct sixel_image { u_int x; u_int y; - /* Visible pixel size inside a padded output canvas. */ - u_int raster_x; - u_int raster_y; - - /* Terminal cell pixel dimensions used for scaling. */ + /* Terminal cell pixel size used for decoding. */ u_int xpixel; u_int ypixel; @@ -69,12 +68,10 @@ struct sixel_image { u_int used_colours; /* SIXEL raster attributes. */ - u_int set_ra; u_int ra_x; u_int ra_y; - /* DCS parameters. */ - u_int p1; + /* DCS background parameter. */ u_int p2; /* Current parser position and colour register. */ @@ -121,29 +118,24 @@ struct sixel_pending { u_int dst_y; }; -/* An indexed image cached for one terminal cell size. */ -struct sixel_image_cache { - /* Image and terminal geometry associated with this entry. */ - u_int server_id; - u_int cell_w; - u_int cell_h; - /* Memory and age used for cache eviction. */ - size_t size; - uint64_t age; - /* Cached decoded/scaled image and next entry. */ +/* Item in the cache. */ +struct sixel_item { + u_int id; struct sixel_image *si; - struct sixel_image_cache *next; + size_t size; + + u_int cw; + u_int ch; + + RB_ENTRY(sixel_item) tree_entry; + TAILQ_ENTRY(sixel_item) entry; }; -/* Cached images and rows waiting to be sent to a terminal. */ -struct sixel_output { - /* Per-terminal cached images and aggregate cache state. */ - struct sixel_image_cache *images; - size_t size; - uint64_t age; - /* Rows waiting to be written as one SIXEL. */ - struct sixel_pending pending; -}; +RB_HEAD(sixel_tree, sixel_item); +TAILQ_HEAD(sixel_items, sixel_item); +static struct sixel_tree sixel_tree = RB_INITIALIZER(&sixel_tree); +static struct sixel_items sixel_cache = TAILQ_HEAD_INITIALIZER(sixel_cache); +static size_t sixel_cache_size; /* RGB histogram entry. */ struct sixel_hgram { @@ -196,6 +188,28 @@ struct sixel_source { u_int out_h; }; +static void sixel_free(struct sixel_image *); + +/* SIXEL cache item comparison. */ +static int +sixel_cache_cmp(struct sixel_item *a, struct sixel_item *b) +{ + if (a->id < b->id) + return (-1); + if (a->id > b->id) + return (1); + if (a->cw < b->cw) + return (-1); + if (a->cw > b->cw) + return (1); + if (a->ch < b->ch) + return (-1); + if (a->ch > b->ch) + return (1); + return (0); +} +RB_GENERATE_STATIC(sixel_tree, sixel_item, tree_entry, sixel_cache_cmp); + /* Grow a SIXEL image to contain a line. */ static int sixel_parse_expand_lines(struct sixel_image *si, u_int y) @@ -356,10 +370,6 @@ sixel_parse_attributes(struct sixel_image *si, const char *cp, const char *end) return (NULL); } - si->set_ra = 1; - si->ra_x = x; - si->ra_y = y; - return (last); } @@ -386,8 +396,6 @@ sixel_parse_colour(struct sixel_image *si, const char *cp, const char *end) log_debug("%s: too many colours", __func__); return (NULL); } - if (si->used_colours <= c) - si->used_colours = c + 1; si->dc = c + 1; if (endptr == last || *endptr != ';') return (last); @@ -486,8 +494,7 @@ sixel_parse_repeat(struct sixel_image *si, const char *cp, const char *end) /* Parse SIXEL data into an indexed image. */ struct sixel_image * -sixel_parse(const char *buf, size_t len, u_int p1, u_int p2, u_int xpixel, - u_int ypixel) +sixel_parse(const char *buf, size_t len, u_int p2, u_int xpixel, u_int ypixel) { struct sixel_image *si; const char *cp = buf, *end = buf + len; @@ -501,7 +508,6 @@ sixel_parse(const char *buf, size_t len, u_int p1, u_int p2, u_int xpixel, si = xcalloc(1, sizeof *si); si->xpixel = xpixel; si->ypixel = ypixel; - si->p1 = p1; si->p2 = p2; while (cp != end) { @@ -551,7 +557,7 @@ bad: } /* Free a SIXEL image. */ -void +static void sixel_free(struct sixel_image *si) { u_int y; @@ -575,33 +581,6 @@ sixel_size_in_cells(struct sixel_image *si, u_int *x, u_int *y) image_size_in_cells(si->x, si->y, si->xpixel, si->ypixel, x, y); } -/* Log a SIXEL image. */ -void -sixel_log(struct sixel_image *si) -{ - struct sixel_line *sl; - char s[SIXEL_WIDTH_LIMIT + 1]; - u_int i, x, y, cx, cy; - - sixel_size_in_cells(si, &cx, &cy); - log_debug("%s: image %ux%u (%ux%u)", __func__, si->x, si->y, cx, cy); - for (i = 0; i < si->ncolours; i++) - log_debug("%s: colour %u is %07x", __func__, i, si->colours[i]); - for (y = 0; y < si->y; y++) { - sl = &si->lines[y]; - for (x = 0; x < si->x; x++) { - if (x >= sl->x) - s[x] = '_'; - else if (sl->data[x] != 0) - s[x] = '0' + (sl->data[x] - 1) % 10; - else - s[x] = '.'; - } - s[x] = '\0'; - log_debug("%s: %4u: %s", __func__, y, s); - } -} - /* Convert one HLS component to RGB. */ static double sixel_hue(double p, double q, double t) @@ -654,7 +633,7 @@ sixel_colour_to_rgb(u_int c, u_char *r, u_char *g, u_char *b) } /* Convert a SIXEL image into the common RGBA format. */ -struct image * +static struct image * sixel_to_image(struct sixel_image *si) { struct image *im; @@ -694,201 +673,21 @@ sixel_to_image(struct sixel_image *si) sy, pixels); if (im == NULL) free(pixels); - if (im != NULL) - image_set_sixel(im, si); return (im); } -/* Scale or crop an indexed SIXEL image. */ -static struct sixel_image * -sixel_scale(struct sixel_image *si, u_int cell_w, u_int cell_h, u_int ox, - u_int oy, u_int sx, u_int sy, int colours) +/* Write a SIXEL image. */ +void +sixel_write(struct screen_write_ctx *ctx, struct sixel_image *si, u_int bg) { - struct sixel_image *new; - u_int cx, cy, raster_sx, raster_sy; - u_int pox, poy, psx, psy, tsx, tsy, px, py; - uint64_t source_left, source_right; - uint64_t source_top, source_bottom; - uint64_t target_left, target_right; - uint64_t target_top, target_bottom; - u_int x, y, i; + struct image *im; - /* Clip image cells before mapping them to terminal cells. */ - sixel_size_in_cells(si, &cx, &cy); - if (ox >= cx || oy >= cy) - return (NULL); - if (ox + sx >= cx) - sx = cx - ox; - if (oy + sy >= cy) - sy = cy - oy; - - if (cell_w == 0) - cell_w = si->xpixel; - if (cell_h == 0) - cell_h = si->ypixel; - raster_sx = (si->raster_x != 0 ? si->raster_x : si->x); - raster_sy = (si->raster_y != 0 ? si->raster_y : si->y); - if (raster_sx > si->x) - raster_sx = si->x; - if (raster_sy > si->y) - raster_sy = si->y; - - /* Map whole cells at pixel boundaries and clip the final cell. */ - source_left = (uint64_t)ox * si->xpixel; - source_right = (uint64_t)(ox + sx) * si->xpixel; - source_top = (uint64_t)oy * si->ypixel; - source_bottom = (uint64_t)(oy + sy) * si->ypixel; - if (source_right > raster_sx) - source_right = raster_sx; - if (source_bottom > raster_sy) - source_bottom = raster_sy; - if (source_right <= source_left || source_bottom <= source_top) - return (NULL); - pox = source_left; - poy = source_top; - psx = source_right - source_left; - psy = source_bottom - source_top; - - /* Preserve the pixel extent of a partial final cell. */ - target_right = ((uint64_t)raster_sx * cell_w + si->xpixel - 1) / - si->xpixel; - target_bottom = ((uint64_t)raster_sy * cell_h + si->ypixel - 1) / - si->ypixel; - if (target_right > UINT_MAX || target_bottom > UINT_MAX) - return (NULL); - target_left = (uint64_t)ox * cell_w; - target_top = (uint64_t)oy * cell_h; - if (target_left >= target_right || target_top >= target_bottom) - return (NULL); - if ((uint64_t)(ox + sx) * cell_w < target_right) - target_right = (uint64_t)(ox + sx) * cell_w; - if ((uint64_t)(oy + sy) * cell_h < target_bottom) - target_bottom = (uint64_t)(oy + sy) * cell_h; - tsx = target_right - target_left; - tsy = target_bottom - target_top; - if (tsx == 0 || tsy == 0) - return (NULL); - - if ((uint64_t)tsx * tsy > IMAGE_SIZE_LIMIT / 4) - return (NULL); - - /* Resample the crop and preserve its palette and raster attributes. */ - new = xcalloc(1, sizeof *si); - new->xpixel = cell_w; - new->ypixel = cell_h; - new->p1 = si->p1; - new->p2 = si->p2; - - new->set_ra = si->set_ra; - /* The raster attributes describe the scaled output rectangle. */ - new->ra_x = tsx; - new->ra_y = tsy; - - new->used_colours = si->used_colours; - for (y = 0; y < tsy; y++) { - py = poy + ((double)y * psy / tsy); - for (x = 0; x < tsx; x++) { - px = pox + ((double)x * psx / tsx); - if (sixel_set_pixel(new, x, y, sixel_get_pixel(si, px, py)) != 0) { - sixel_free(new); - return (NULL); - } - } - } - /* Keep transparent edges in the scaled raster canvas as well. */ - if (sixel_parse_expand_lines(new, tsy) != 0) { - sixel_free(new); - return (NULL); - } - new->x = tsx; - - if (colours && si->ncolours != 0) { - new->colours = xmalloc(si->ncolours * sizeof *new->colours); - for (i = 0; i < si->ncolours; i++) - new->colours[i] = si->colours[i]; - new->ncolours = si->ncolours; - } - return (new); -} - -/* Fit an indexed SIXEL image into a terminal cell canvas. */ -static struct sixel_image * -sixel_fit(struct sixel_image *si, u_int cell_w, u_int cell_h, u_int cells_x, - u_int cells_y) -{ - struct sixel_image *new; - uint64_t canvas_width, canvas_height; - u_int width, height, x, y, px, py, i; - - if (si->xpixel == 0) - si->xpixel = 8; - if (si->ypixel == 0) - si->ypixel = 16; - if (cell_w == 0) - cell_w = si->xpixel; - if (cell_h == 0) - cell_h = si->ypixel; - - canvas_width = (uint64_t)cells_x * cell_w; - canvas_height = (uint64_t)cells_y * cell_h; - if (si->x == 0 || si->y == 0 || canvas_width == 0 || - canvas_height == 0 || canvas_width > SIXEL_WIDTH_LIMIT || - canvas_height > SIXEL_HEIGHT_LIMIT) { - return (NULL); - } - - /* Keep the same cell footprint as the RGBA output path. */ - width = ((uint64_t)si->x * cell_w + si->xpixel - 1) / si->xpixel; - height = ((uint64_t)si->y * cell_h + si->ypixel - 1) / si->ypixel; - if (width == 0) - width = 1; - if (height == 0) - height = 1; - if (width > canvas_width) - width = canvas_width; - if (height > canvas_height) - height = canvas_height; - - if (canvas_width * canvas_height > IMAGE_SIZE_LIMIT / 4) - return (NULL); - - /* Resample the raster and pad the unused cell canvas. */ - new = xcalloc(1, sizeof *new); - new->xpixel = cell_w; - new->ypixel = cell_h; - new->p1 = si->p1; - new->p2 = si->p2; - new->set_ra = 1; - new->ra_x = width; - new->ra_y = height; - new->raster_x = width; - new->raster_y = height; - new->used_colours = si->used_colours; - - for (y = 0; y < height; y++) { - py = (uint64_t)y * si->y / height; - for (x = 0; x < width; x++) { - px = (uint64_t)x * si->x / width; - if (sixel_set_pixel(new, x, y, sixel_get_pixel(si, px, py)) != 0) { - sixel_free(new); - return (NULL); - } - } - } - /* Keep the unused part of the cell canvas as blank padding. */ - if (sixel_parse_expand_lines(new, canvas_height) != 0) { - sixel_free(new); - return (NULL); - } - new->x = canvas_width; - - if (si->ncolours != 0) { - new->colours = xmalloc(si->ncolours * sizeof *new->colours); - for (i = 0; i < si->ncolours; i++) - new->colours[i] = si->colours[i]; - new->ncolours = si->ncolours; - } - return (new); + im = sixel_to_image(si); + sixel_free(si); + if (im == NULL) + return; + image_write_sixel(ctx, im, bg); + image_free(im); } /* Append data to a growing SIXEL output buffer. */ @@ -999,26 +798,19 @@ sixel_print(struct sixel_image *si, size_t *size) len = 8192; buf = xmalloc(len); - tmplen = xsnprintf(tmp, sizeof tmp, "\033P%u;%uq", si->p1, si->p2); - sixel_print_add(&buf, &len, &used, tmp, tmplen); + sixel_print_add(&buf, &len, &used, "\033P9;1q", 6); - if (si->set_ra) { - tmplen = xsnprintf(tmp, sizeof tmp, "\"1;1;%u;%u", si->ra_x, - si->ra_y); - sixel_print_add(&buf, &len, &used, tmp, tmplen); - } + tmplen = xsnprintf(tmp, sizeof tmp, "\"1;1;%u;%u", si->ra_x, + si->ra_y); + sixel_print_add(&buf, &len, &used, tmp, tmplen); /* Track colour planes containing pixels in the current six-row band. */ planes = xcalloc(used_colours, sizeof *planes); active = xcalloc(used_colours, sizeof *active); /* Emit the palette before the encoded colour planes. */ - for (i = 0; i < used_colours; i++) { - c = 0; - if (i < si->ncolours) - c = si->colours[i]; - if (c >> 25 == 0) - c = 2U << 25; + for (i = 0; i < si->ncolours; i++) { + c = si->colours[i]; tmplen = xsnprintf(tmp, sizeof tmp, "#%u;%u;%u;%u;%u", i, c >> 25, (c >> 16) & 0x1ff, (c >> 8) & 0xff, c & 0xff); sixel_print_add(&buf, &len, &used, tmp, tmplen); @@ -1439,11 +1231,6 @@ sixel_make_image(struct image *im, u_int cx, u_int cy, u_int csx, u_int csy, /* Create the indexed image with a SIXEL RGB palette. */ si = xcalloc(1, sizeof *si); - si->xpixel = cw; - si->ypixel = ch; - si->p1 = 9; - si->p2 = 1; - si->set_ra = 1; si->ra_x = sx; si->ra_y = sy; si->ncolours = si->used_colours = nc; @@ -1490,6 +1277,151 @@ fail: return (NULL); } +/* Return the number of bytes allocated for an indexed SIXEL image. */ +static size_t +sixel_image_size(struct sixel_image *si) +{ + return (sizeof *si + si->y * sizeof *si->lines + si->allocated + + si->ncolours * sizeof *si->colours); +} + +/* Remove an image from the global SIXEL output cache. */ +static void +sixel_remove_cache(struct sixel_item *cache) +{ + RB_REMOVE(sixel_tree, &sixel_tree, cache); + TAILQ_REMOVE(&sixel_cache, cache, entry); + sixel_cache_size -= cache->size; + sixel_free(cache->si); + free(cache); +} + +/* Drop SIXEL cache entries whose source images have gone away. */ +static void +sixel_collect_images(void) +{ + struct sixel_item *cache, *next; + + TAILQ_FOREACH_SAFE(cache, &sixel_cache, entry, next) { + if (image_find(cache->id) == NULL) + sixel_remove_cache(cache); + } +} + +/* Get a cached image at the given cell size. */ +static struct sixel_image * +sixel_get_image(struct image *im, u_int cw, u_int ch) +{ + struct sixel_image *si; + size_t size; + u_int id = image_get_id(im), sx, sy; + struct sixel_item *cache, find = { .id = id, .cw = cw, .ch = ch }; + + cache = RB_FIND(sixel_tree, &sixel_tree, &find); + if (cache != NULL) { + TAILQ_REMOVE(&sixel_cache, cache, entry); + TAILQ_INSERT_TAIL(&sixel_cache, cache, entry); + return (cache->si); + } + image_get_size_in_cells(im, &sx, &sy); + + si = sixel_make_image(im, 0, 0, sx, sy, cw, ch); + if (si == NULL) + return (NULL); + + size = (sizeof *cache) + sixel_image_size(si); + if (size > SIXEL_CACHE_SIZE_LIMIT) { + sixel_free(si); + return (NULL); + } + if (sixel_cache_size > SIXEL_CACHE_SIZE_LIMIT - size) + sixel_collect_images(); + while (sixel_cache_size > SIXEL_CACHE_SIZE_LIMIT - size) + sixel_remove_cache(TAILQ_FIRST(&sixel_cache)); + + cache = xcalloc(1, sizeof *cache); + cache->id = id; + cache->cw = cw; + cache->ch = ch; + cache->size = size; + cache->si = si; + RB_INSERT(sixel_tree, &sixel_tree, cache); + TAILQ_INSERT_TAIL(&sixel_cache, cache, entry); + + sixel_cache_size += size; + return (si); +} + +/* Copy a rectangle from a SIXEL image. */ +static struct sixel_image * +sixel_crop(struct sixel_image *si, u_int cx, u_int cy, u_int csx, u_int csy, + u_int cw, u_int ch) +{ + struct sixel_image *new; + struct sixel_line *src, *dst; + uint64_t left, right, top, bottom; + u_int x, y, w, h, i; + + left = (uint64_t)cx * cw; + top = (uint64_t)cy * ch; + right = ((uint64_t)cx + csx) * cw; + bottom = ((uint64_t)cy + csy) * ch; + if (right > si->ra_x) + right = si->ra_x; + if (bottom > si->ra_y) + bottom = si->ra_y; + if (right <= left || bottom <= top) + return (NULL); + x = left; + y = top; + w = right - left; + h = bottom - top; + + new = xcalloc(1, sizeof *new); + new->x = new->ra_x = w; + new->y = new->ra_y = h; + new->lines = xcalloc(h, sizeof *new->lines); + for (i = 0; i < h; i++) { + if (y + i >= si->y) + break; + src = &si->lines[y + i]; + if (x >= src->x) + continue; + dst = &new->lines[i]; + dst->x = src->x - x; + if (dst->x > w) + dst->x = w; + dst->data = xmalloc(dst->x * sizeof *dst->data); + memcpy(dst->data, src->data + x, dst->x * sizeof *dst->data); + new->allocated += dst->x * sizeof *dst->data; + } + new->used_colours = si->used_colours; + new->ncolours = si->ncolours; + new->colours = xmalloc(new->ncolours * sizeof *new->colours); + memcpy(new->colours, si->colours, new->ncolours * sizeof *new->colours); + return (new); +} + +/* Encode part of an image as SIXEL. */ +static char * +sixel_from_image(struct image *im, u_int x, u_int y, u_int sx, u_int sy, + u_int cw, u_int ch, size_t *size) +{ + struct sixel_image *si, *cached; + char *data; + + cached = sixel_get_image(im, cw, ch); + if (cached != NULL) + si = sixel_crop(cached, x, y, sx, sy, cw, ch); + else + si = sixel_make_image(im, x, y, sx, sy, cw, ch); + if (si == NULL) + return (NULL); + data = sixel_print(si, size); + sixel_free(si); + return (data); +} + /* Return whether the terminal supports SIXEL. */ int sixel_select(struct tty *tty) @@ -1513,248 +1445,94 @@ sixel_get_flags(struct tty *tty) return (flags); } -/* Return the SIXEL output cache for a terminal. */ -static struct sixel_output * -sixel_get_output(struct tty *tty) +/* Get pending SIXEL rows for a terminal. */ +static struct sixel_pending * +sixel_get_pending(struct tty *tty) { - struct sixel_output *so = tty->image_data; + struct sixel_pending *sp = tty->image_data; - if (so == NULL) { - so = xcalloc(1, sizeof *so); - tty->image_data = so; + if (sp == NULL) { + sp = xcalloc(1, sizeof *sp); + tty->image_data = sp; } - return (so); + return (sp); } -/* Return the memory used by an indexed SIXEL image. */ -static size_t -sixel_image_size(struct sixel_image *si) -{ - uint64_t size; - - if ((uint64_t)si->x * si->y > SIZE_MAX / sizeof(uint16_t)) - return (0); - size = (uint64_t)si->x * si->y * sizeof(uint16_t); - if ((uint64_t)si->ncolours * sizeof *si->colours > SIZE_MAX - size) - return (0); - size += (uint64_t)si->ncolours * sizeof *si->colours; - if (size > SIZE_MAX) - return (0); - return (size); -} - -/* Remove an image from the SIXEL output cache. */ -static void -sixel_remove_cache(struct sixel_output *so, struct sixel_image_cache **pp) -{ - struct sixel_image_cache *cache = *pp; - - *pp = cache->next; - so->size -= cache->size; - sixel_free(cache->si); - free(cache); -} - -/* Drop SIXEL cache entries whose source images have gone away. */ -static void -sixel_collect_images(struct sixel_output *so) -{ - struct sixel_image_cache **pp, *cache; - - for (pp = &so->images; (cache = *pp) != NULL; ) { - if (image_find(cache->server_id) == NULL) - sixel_remove_cache(so, pp); - else - pp = &cache->next; - } -} - -/* Free SIXEL output state for a terminal. */ +/* Free pending SIXEL rows for a terminal. */ void -sixel_free_output(struct tty *tty, __unused int send) +sixel_free_pending(struct tty *tty, __unused int send) { - struct sixel_output *so = tty->image_data; - struct sixel_image_cache *cache, *next; + struct sixel_pending *sp = tty->image_data; - if (so == NULL) + if (sp == NULL) return; - /* Discard pending rows measured at the old terminal geometry. */ - so->pending.image = NULL; - for (cache = so->images; cache != NULL; cache = next) { - next = cache->next; - sixel_free(cache->si); - free(cache); - } - if (so->pending.image != NULL) - image_free(so->pending.image); - free(so); + if (sp->image != NULL) + image_free(sp->image); + free(sp); tty->image_data = NULL; } -/* Render an image at a terminal's current pixel geometry. */ -static struct sixel_image * -sixel_render_image(struct image *im, u_int cell_w, u_int cell_h) -{ - struct sixel_image *original, *si; - u_int sx, sy; - - image_get_size_in_cells(im, &sx, &sy); - /* Preserve the original SIXEL palette and pixels when possible. */ - original = image_get_sixel(im); - if (original != NULL) - si = sixel_fit(original, cell_w, cell_h, sx, sy); - else - si = sixel_make_image(im, 0, 0, sx, sy, cell_w, cell_h); - return (si); -} - -/* Return a rendered image from the SIXEL output cache. */ -static struct sixel_image * -sixel_get_image(struct tty *tty, struct image *im) -{ - struct sixel_output *so = sixel_get_output(tty); - struct sixel_image_cache **pp, *cache, **oldest; - struct sixel_image *si; - size_t size; - - sixel_collect_images(so); - for (cache = so->images; cache != NULL; cache = cache->next) { - if (cache->server_id != image_get_id(im) || - cache->cell_w != tty->xpixel || - cache->cell_h != tty->ypixel) { - continue; - } - cache->age = ++so->age; - return (cache->si); - } - - si = sixel_render_image(im, tty->xpixel, tty->ypixel); - if (si == NULL) - return (NULL); - size = sixel_image_size(si); - if (size == 0 || size > IMAGE_SIZE_LIMIT) { - /* Use the rendered image without caching it. */ - return (si); - } - while (so->size > IMAGE_SIZE_LIMIT - size) { - oldest = NULL; - for (pp = &so->images; (cache = *pp) != NULL; - pp = &cache->next) { - if (oldest == NULL || cache->age < (*oldest)->age) - oldest = pp; - } - if (oldest == NULL) - break; - sixel_remove_cache(so, oldest); - } - cache = xcalloc(1, sizeof *cache); - cache->server_id = image_get_id(im); - cache->cell_w = tty->xpixel; - cache->cell_h = tty->ypixel; - cache->size = size; - cache->age = ++so->age; - cache->si = si; - cache->next = so->images; - so->images = cache; - so->size += size; - return (si); -} - -/* Return if a SIXEL image is held by the output cache. */ -static int -sixel_image_is_cached(struct tty *tty, struct sixel_image *si) -{ - struct sixel_output *so = tty->image_data; - struct sixel_image_cache *cache; - - if (so == NULL) - return (0); - for (cache = so->images; cache != NULL; cache = cache->next) { - if (cache->si == si) - return (1); - } - return (0); -} - /* Write the rows held in the pending run as a single SIXEL. */ void -sixel_flush_output(struct tty *tty) +sixel_flush_pending(struct tty *tty) { - struct sixel_output *so = tty->image_data; - struct sixel_pending *sp; - struct sixel_image *si, *crop; + struct sixel_pending *sp = tty->image_data; struct image *im; char *data; size_t size; - if (so == NULL || so->pending.image == NULL) + if (sp == NULL || sp->image == NULL) return; - sp = &so->pending; - im = sp->image; - si = sixel_get_image(tty, im); sp->image = NULL; + + data = sixel_from_image(im, sp->src_x, sp->src_y, sp->w, sp->h, + tty->xpixel, tty->ypixel, &size); + if (data != NULL) { + tty_region_off(tty); + tty_margin_off(tty); + tty_cursor(tty, sp->dst_x, sp->dst_y); + + tty->flags |= TTY_NOBLOCK; + tty_putn(tty, data, size, 0); + tty->cx = tty->cy = UINT_MAX; + + free(data); + } image_free(im); - if (si == NULL) - return; - crop = sixel_scale(si, tty->xpixel, tty->ypixel, sp->src_x, - sp->src_y, sp->w, sp->h, 1); - if (!sixel_image_is_cached(tty, si)) - sixel_free(si); - if (crop == NULL) - return; - data = sixel_print(crop, &size); - sixel_free(crop); - if (data == NULL) - return; - tty_region_off(tty); - tty_margin_off(tty); - tty_cursor(tty, sp->dst_x, sp->dst_y); - tty->flags |= TTY_NOBLOCK; - tty_putn(tty, data, size, 0); - /* SIXEL moves the cursor, but does not change terminal attributes. */ - tty->cx = tty->cy = UINT_MAX; - free(data); } /* Queue adjacent placement rows for one SIXEL output sequence. */ void -sixel_draw_rect(struct tty *tty, const struct image_rect *rectangle) +sixel_draw_rect(struct tty *tty, const struct image_rect *rect) { - struct sixel_output *so = sixel_get_output(tty); - struct sixel_pending *sp = &so->pending; - struct image *im = image_rect_get_image(rectangle); - u_int source_x, source_y, width, height; - u_int destination_x, destination_y; + struct sixel_pending *sp = sixel_get_pending(tty); - image_rect_get_coords(rectangle, &source_x, &source_y, &width, - &height, &destination_x, &destination_y); - - if (sp->image == im && - sp->src_x == source_x && - sp->w == width && - sp->dst_x == destination_x && - sp->src_y + sp->h == source_y && - sp->dst_y + sp->h == destination_y) { - sp->h += height; + if (sp->image == rect->image && + sp->src_x == rect->src_x && + sp->w == rect->sx && + sp->dst_x == rect->dst_x && + sp->src_y + sp->h == rect->src_y && + sp->dst_y + sp->h == rect->dst_y) { + sp->h += rect->sy; return; } - /* Flush a completed run before queuing a different rectangle. */ - image_ref(im); - sixel_flush_output(tty); - sp->image = im; - sp->src_x = source_x; - sp->src_y = source_y; - sp->w = width; - sp->h = height; - sp->dst_x = destination_x; - sp->dst_y = destination_y; + image_ref(rect->image); + sixel_flush_pending(tty); + + sp->image = rect->image; + sp->src_x = rect->src_x; + sp->src_y = rect->src_y; + sp->w = rect->sx; + sp->h = rect->sy; + sp->dst_x = rect->dst_x; + sp->dst_y = rect->dst_y; } -/* Remove old SIXEL pixels before replaying a dirty image area. */ +/* Remove old SIXEL pixels before drawing an image area. */ void -sixel_redraw_start(struct tty *tty, u_int x, u_int y, u_int sx, u_int sy) +sixel_start_draw(struct tty *tty, u_int x, u_int y, u_int sx, u_int sy) { u_int yy; @@ -1766,18 +1544,3 @@ sixel_redraw_start(struct tty *tty, u_int x, u_int y, u_int sx, u_int sy) tty_repeat_space(tty, sx); } } - -/* Write a SIXEL image and release the parsed data. */ -void -sixel_write(struct screen_write_ctx *ctx, struct sixel_image *si, u_int bg) -{ - struct image *im; - - im = sixel_to_image(si); - if (im == NULL) { - sixel_free(si); - return; - } - image_write_sixel(ctx, im, bg); - image_free(im); -} diff --git a/image.c b/image.c index 6e93c42fb..07ad75709 100644 --- a/image.c +++ b/image.c @@ -60,7 +60,6 @@ struct image { size_t stride; size_t size; u_char *pixels; - struct sixel_image *sixel; RB_ENTRY(image) entry; }; @@ -255,8 +254,6 @@ image_free(struct image *im) free(im->pixels); if (im->parent != NULL) image_free(im->parent); - if (im->sixel != NULL) - sixel_free(im->sixel); free(im); } } @@ -362,20 +359,6 @@ image_has_no_cursor(struct image *im) return (!!(im->flags & IMAGE_FLAG_NO_CURSOR)); } -/* Return an image's original SIXEL data. */ -struct sixel_image * -image_get_sixel(struct image *im) -{ - return (im->sixel); -} - -/* Associate original SIXEL data with an image. */ -void -image_set_sixel(struct image *im, struct sixel_image *si) -{ - im->sixel = si; -} - /* Return the original source image ID. */ u_int image_get_source_id(struct image *im) diff --git a/input.c b/input.c index 1d0458c4e..737ef6652 100644 --- a/input.c +++ b/input.c @@ -2665,7 +2665,7 @@ input_dcs_dispatch(struct input_ctx *ictx) #ifdef ENABLE_IMAGES struct window *w; struct sixel_image *si; - int p1, p2; + int p2; #endif if (wp == NULL) @@ -2683,13 +2683,10 @@ input_dcs_dispatch(struct input_ctx *ictx) w = wp->window; if (input_split(ictx) != 0) return (0); - p1 = input_get(ictx, 0, 0, 0); - if (p1 == -1) - p1 = 0; p2 = input_get(ictx, 1, 0, 0); if (p2 == -1) p2 = 0; - si = sixel_parse(buf, len, p1, p2, w->xpixel, w->ypixel); + si = sixel_parse(buf, len, p2, w->xpixel, w->ypixel); if (si != NULL) sixel_write(sctx, si, ictx->cell.cell.bg); } diff --git a/regress/image-sixel-cache.sh b/regress/image-sixel-cache.sh new file mode 100644 index 000000000..6eb8a3123 --- /dev/null +++ b/regress/image-sixel-cache.sh @@ -0,0 +1,78 @@ +#!/bin/sh + +# Check that cached colours and dithering survive clipping and client changes, +# and that a visible crop is drawn when the whole scaled image is too large. + +PATH=/bin:/usr/bin +TERM=screen +export TERM +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +python3 -B - "$TEST_TMUX" <<'PY' +import sys +from image_test import image_test, sixel, check_image + +levels = [0, 14, 29, 43, 57, 71, 86, 100] +palette = [(r, g, b) for r in levels for g in levels for b in levels] +wire = sixel(32, 16, palette, list(range(512))) + +with image_test(sys.argv[1]) as test: + first = test.attach() + test.emit(wire) + original = first.images() + assert len(original) == 1 and original[0][:2] == (32, 16) + + # Clients with the same cell size draw identical indexed pixels. + second = test.attach() + test.redraw() + assert first.images() == second.images() == original + + # A different cell size must not replace the first clients' result. + small = test.attach(cw=4, ch=8) + test.redraw() + scaled = small.images() + assert len(scaled) == 1 and scaled[0][:2] == (16, 8) + assert first.images() == second.images() == original + + # Overwrite the first cell. The remaining pixels retain the full palette + # and dithering, rather than converting the smaller rectangle again. + test.emit(b'\033[HX') + expected = [original[0][2][y * 32 + x] + for y in range(16) for x in range(8, 32)] + for client in (first, second): + images = client.images() + assert len(images) == 1 + check_image(images[0], 24, 16, expected) + small_expected = [scaled[0][2][y * 16 + x] + for y in range(8) for x in range(4, 16)] + images = small.images() + assert len(images) == 1 + check_image(images[0], 12, 8, small_expected) + + # Changing back to an earlier cell size reuses the same indexed pixels. + test.resize(small, 40, 16) + test.redraw() + for client in (first, second, small): + images = client.images() + assert len(images) == 1 + check_image(images[0], 24, 16, expected) + + # Replacing the source must not draw pixels from the deleted image. + test.emit(b'\033[2J\033[H' + + sixel(8, 16, [(100, 0, 0)], [0] * (8 * 16))) + for client in (first, second, small): + images = client.images() + assert len(images) == 1 + check_image(images[0], 8, 16, [(255, 0, 0)] * (8 * 16)) + +with image_test(sys.argv[1], sx=1) as test: + client = test.attach() + test.emit(sixel(16, 16, [(100, 0, 0)], [0] * (16 * 16))) + # The whole image would be 12000 pixels wide, beyond the SIXEL limit. + # Its visible first cell is still valid and must be converted directly. + test.resize(client, 1, 16, cw=6000, ch=16) + test.redraw() + images = client.images() + assert len(images) == 1 + check_image(images[0], 6000, 16, [(255, 0, 0)] * (6000 * 16)) +PY diff --git a/regress/image-sixel-palette.sh b/regress/image-sixel-palette.sh index 9f529892e..167295da4 100755 --- a/regress/image-sixel-palette.sh +++ b/regress/image-sixel-palette.sh @@ -18,28 +18,23 @@ palette = [(r, g, b) for r in levels for g in levels for b in levels] expected = [tuple((c * 255 + 50) // 100 for c in p) for p in palette] with image_test(sys.argv[1]) as test: client = test.attach() - test.emit(sixel(32, 16, palette, list(range(512)))) - images = client.images() - assert len(images) == 1 and images[0][:2] == (32, 16) - assert images[0][2] == expected, 'original SIXEL palette changed' - test.redraw() - assert client.images() == images, 'retained palette changed on redraw' - - # Kitty input exercises RGBA quantization rather than SIXEL retention. rgba = bytes(c for pixel in expected for c in (*pixel, 255)) - test.emit(b'\033[2J\033[H\033_Ga=T,q=2,f=32,s=32,v=16;' + + kitty = (b'\033_Ga=T,q=2,f=32,s=32,v=16;' + base64.b64encode(rgba) + b'\033\\') - images = client.images() - assert len(images) == 1 and images[0][:2] == (32, 16) - actual = images[0][2] - assert all(c is not None for c in actual), 'dithering introduced holes' - assert 128 <= len(set(actual)) <= 256, len(set(actual)) - errors = [abs(a - e) for p, q in zip(actual, expected) for a, e in zip(p, q)] - assert max(errors) <= 80, max(errors) - assert sum(errors) / len(errors) <= 16, sum(errors) / len(errors) - assert max(actual[0]) <= 30 and min(actual[-1]) >= 225, ( - 'black/white extremes lost', actual[0], actual[-1]) - for _ in range(3): - test.redraw() - assert client.images() == images, 'palette or dithering changed on redraw' + # Both input protocols use the shared RGBA model and quantization. + for wire in (sixel(32, 16, palette, list(range(512))), kitty): + test.emit(b'\033[2J\033[H' + wire) + images = client.images() + assert len(images) == 1 and images[0][:2] == (32, 16) + actual = images[0][2] + assert all(c is not None for c in actual), 'dithering introduced holes' + assert 128 <= len(set(actual)) <= 256, len(set(actual)) + errors = [abs(a - e) for p, q in zip(actual, expected) for a, e in zip(p, q)] + assert max(errors) <= 80, max(errors) + assert sum(errors) / len(errors) <= 16, sum(errors) / len(errors) + assert max(actual[0]) <= 30 and min(actual[-1]) >= 225, ( + 'black/white extremes lost', actual[0], actual[-1]) + for _ in range(3): + test.redraw() + assert client.images() == images, 'palette or dithering changed on redraw' PY diff --git a/regress/image-sixel-parse.sh b/regress/image-sixel-parse.sh index db8b0919b..dc63b17a4 100755 --- a/regress/image-sixel-parse.sh +++ b/regress/image-sixel-parse.sh @@ -31,7 +31,7 @@ with image_test(sys.argv[1]) as test: assert len(client.images()) == 1 check_image(client.images()[0], 8, 16, [rgb] * (8 * 16)) - # Undefined registers are black in the shared model and in retained output. + # Undefined registers are black in the shared image model. test.emit(b'\033[2J\033[H\033P9;1q"1;1;8;16' b'#5!8~-!8~-!8N\033\\') assert len(client.images()) == 1 diff --git a/tmux-image.h b/tmux-image.h index c13a7a4df..c2cf92efd 100644 --- a/tmux-image.h +++ b/tmux-image.h @@ -132,8 +132,6 @@ void image_size_in_cells(u_int, u_int, u_int, u_int, u_int *, u_int *); void image_set_no_cursor(struct image *); int image_has_no_cursor(struct image *); -struct sixel_image *image_get_sixel(struct image *); -void image_set_sixel(struct image *, struct sixel_image *); u_int image_get_source_id(struct image *); u_int image_get_order_id(struct image *); void image_set_order_id(struct image *, u_int); @@ -222,17 +220,13 @@ void kitty_free_output_state(struct tty *, int); #define SIXEL_COLOUR_REGISTERS 1024 int sixel_select(struct tty *); int sixel_get_flags(struct tty *); -struct sixel_image *sixel_parse(const char *, size_t, u_int, u_int, u_int, - u_int); +struct sixel_image *sixel_parse(const char *, size_t, u_int, u_int, u_int); void sixel_write(struct screen_write_ctx *, struct sixel_image *, u_int); void sixel_draw_rect(struct tty *, const struct image_rect *); -void sixel_redraw_start(struct tty *, u_int, u_int, u_int, u_int); -void sixel_free_output(struct tty *, int); -void sixel_flush_output(struct tty *); -struct image *sixel_to_image(struct sixel_image *); -void sixel_free(struct sixel_image *); -void sixel_log(struct sixel_image *); +void sixel_start_draw(struct tty *, u_int, u_int, u_int, u_int); +void sixel_free_pending(struct tty *, int); +void sixel_flush_pending(struct tty *); #endif /* ENABLE_IMAGES */