From 832d2beb2de76fcc196a88eaa0294b3e4e246802 Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Mon, 3 Aug 2026 12:14:49 +0100 Subject: [PATCH] Preserve SIXEL pixel dimensions Retain the padded canonical pixel canvas separately from the image content. Scale only the populated portion of partial edge cells so an image keeps its exact raster size on the originating terminal while retaining its cell footprint. --- image-sixel.c | 33 ++++++++++++++++++++++++----- image.c | 45 +++++++++++++++++++++++++--------------- regress/image-support.sh | 10 +++++---- tmux.h | 5 ++++- 4 files changed, 66 insertions(+), 27 deletions(-) diff --git a/image-sixel.c b/image-sixel.c index e2ab71307..fd25c6e37 100644 --- a/image-sixel.c +++ b/image-sixel.c @@ -485,7 +485,13 @@ sixel_to_image(struct sixel_image *si) } } sixel_size_in_cells(si, &sx, &sy); - im = image_create(si->x, si->y, sx, sy, pixels); + if ((uint64_t)sx * si->xpixel > UINT_MAX || + (uint64_t)sy * si->ypixel > UINT_MAX) { + free(pixels); + return (NULL); + } + im = image_create(si->x, si->y, sx * si->xpixel, sy * si->ypixel, + sx, sy, pixels); if (im == NULL) free(pixels); return (im); @@ -743,12 +749,29 @@ sixel_from_image(struct image *im, u_int ox, u_int oy, u_int cells_x, u_int x, y, sx, sy, sourcex, sourcey; u_int sourcex0, sourcey0, sourcewidth, sourceheight; u_int r, g, b, colour, i; + uint64_t destination_width, destination_height; + uint64_t content_width, content_height, x0, x1, y0, y1; - if ((uint64_t)cells_x * xpixel > UINT_MAX || - (uint64_t)cells_y * ypixel > UINT_MAX) + destination_width = (uint64_t)im->sx * xpixel; + destination_height = (uint64_t)im->sy * ypixel; + if (destination_width > UINT_MAX || destination_height > UINT_MAX) return (NULL); - sx = cells_x * xpixel; - sy = cells_y * ypixel; + content_width = ((uint64_t)im->width * destination_width + + im->canvas_width - 1) / im->canvas_width; + content_height = ((uint64_t)im->height * destination_height + + im->canvas_height - 1) / im->canvas_height; + x0 = (uint64_t)ox * xpixel; + y0 = (uint64_t)oy * ypixel; + x1 = ((uint64_t)ox + cells_x) * xpixel; + y1 = ((uint64_t)oy + cells_y) * ypixel; + if (x1 > content_width) + x1 = content_width; + if (y1 > content_height) + y1 = content_height; + if (x1 <= x0 || y1 <= y0) + return (NULL); + sx = x1 - x0; + sy = y1 - y0; if (sx == 0 || sy == 0 || sx > SIXEL_WIDTH_LIMIT || sy > SIXEL_HEIGHT_LIMIT) return (NULL); diff --git a/image.c b/image.c index a87980176..7b34e7e54 100644 --- a/image.c +++ b/image.c @@ -108,14 +108,19 @@ image_sample(struct image *im, uint64_t sample_x, uint64_t sample_y, uint64_t brightness = 0, count = 0; u_int x, y, x0, x1, y0, y1; - x0 = sample_x * im->width / sample_columns; - x1 = (sample_x + 1) * im->width / sample_columns; - y0 = sample_y * im->height / sample_rows; - y1 = (sample_y + 1) * im->height / sample_rows; + x0 = sample_x * im->canvas_width / sample_columns; + x1 = ((sample_x + 1) * im->canvas_width + sample_columns - 1) / + sample_columns; + y0 = sample_y * im->canvas_height / sample_rows; + y1 = ((sample_y + 1) * im->canvas_height + sample_rows - 1) / + sample_rows; if (x1 <= x0) x1 = x0 + 1; if (y1 <= y0) y1 = y0 + 1; + count = (uint64_t)(x1 - x0) * (y1 - y0); + if (x0 >= im->width || y0 >= im->height) + return; if (x1 > im->width) x1 = im->width; if (y1 > im->height) @@ -131,7 +136,6 @@ image_sample(struct image *im, uint64_t sample_x, uint64_t sample_y, brightness += ((2126ULL * pixel[0] + 7152ULL * pixel[1] + 722ULL * pixel[2]) / 10000) * pixel[3] / 255; - count++; } } if (count == 0) @@ -182,11 +186,13 @@ image_find(u_int id) } struct image * -image_create(u_int width, u_int height, u_int sx, u_int sy, u_char *pixels) +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) { struct image *im; - if (width == 0 || height == 0 || sx == 0 || sy == 0 || pixels == NULL) + if (width == 0 || height == 0 || canvas_width < width || + canvas_height < height || sx == 0 || sy == 0 || pixels == NULL) return (NULL); if ((uint64_t)width * height * 4 > SIZE_MAX) return (NULL); @@ -203,6 +209,8 @@ image_create(u_int width, u_int height, u_int sx, u_int sy, u_char *pixels) 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; @@ -210,8 +218,9 @@ image_create(u_int width, u_int height, u_int sx, u_int sy, u_char *pixels) im->pixels = pixels; RB_INSERT(images, &images, im); - log_debug("%s: image %u is %ux%u pixels, %ux%u cells", __func__, - im->id, width, height, sx, sy); + 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); } @@ -281,14 +290,16 @@ image_get_pixel_rectangle(const struct image *im, u_int x, u_int y, if (height > im->sy - y) height = im->sy - y; - *px = (uint64_t)x * im->width / im->sx; - *py = (uint64_t)y * im->height / im->sy; - x1 = (uint64_t)(x + width) * im->width / im->sx; - y1 = (uint64_t)(y + height) * im->height / im->sy; - if (*px >= im->width) - *px = im->width - 1; - if (*py >= im->height) - *py = im->height - 1; + *px = (uint64_t)x * im->canvas_width / im->sx; + *py = (uint64_t)y * im->canvas_height / im->sy; + x1 = ((uint64_t)(x + width) * im->canvas_width + im->sx - 1) / + im->sx; + y1 = ((uint64_t)(y + height) * im->canvas_height + im->sy - 1) / + im->sy; + if (*px >= im->width || *py >= im->height) { + *px = *py = 0; + return; + } if (x1 <= *px) x1 = *px + 1; if (y1 <= *py) diff --git a/regress/image-support.sh b/regress/image-support.sh index cc897e5a7..e879a1e67 100755 --- a/regress/image-support.sh +++ b/regress/image-support.sh @@ -42,10 +42,12 @@ $TMUX new-window -d " sleep 1 [ "$($TMUX capture-pane -pt:1 -S0 -E0)" = "XY" ] || exit 1 -# A client without an image feature gets the brightness-based text backend. +# A 26-pixel raster occupies two default 16-pixel cells, but the final cell +# remains only partially filled instead of stretching the raster to 32 pixels. +# A client without an image feature exposes this in the sampled text backend. $TMUX kill-server 2>/dev/null $TMUX2 new-session -d -x 10 -y 4 " - printf '\033Pq\"1;1;8;16#0;2;100;100;100#0~~~~~~~~\044-~~~~~~~~\033\\' + printf '\033Pq\"1;1;26;26#0;2;100;100;100#0!26~-!26~-!26~-!26~-!26B\033\\' sleep 10" || exit 1 $TMUX2 set -g status off || exit 1 $TMUX new-session -d -x 10 -y 4 || exit 1 @@ -54,7 +56,7 @@ $TMUX send-keys -l "$TMUX2 attach-session" || exit 1 $TMUX send-keys Enter || exit 1 sleep 1 $TMUX capture-pane -pS0 -E0 >$TMP || exit 1 -grep -q '[.:-=+*#%@]' $TMP || exit 1 +grep -q '^#=' $TMP || exit 1 # Selection redraws must leave text image cells visible. $TMUX2 copy-mode || exit 1 @@ -63,6 +65,6 @@ $TMUX2 send-keys -X start-of-line || exit 1 $TMUX2 send-keys -X begin-selection || exit 1 sleep 1 $TMUX capture-pane -pS0 -E0 >$TMP || exit 1 -grep -q '[.:-=+*#%@]' $TMP || exit 1 +grep -q '^#=' $TMP || exit 1 exit 0 diff --git a/tmux.h b/tmux.h index 9e76fce21..12f3585a7 100644 --- a/tmux.h +++ b/tmux.h @@ -1061,6 +1061,8 @@ struct image { u_int references; u_int width; u_int height; + u_int canvas_width; + u_int canvas_height; u_int sx; u_int sy; size_t stride; @@ -4253,7 +4255,8 @@ char *regsub(const char *, const char *, const char *, int); #ifdef ENABLE_IMAGES /* image.c */ -struct image *image_create(u_int, u_int, u_int, u_int, u_char *); +struct image *image_create(u_int, u_int, u_int, u_int, u_int, u_int, + u_char *); struct image *image_find(u_int); void image_ref(u_int); void image_free(u_int);