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 08f0c0485b
commit 832d2beb2d
4 changed files with 66 additions and 27 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

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

View File

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

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