Merge image_merge into 4902-image-support

Keep retained indexed SIXEL data and the bounded per-terminal output cache.
Use the cleaned-up decoder and RGBA encoder from image_merge, including
parser memory limits, pixel-size handling and references for pending output.
Match retained SIXEL scaling to the shared image cell footprint and emit
black definitions for undefined colour registers.

Keep Kitty input, output, layers and placeholders. Adapt backend selection,
flags, drawing and cleanup to the callbacks and Iscrl capability. Keep the
pane-status repaint with the requested XXX comment.

Replace the elaborate fallback with image_merge's implementation, removing
its caches, sample structures, quadrant/sextant features and ACS additions.
Keep the image_merge regressions and adapt the Kitty tests to the simpler
fallback. Use skip status 101 and disable Python bytecode for image tests.

Small changes still outside image_merge:
- Guard zero-height synchronized redraws before bit_nset.
- Select the image backend when the tty opens.
- Enable imagescroll by default for Kitty and Ghostty.
- Keep the explanatory manual-window pixel-size comment (comment only).
- Keep the additional SIXEL fixtures and visual test tools.

Validation: image-enabled and disabled builds; 31 image and 13 related
regressions pass normally and under ASAN. All image tests skip without
images. Focused palette, parser, scaling, transparency and Kitty layer
tests also pass after the retained-output adjustments. Diff checks clean.
This commit is contained in:
Nicholas Marriott
2026-10-14 14:28:22 +01:00
57 changed files with 2722 additions and 2295 deletions

205
image.c
View File

