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.
This commit is contained in:
Michael Grant
2026-08-03 12:14:49 +01:00
parent 01db040717
commit 7748e525a4
4 changed files with 68 additions and 31 deletions

View File

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

45
image.c
View File

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

View File

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

5
tmux.h
View File

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