From 7748e525a4ca896f81f5a72d61c07fc690574942 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 | 16 +++++++------- tmux.h | 5 ++++- 4 files changed, 68 insertions(+), 31 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 eb82a265f..104377b04 100644 --- a/image.c +++ b/image.c @@ -132,14 +132,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) @@ -155,7 +160,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) @@ -206,11 +210,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); @@ -227,6 +233,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; @@ -234,8 +242,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); } @@ -305,14 +314,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 82564ca4d..05eabc128 100755 --- a/regress/image-support.sh +++ b/regress/image-support.sh @@ -69,10 +69,12 @@ $TMUX copy-mode -t:4 || exit 1 $TMUX send-keys -t:4 -X history-top || exit 1 $TMUX capture-pane -pt:4 >$TMP || exit 1 -# A client without an image feature gets the brightness-based ASCII 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 '\033_Ga=T,q=2,f=32,s=2,v=2,c=2,r=2;/wAA/wD/AP8AAP///////w==\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 @@ -80,9 +82,8 @@ $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 +$TMUX capture-pane -pS0 -E0 >$TMP || exit 1 +grep -q '^#=' $TMP || exit 1 # A selection redraw uses the single-cell path. It must leave ASCII image # cells visible (graphical clients skip these cells to avoid erasing pixels). @@ -92,8 +93,7 @@ $TMUX2 send-keys -X start-of-line || exit 1 $TMUX2 send-keys -X begin-selection || exit 1 $TMUX2 send-keys -X cursor-right || exit 1 sleep 1 -$TMUX capture-pane -pS0 -E1 >$TMP || exit 1 -sed -n 1p $TMP | grep -q '^\.\*' || exit 1 -sed -n 2p $TMP | grep -q '^ @' || exit 1 +$TMUX capture-pane -pS0 -E0 >$TMP || exit 1 +grep -q '^#=' $TMP || exit 1 exit 0 diff --git a/tmux.h b/tmux.h index c553b4feb..ca2b282f1 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; @@ -4255,7 +4257,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);