@@ -61,8 +61,6 @@ struct image {
size_t size;
u_char *pixels;
struct sixel_image *sixel;
struct image_cell *cells;
void *fallback_data;
RB_ENTRY(image) entry;
};
@@ -92,7 +90,7 @@ image_find(u_int id)
return (RB_FIND(images, &images, &find));
}
/* Return the ID of an image. */
/* Get the ID of an image. */
u_int
image_get_id(struct image *im)
{
@@ -145,7 +143,7 @@ image_create(u_int w, u_int h, u_int cw, u_int ch, u_int sx, u_int sy,
return (NULL);
if (sx > USHRT_MAX || sy > USHRT_MAX)
return (NULL);
if ((uint64_t)sx * sy > IMAGE_SIZE_LIMIT / sizeof(struct image_cell))
if ((uint64_t)sx * sy > IMAGE_SIZE_LIMIT)
return (NULL);
im = image_alloc(w, h, cw, ch, sx, sy, w * 4, pixels);
@@ -204,8 +202,7 @@ image_create_view(struct image *src, struct image_view *view)
view->height > src->height - view->y ||
view->sx > USHRT_MAX ||
view->sy > USHRT_MAX ||
(uint64_t)view->sx * view->sy >
IMAGE_SIZE_LIMIT / sizeof(struct image_cell) ||
(uint64_t)view->sx * view->sy > IMAGE_SIZE_LIMIT ||
view->canvas_xoff > view->canvas_width ||
view->scaled_width > view->canvas_width - view->canvas_xoff ||
view->canvas_yoff > view->canvas_height ||
@@ -260,13 +257,11 @@ image_free(struct image *im)
image_free(im->parent);
if (im->sixel != NULL)
sixel_free(im->sixel);
fallback_free(im);
free(im->cells);
free(im);
}
}
/* Return image size in pixels. */
/* Get image size in pixels. */
void
image_get_size(struct image *im, u_int *w, u_int *h)
{
@@ -274,7 +269,7 @@ image_get_size(struct image *im, u_int *w, u_int *h)
*h = im->height;
}
/* Return canvas size in pixels. */
/* Get canvas size in pixels. */
void
image_get_canvas_size(struct image *im, u_int *w, u_int *h)
{
@@ -282,7 +277,7 @@ image_get_canvas_size(struct image *im, u_int *w, u_int *h)
*h = im->canvas_height;
}
/* Return image size in cells. */
/* Get image size in cells. */
void
image_get_size_in_cells(struct image *im, u_int *sx, u_int *sy)
{
@@ -290,7 +285,7 @@ image_get_size_in_cells(struct image *im, u_int *sx, u_int *sy)
*sy = im->sy;
}
/* Return image pixels. */
/* Get image pixels. */
const u_char *
image_get_pixels(struct image *im, size_t *stride, size_t *size)
{
@@ -314,85 +309,42 @@ image_size_in_cells(u_int w, u_int h, u_int xpixel, u_int ypixel, u_int *sx,
*sy = h / ypixel + (h % ypixel != 0);
}
/* Average a rectangle of image pixels into one fallback sample. */
static void
image_sample(struct image *im, uint64_t sample_x, uint64_t sample_y,
uint64_t sample_columns, uint64_t sample_rows, struct image_sample *sample)
/* Find area covered by a rectangle of cells in pixels. */
void
image_get_pixel_area(struct image *im, u_int x, u_int y, u_int w, u_int h,
u_int *px, u_int *py, u_int *pw, u_int *ph)
{
const u_char *pixel;
uint64_t red = 0, green = 0, blue = 0, alpha = 0;
uint64_t brightness = 0, count = 0;
u_int x, y, x0, x1, y0, y1;
u_int left, top, right, bottom;
/* Map sample bounds to pixels, counting transparent canvas padding. */
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)
*px = *py = *pw = *ph = 0;
if (im == NULL || x >= im->sx || y >= im->sy || w == 0 || h == 0)
return;
if (x1 > im->width)
x1 = im->width;
if (y1 > im->height)
y1 = im->height;
if (w > im->sx - x)
w = im->sx - x;
if (h > im->sy - y)
h = im->sy - y;
/* Average premultiplied colour and brightness over the sample area. */
for (y = y0; y < y1; y++) {
for (x = x0; x < x1; x++) {
pixel = im->pixels + y * im->stride + x * 4;
red += pixel[0] * pixel[3] / 255;
green += pixel[1] * pixel[3] / 255;
blue += pixel[2] * pixel[3] / 255;
alpha += pixel[3];
brightness += ((2126ULL * pixel[0] +
7152ULL * pixel[1] + 722ULL * pixel[2]) / 10000) *
pixel[3] / 255;
}
}
if (count == 0)
return;
sample->red = red / count;
sample->green = green / count;
sample->blue = blue / count;
sample->alpha = alpha / count;
sample->brightness = brightness / count;
}
left = (uint64_t)x * im->canvas_width / im->sx;
top = (uint64_t)y * im->canvas_height / im->sy;
right = ((uint64_t)(x + w) * im->canvas_width + im->sx - 1) / im->sx;
bottom = ((uint64_t)(y + h) * im->canvas_height + im->sy - 1) / im->sy;
/* Build fallback samples for every image cell. */
static void
image_make_cells(struct image *im)
{
struct image_cell *cell;
uint64_t columns, rows, column, row;
u_int x, y, sample_x, sample_y;
columns = (uint64_t)im->sx * IMAGE_SAMPLE_COLUMNS;
rows = (uint64_t)im->sy * IMAGE_SAMPLE_ROWS;
im->cells = xcalloc((size_t)im->sx * im->sy, sizeof *im->cells);
for (y = 0; y < im->sy; y++) {
for (x = 0; x < im->sx; x++) {
cell = &im->cells[(size_t)y * im->sx + x];
image_sample(im, x, y, im->sx, im->sy, &cell->whole);
column = (uint64_t)x * IMAGE_SAMPLE_COLUMNS;
row = (uint64_t)y * IMAGE_SAMPLE_ROWS;
for (sample_y = 0; sample_y < IMAGE_SAMPLE_ROWS;
sample_y++) {
for (sample_x = 0;
sample_x < IMAGE_SAMPLE_COLUMNS;
sample_x++) {
image_sample(im, column + sample_x,
row + sample_y, columns, rows,
&cell->samples[sample_y][sample_x]);
}
}
}
if (left >= im->width || top >= im->height)
*px = *py = 0;
else {
if (right <= left)
right = left + 1;
if (bottom <= top)
bottom = top + 1;
if (right > im->width)
right = im->width;
if (bottom > im->height)
bottom = im->height;
*px = left;
*py = top;
*pw = right - left;
*ph = bottom - top;
}
}
@@ -403,6 +355,13 @@ image_set_no_cursor(struct image *im)
im->flags |= IMAGE_FLAG_NO_CURSOR;
}
/* Return whether writing the image should leave the cursor in place. */
int
image_has_no_cursor(struct image *im)
{
return (!!(im->flags & IMAGE_FLAG_NO_CURSOR));
}
/* Return an image's original SIXEL data. */
struct sixel_image *
image_get_sixel(struct image *im)
@@ -438,80 +397,6 @@ image_set_order_id(struct image *im, u_int id)
im->order_id = id;
}
/* Return whether writing the image should leave the cursor in place. */
int
image_has_no_cursor(struct image *im)
{
return (!!(im->flags & IMAGE_FLAG_NO_CURSOR));
}
/* Return cached fallback output data. */
void *
image_get_fallback_data(struct image *im)
{
return (im->fallback_data);
}
/* Store cached fallback output data. */
void
image_set_fallback_data(struct image *im, void *data)
{
im->fallback_data = data;
}
/* Return a precomputed fallback cell sample. */
const struct image_cell *
image_get_cell(struct image *im, u_int x, u_int y)
{
if (im == NULL || x >= im->sx || y >= im->sy)
return (NULL);
if (im->cells == NULL)
image_make_cells(im);
return (&im->cells[(size_t)y * im->sx + x]);
}
/* Convert an image cell rectangle to pixel coordinates. */
void
image_get_pixel_rect(struct image *im, u_int x, u_int y,
u_int width, u_int height, u_int *px, u_int *py, u_int *pwidth,
u_int *pheight)
{
u_int left, top, right, bottom;
*px = *py = *pwidth = *pheight = 0;
if (im == NULL || x >= im->sx || y >= im->sy || width == 0 ||
height == 0) {
return;
}
if (width > im->sx - x)
width = im->sx - x;
if (height > im->sy - y)
height = im->sy - y;
left = (uint64_t)x * im->canvas_width / im->sx;
top = (uint64_t)y * im->canvas_height / im->sy;
right = ((uint64_t)(x + width) * im->canvas_width + im->sx - 1) /
im->sx;
bottom = ((uint64_t)(y + height) * im->canvas_height + im->sy - 1) /
im->sy;
if (left >= im->width || top >= im->height) {
*px = *py = 0;
return;
}
if (right <= left)
right = left + 1;
if (bottom <= top)
bottom = top + 1;
if (right > im->width)
right = im->width;
if (bottom > im->height)
bottom = im->height;
*px = left;
*py = top;
*pwidth = right - left;
*pheight = bottom - top;
}
/* Decode a base64 payload with a size limit. */
u_char *
image_base64_decode(const char *data, size_t len, size_t limit, size_t *size)