Support Kitty source rectangle placements

This commit is contained in:
Michael Grant
2026-08-03 12:31:11 +01:00
parent a4b8d1c822
commit cfab0590a4
4 changed files with 227 additions and 46 deletions

View File

@@ -75,6 +75,10 @@ struct kitty_state {
char compression;
u_int width;
u_int height;
u_int source_x;
u_int source_y;
u_int source_width;
u_int source_height;
u_int columns;
u_int rows;
u_int image_id;
@@ -232,7 +236,8 @@ kitty_upload(struct tty *tty, struct image *im)
struct kitty_output *ko = kitty_get_output(tty);
struct kitty_image_cache *cache;
char control[128], encoded[4097];
size_t offset, size;
u_char raw[3072];
size_t offset, size, copied, row, column, available;
int encodedlen;
u_int id;
@@ -261,9 +266,18 @@ kitty_upload(struct tty *tty, struct image *im)
for (offset = 0; offset < im->size; offset += size) {
size = im->size - offset;
if (size > 3072)
size = 3072;
encodedlen = b64_ntop(im->pixels + offset, size, encoded,
if (size > sizeof raw)
size = sizeof raw;
for (copied = 0; copied < size; copied += available) {
row = (offset + copied) / ((size_t)im->width * 4);
column = (offset + copied) % ((size_t)im->width * 4);
available = (size_t)im->width * 4 - column;
if (available > size - copied)
available = size - copied;
memcpy(raw + copied, im->pixels + row * im->stride +
column, available);
}
encodedlen = b64_ntop(raw, size, encoded,
sizeof encoded);
if (encodedlen < 0)
return (NULL);
@@ -418,6 +432,10 @@ kitty_control(struct kitty_state *ks, const u_char *buf, size_t len)
case 'f':
case 's':
case 'v':
case 'x':
case 'y':
case 'w':
case 'h':
case 'c':
case 'r':
case 'i':
@@ -431,6 +449,10 @@ kitty_control(struct kitty_state *ks, const u_char *buf, size_t len)
case 'f': ks->format = number; break;
case 's': ks->width = number; break;
case 'v': ks->height = number; break;
case 'x': ks->source_x = number; break;
case 'y': ks->source_y = number; break;
case 'w': ks->source_width = number; break;
case 'h': ks->source_height = number; break;
case 'c': ks->columns = number; break;
case 'r': ks->rows = number; break;
case 'i': ks->image_id = number; break;
@@ -594,9 +616,87 @@ kitty_raw(struct kitty_state *ks, u_char *data, size_t size)
return (pixels);
}
static struct image *
kitty_place_image(struct image *source, struct kitty_state *ks, u_int xpixel,
u_int ypixel)
{
uint64_t numerator, denominator, value;
u_int x, y, width, height, sx, sy, canvas_width;
u_int canvas_height, cell_width, cell_height;
x = ks->source_x;
y = ks->source_y;
if (x >= source->width || y >= source->height)
return (NULL);
width = ks->source_width;
if (width == 0 || width > source->width - x)
width = source->width - x;
height = ks->source_height;
if (height == 0 || height > source->height - y)
height = source->height - y;
cell_width = (xpixel == 0 ? 8 : xpixel);
cell_height = (ypixel == 0 ? 16 : ypixel);
if (ks->columns == 0 && ks->rows == 0) {
image_size_in_cells(width, height, cell_width, cell_height,
&sx, &sy);
value = (uint64_t)sx * cell_width;
if (value > UINT_MAX)
return (NULL);
canvas_width = value;
value = (uint64_t)sy * cell_height;
if (value > UINT_MAX)
return (NULL);
canvas_height = value;
} else if (ks->columns != 0 && ks->rows != 0) {
sx = ks->columns;
sy = ks->rows;
canvas_width = width;
canvas_height = height;
} else if (ks->columns != 0) {
sx = ks->columns;
numerator = (uint64_t)height * sx * cell_width;
denominator = (uint64_t)width * cell_height;
value = (numerator + denominator - 1) / denominator;
if (value == 0 || value > UINT_MAX)
return (NULL);
sy = value;
canvas_width = width;
numerator = (uint64_t)sy * cell_height * width;
denominator = (uint64_t)sx * cell_width;
value = (numerator + denominator - 1) / denominator;
if (value < height)
value = height;
if (value > UINT_MAX)
return (NULL);
canvas_height = value;
} else {
sy = ks->rows;
numerator = (uint64_t)width * sy * cell_height;
denominator = (uint64_t)height * cell_width;
value = (numerator + denominator - 1) / denominator;
if (value == 0 || value > UINT_MAX)
return (NULL);
sx = value;
canvas_height = height;
numerator = (uint64_t)sx * cell_width * height;
denominator = (uint64_t)sy * cell_height;
value = (numerator + denominator - 1) / denominator;
if (value < width)
value = width;
if (value > UINT_MAX)
return (NULL);
canvas_width = value;
}
return (image_create_view(source, x, y, width, height, canvas_width,
canvas_height, sx, sy));
}
/*
* Parse one Kitty graphics APC body (without the leading G). Only direct
* static images are accepted. The returned image owns decoded RGBA pixels.
* static images are accepted. The returned image retains immutable RGBA
* pixels for the lifetime of its placement.
*/
struct image *
kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel,
@@ -609,8 +709,9 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel,
u_char *uncompressed;
size_t controllen, payloadlen, decodedlen;
uLongf uncompressedlen;
u_int sx, sy;
struct image *im;
u_int sx, sy, cell_width, cell_height;
uint64_t canvas_width, canvas_height;
struct image *im = NULL, *source;
if (kc == NULL) {
kc = xcalloc(1, sizeof *kc);
@@ -650,9 +751,16 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel,
}
kc->transfer = NULL;
if (ks->action == 'p') {
im = kitty_source_get(kc, ks->image_id);
*status = (im == NULL ? KITTY_PARSE_MISSING :
KITTY_PARSE_OK);
source = kitty_source_get(kc, ks->image_id);
if (source == NULL) {
*status = KITTY_PARSE_MISSING;
im = NULL;
} else {
im = kitty_place_image(source, ks, xpixel, ypixel);
image_free(source->id);
if (im != NULL)
*status = KITTY_PARSE_OK;
}
kitty_state_free(ks);
return (im);
}
@@ -717,21 +825,30 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel,
if (pixels == NULL)
goto fail;
image_size_in_cells(ks->width, ks->height, xpixel, ypixel, &sx, &sy);
if (ks->columns != 0)
sx = ks->columns;
if (ks->rows != 0)
sy = ks->rows;
im = image_create(ks->width, ks->height, sx, sy, pixels);
if (im == NULL)
free(pixels);
cell_width = (xpixel == 0 ? 8 : xpixel);
cell_height = (ypixel == 0 ? 16 : ypixel);
image_size_in_cells(ks->width, ks->height, cell_width, cell_height,
&sx, &sy);
canvas_width = (uint64_t)sx * cell_width;
canvas_height = (uint64_t)sy * cell_height;
if (canvas_width > UINT_MAX || canvas_height > UINT_MAX)
source = NULL;
else
source = image_create(ks->width, ks->height, canvas_width,
canvas_height, sx, sy, pixels);
if (source == NULL)
free(pixels);
else {
*status = KITTY_PARSE_OK;
if (im != NULL && ks->action != 'q')
kitty_source_set(kc, ks->image_id, im);
if (im != NULL && (ks->action == 'q' || ks->action == 't')) {
image_free(im->id);
im = NULL;
if (ks->action != 'q')
kitty_source_set(kc, ks->image_id, source);
if (ks->action == 'T') {
im = kitty_place_image(source, ks, xpixel, ypixel);
if (im == NULL)
*status = KITTY_PARSE_ERROR;
} else
im = NULL;
image_free(source->id);
}
kitty_state_free(ks);
return (im);

88
image.c
View File

@@ -209,6 +209,38 @@ image_find(u_int id)
return (RB_FIND(images, &images, &find));
}
static struct image *
image_create1(u_int width, u_int height, u_int canvas_width,
u_int canvas_height, u_int sx, u_int sy, size_t stride, u_char *pixels)
{
struct image *im;
im = xcalloc(1, sizeof *im);
do {
if (++image_next_id == 0)
image_next_id++;
im->id = image_next_id;
} while (image_find(im->id) != NULL);
im->references = 1;
im->source_id = im->id;
im->width = width;
im->height = height;
im->canvas_width = canvas_width;
im->canvas_height = canvas_height;
im->sx = sx;
im->sy = sy;
im->stride = stride;
im->size = (size_t)width * height * 4;
im->pixels = pixels;
RB_INSERT(images, &images, im);
log_debug("%s: image %u is %ux%u pixels on %ux%u canvas, "
"%ux%u cells", __func__, im->id, width, height, canvas_width,
canvas_height, sx, sy);
return (im);
}
struct image *
image_create(u_int width, u_int height, u_int canvas_width,
u_int canvas_height, u_int sx, u_int sy, u_char *pixels)
@@ -222,29 +254,32 @@ image_create(u_int width, u_int height, u_int canvas_width,
return (NULL);
if ((uint64_t)sx * sy > SIZE_MAX / sizeof *im->cells)
return (NULL);
im = image_create1(width, height, canvas_width, canvas_height, sx, sy,
(size_t)width * 4, pixels);
return (im);
}
im = xcalloc(1, sizeof *im);
do {
if (++image_next_id == 0)
image_next_id++;
im->id = image_next_id;
} while (image_find(im->id) != NULL);
/* Create an immutable rectangular view without copying its source pixels. */
struct image *
image_create_view(struct image *source, u_int x, u_int y, u_int width,
u_int height, u_int canvas_width, u_int canvas_height, u_int sx, u_int sy)
{
struct image *im;
im->references = 1;
im->width = width;
im->height = height;
im->canvas_width = canvas_width;
im->canvas_height = canvas_height;
im->sx = sx;
im->sy = sy;
im->stride = (size_t)width * 4;
im->size = im->stride * height;
im->pixels = pixels;
if (source == NULL || x >= source->width || y >= source->height ||
width == 0 || width > source->width - x || height == 0 ||
height > source->height - y || canvas_width < width ||
canvas_height < height || sx == 0 || sy == 0)
return (NULL);
if ((uint64_t)sx * sy > SIZE_MAX / sizeof *im->cells)
return (NULL);
RB_INSERT(images, &images, im);
log_debug("%s: image %u is %ux%u pixels on %ux%u canvas, "
"%ux%u cells", __func__, im->id, width, height, canvas_width,
canvas_height, sx, sy);
im = image_create1(width, height, canvas_width, canvas_height, sx, sy,
source->stride, source->pixels + (size_t)y * source->stride +
(size_t)x * 4);
im->parent_id = source->id;
im->source_id = source->source_id;
image_ref(source->id);
return (im);
}
@@ -272,7 +307,10 @@ image_free(u_int id)
log_debug("%s: freeing image %u", __func__, id);
RB_REMOVE(images, &images, im);
free(im->pixels);
if (im->parent_id == 0)
free(im->pixels);
else
image_free(im->parent_id);
free(im->cells);
free(im);
}
@@ -413,13 +451,17 @@ image_clear(struct screen_write_ctx *ctx, u_int id)
struct screen *s = ctx->s;
struct grid *gd = s->grid;
struct grid_cell gc;
struct image *im;
u_int x, y;
for (y = 0; y < gd->hsize + gd->sy; y++) {
for (x = 0; x < gd->sx; x++) {
grid_get_cell(gd, x, y, &gc);
if ((gc.flags & GRID_FLAG_IMAGE) &&
(id == 0 || gc.image_id == id)) {
im = NULL;
if (gc.flags & GRID_FLAG_IMAGE)
im = image_find(gc.image_id);
if (im != NULL && (id == 0 || gc.image_id == id ||
im->source_id == id)) {
if (y >= gd->hsize)
image_damage_area(ctx, x, y - gd->hsize,
1, 1);

View File

@@ -96,4 +96,22 @@ sleep 1
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
grep -q '^#=' $TMP || exit 1
# A retained Kitty image may be placed repeatedly using source rectangles.
# Crop the green and white right column from a red/green/blue/white image.
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
$TMUX2 new-session -d -x 10 -y 4 "
printf '\033_Ga=t,q=2,f=32,s=2,v=2,i=10;/wAA/wD/AP8AAP///////w==\033\\'
printf '\033_Ga=p,q=2,i=10,x=1,y=0,w=1,h=2,c=1,r=2\033\\'
sleep 10" || exit 1
$TMUX2 set -g status off || exit 1
$TMUX new-session -d -x 10 -y 4 || exit 1
$TMUX set -g status off || exit 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
$TMUX capture-pane -pS0 -E1 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "*" ] || exit 1
[ "$(sed -n 2p $TMP)" = "@" ] || exit 1
exit 0

4
tmux.h
View File

@@ -1059,6 +1059,8 @@ struct image_cell {
struct image {
u_int id;
u_int references;
u_int parent_id;
u_int source_id;
u_int width;
u_int height;
u_int canvas_width;
@@ -4259,6 +4261,8 @@ char *regsub(const char *, const char *, const char *, int);
/* image.c */
struct image *image_create(u_int, u_int, u_int, u_int, u_int, u_int,
u_char *);
struct image *image_create_view(struct image *, u_int, u_int, u_int,
u_int, u_int, u_int, u_int, u_int);
struct image *image_find(u_int);
void image_ref(u_int);
void image_free(u_int);