Merge image_merge into 4902-image-support

Replace SIXEL retention and the per-client cache with the global indexed-image cache from image_merge. Adapt its backend callbacks for Kitty and test palette quantization through both input protocols.
This commit is contained in:
Nicholas Marriott
2026-10-14 15:39:59 +01:00
8 changed files with 364 additions and 554 deletions

View File

@@ -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. */

View File

@@ -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);
}

17
image.c
View File

@@ -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)

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 */