diff --git a/grid.c b/grid.c index 1e29878f8..88eafaa46 100644 --- a/grid.c +++ b/grid.c @@ -1383,8 +1383,9 @@ grid_reflow_has_image(struct grid_line *gl) /* Kitty Unicode placeholder base character (U+10EEEE). */ if (gc.data.size >= 4 && gc.data.data[0] == 0xf4 && gc.data.data[1] == 0x8e && gc.data.data[2] == 0xbb && - gc.data.data[3] == 0xae) + gc.data.data[3] == 0xae) { return (1); + } } return (0); } diff --git a/image-fallback.c b/image-fallback.c index 5292b3959..63367279b 100644 --- a/image-fallback.c +++ b/image-fallback.c @@ -48,9 +48,10 @@ struct image_rgb { u_char b; }; +/* Shading levels cached for the lifetime of an image. */ struct image_glyph_data { - u_char *shade5; - u_char *shade8; + u_char *shade5; /* Cached levels for five-shade output. */ + u_char *shade8; /* Cached levels for eight-shade output. */ }; static const struct image_rgb image_ansi_colours[16] = { @@ -64,6 +65,7 @@ static const struct image_rgb image_ansi_colours[16] = { { 0x00, 0xff, 0xff }, { 0xff, 0xff, 0xff } }; +/* Return the squared RGB distance between two colours. */ static u_int image_glyph_distance(struct image_rgb a, struct image_rgb b) { @@ -72,6 +74,7 @@ image_glyph_distance(struct image_rgb a, struct image_rgb b) return (r * r + g * g + bl * bl); } +/* Return whether a colour is close enough to grey to use neutral colours. */ static int image_glyph_low_saturation(struct image_rgb colour) { @@ -89,6 +92,7 @@ image_glyph_low_saturation(struct image_rgb colour) return (maximum == 0 || (maximum - minimum) * 4 <= maximum); } +/* Find the closest ANSI colour, keeping greys on the neutral ramp. */ static u_int image_glyph_nearest_ansi(struct image_rgb colour, u_int colours) { @@ -97,8 +101,9 @@ image_glyph_nearest_ansi(struct image_rgb colour, u_int colours) for (i = 0; i < colours; i++) { if (neutral && i != 0 && i != 7 && - (colours != 16 || (i != 8 && i != 15))) + (colours != 16 || (i != 8 && i != 15))) { continue; + } distance = image_glyph_distance(colour, image_ansi_colours[i]); if (distance < best_distance) { best_distance = distance; @@ -108,13 +113,14 @@ image_glyph_nearest_ansi(struct image_rgb colour, u_int colours) return (best); } +/* Map an RGB colour to the terminal palette and its output colour code. */ static struct image_rgb image_glyph_quantize(struct image_rgb colour, enum image_glyph_palette palette, int *output) { - struct image_rgb result; + struct image_rgb result; int value; - u_int index; + u_int index, ncolours; if (palette == IMAGE_GLYPH_PALETTE_RGB) { *output = colour_join_rgb(colour.r, colour.g, colour.b); @@ -126,8 +132,10 @@ image_glyph_quantize(struct image_rgb colour, enum image_glyph_palette palette, colour_split_rgb(value, &result.r, &result.g, &result.b); return (result); } - index = image_glyph_nearest_ansi(colour, - palette == IMAGE_GLYPH_PALETTE_8 ? 8 : 16); + ncolours = 16; + if (palette == IMAGE_GLYPH_PALETTE_8) + ncolours = 8; + index = image_glyph_nearest_ansi(colour, ncolours); result = image_ansi_colours[index]; if (index < 8) *output = index; @@ -136,6 +144,7 @@ image_glyph_quantize(struct image_rgb colour, enum image_glyph_palette palette, return (result); } +/* Fit two colours to a block glyph's pixel samples. */ static void image_glyph_fit_colours(const struct image_rgb *samples, u_int count, struct image_rgb centres[2]) @@ -143,6 +152,7 @@ image_glyph_fit_colours(const struct image_rgb *samples, u_int count, u_int sum[2][3], counts[2], group, i, j, iteration, distance; u_int maximum = 0, first = 0, second = 0; + /* Start with the two most widely separated samples. */ for (i = 0; i < count; i++) { for (j = i + 1; j < count; j++) { distance = image_glyph_distance(samples[i], samples[j]); @@ -155,6 +165,8 @@ image_glyph_fit_colours(const struct image_rgb *samples, u_int count, } centres[0] = samples[first]; centres[1] = samples[second]; + + /* Refine the foreground and background by averaging each cluster. */ for (iteration = 0; iteration < 4; iteration++) { memset(sum, 0, sizeof sum); memset(counts, 0, sizeof counts); @@ -176,11 +188,12 @@ image_glyph_fit_colours(const struct image_rgb *samples, u_int count, } } +/* Copy the UTF-8 character for an ACS key into a fallback cell. */ static int image_glyph_set_acs(struct tty *tty, struct utf8_data *data, u_char key) { struct utf8_data *ud; - const char *s = tty_acs_get(tty, key); + const char *s = tty_acs_get(tty, key); if (s == NULL) return (0); @@ -194,6 +207,7 @@ image_glyph_set_acs(struct tty *tty, struct utf8_data *data, u_char key) return (1); } +/* Map a block's foreground mask to a half, quadrant or sextant glyph. */ static u_char image_glyph_block_key(enum image_glyph_detail detail, u_int mask) { @@ -234,6 +248,7 @@ image_glyph_block_key(enum image_glyph_detail detail, u_int mask) return (key); } +/* Build and cache dithered brightness levels for a shading palette. */ static u_char * image_glyph_make_shades(struct image *im, u_int levels) { @@ -250,6 +265,8 @@ image_glyph_make_shades(struct image *im, u_int levels) result = (levels == 5 ? data->shade5 : data->shade8); if (result != NULL) return (result); + + /* Collect cell brightness and the range used by this image. */ cells = (size_t)im->sx * im->sy; values = xcalloc(cells, sizeof *values); result = xcalloc(cells, 1); @@ -265,11 +282,15 @@ image_glyph_make_shades(struct image *im, u_int levels) values[index] = value; } } + /* Stretch the brightness range to make all shading levels available. */ if (maximum > minimum) { - for (index = 0; index < cells; index++) + for (index = 0; index < cells; index++) { values[index] = (values[index] - minimum) * 255 / (maximum - minimum); + } } + + /* Dither each row, alternating direction to distribute the error. */ for (y = 0; y < im->sy; y++) { reverse = (y & 1); for (scan = 0; scan < im->sx; scan++) { @@ -302,6 +323,8 @@ image_glyph_make_shades(struct image *im, u_int levels) } } free(values); + + /* Keep the result for subsequent redraws at the same shading level. */ if (levels == 5) data->shade5 = result; else @@ -309,10 +332,11 @@ image_glyph_make_shades(struct image *im, u_int levels) return (result); } +/* Choose the richest colour palette supported by this terminal. */ static enum image_glyph_palette image_glyph_get_palette(struct tty *tty) { - int colours; + int colours; if (tty->term->flags & TERM_RGBCOLOURS) return (IMAGE_GLYPH_PALETTE_RGB); @@ -326,6 +350,7 @@ image_glyph_get_palette(struct tty *tty) return (IMAGE_GLYPH_PALETTE_8); } +/* Choose the fallback glyph detail supported by this terminal. */ static enum image_glyph_detail image_glyph_get_detail(struct tty *tty, enum image_glyph_palette palette) { @@ -342,6 +367,7 @@ image_glyph_get_detail(struct tty *tty, enum image_glyph_palette palette) return (IMAGE_GLYPH_SHADE8); } +/* Render one image cell using a two-colour block glyph. */ static void image_glyph_block(struct tty *tty, struct image *im, u_int x, u_int y, enum image_glyph_detail detail, enum image_glyph_palette palette, @@ -354,6 +380,7 @@ image_glyph_block(struct tty *tty, struct image *im, u_int x, u_int y, int colours[2]; u_char key; + /* Average the stored samples into the chosen glyph's subcells. */ columns = (detail == IMAGE_GLYPH_HALF ? 1 : 2); rows = (detail == IMAGE_GLYPH_SEXTANT ? 3 : 2); i = 0; @@ -378,6 +405,8 @@ image_glyph_block(struct tty *tty, struct image *im, u_int x, u_int y, i++; } } + + /* Fit the two colours and choose which subcells use the foreground. */ n = columns * rows; image_glyph_fit_colours(samples, n, centres); quantized[0] = image_glyph_quantize(centres[0], palette, &colours[0]); @@ -385,10 +414,13 @@ image_glyph_block(struct tty *tty, struct image *im, u_int x, u_int y, mask = 0; for (i = 0; i < n; i++) { if (image_glyph_distance(samples[i], quantized[1]) < - image_glyph_distance(samples[i], quantized[0])) + image_glyph_distance(samples[i], quantized[0])) { mask |= (1U << i); + } } key = image_glyph_block_key(detail, mask); + + /* Use a blank cell if the terminal has no matching glyph. */ if (key == 0) utf8_set(&out->data, ' '); else if (!image_glyph_set_acs(tty, &out->data, key)) @@ -397,6 +429,7 @@ image_glyph_block(struct tty *tty, struct image *im, u_int x, u_int y, out->bg = colours[0]; } +/* Render an image cell using the terminal's text and colour capabilities. */ void image_get_fallback_cell(struct tty *tty, struct image *im, u_int x, u_int y, const struct grid_cell *gc, struct grid_cell *out) @@ -414,7 +447,7 @@ image_get_fallback_cell(struct tty *tty, struct image *im, u_int x, u_int y, enum image_glyph_palette palette; enum image_glyph_detail detail; u_char *levels, key; - u_int level = 0; + u_int level = 0, nlevels; memcpy(out, gc, sizeof *out); cell = image_get_cell(im, x, y); @@ -422,6 +455,8 @@ image_get_fallback_cell(struct tty *tty, struct image *im, u_int x, u_int y, utf8_set(&out->data, ' '); return; } + + /* Select the palette and glyph detail for this terminal. */ palette = image_glyph_get_palette(tty); detail = image_glyph_get_detail(tty, palette); if (detail == IMAGE_GLYPH_ASCII) { @@ -430,8 +465,10 @@ image_get_fallback_cell(struct tty *tty, struct image *im, u_int x, u_int y, return; } if (detail == IMAGE_GLYPH_SHADE5 || detail == IMAGE_GLYPH_SHADE8) { - levels = image_glyph_make_shades(im, - detail == IMAGE_GLYPH_SHADE5 ? 5 : 8); + nlevels = 8; + if (detail == IMAGE_GLYPH_SHADE5) + nlevels = 5; + levels = image_glyph_make_shades(im, nlevels); level = levels[(size_t)y * im->sx + x]; key = (detail == IMAGE_GLYPH_SHADE5 ? shades[level] : bold_shades[level]); @@ -443,13 +480,15 @@ image_get_fallback_cell(struct tty *tty, struct image *im, u_int x, u_int y, out->bg = 0; out->attr &= ~GRID_ATTR_BRIGHT; if (detail == IMAGE_GLYPH_SHADE8 && - (level == 2 || level == 4 || level == 7)) + (level == 2 || level == 4 || level == 7)) { out->attr |= GRID_ATTR_BRIGHT; + } return; } image_glyph_block(tty, im, x, y, detail, palette, out); } +/* Free the brightness levels cached for text image rendering. */ void image_free_fallback(struct image *im) { diff --git a/image-kitty.c b/image-kitty.c index ae41189b5..2d44d3cce 100644 --- a/image-kitty.c +++ b/image-kitty.c @@ -69,57 +69,62 @@ static const uint32_t kitty_diacritics[] = { 0x1D244 }; +/* Graphics control values and decoded bytes retained across upload chunks. */ struct kitty_state { char action; char delete; u_int format; char medium; char compression; - u_int width; + u_int width; /* Source pixel dimensions. */ u_int height; - u_int source_x; + u_int source_x; /* Crop origin in source pixels. */ u_int source_y; - u_int x_offset; + u_int x_offset; /* Pixel offset inside the starting cell. */ u_int y_offset; - u_int source_width; + u_int source_width; /* Crop dimensions, zero for the remainder. */ u_int source_height; - u_int columns; + u_int columns; /* Placement cells, zero for automatic size. */ u_int rows; u_int image_id; u_int image_number; u_int placement_id; int32_t z; u_int quiet; - int no_cursor; - int virtual; - u_int data_size; - int more; - int unsupported; + int no_cursor; /* Suppress cursor movement after placement. */ + int virtual; /* Placement uses Unicode placeholders. */ + u_int data_size; /* Expected size of a compressed PNG payload. */ + int more; /* Another upload chunk follows. */ + int unsupported; /* A requested operation is not implemented. */ - u_char *raw; - size_t rawlen; + u_char *raw; /* Base64-decoded upload bytes. */ + size_t rawlen; /* Number of accumulated bytes. */ }; +/* An application placement retained by one pane's Kitty parser. */ struct kitty_placement { u_int placement_id; - u_int server_id; + u_int server_id; /* Referenced image view. */ int32_t z; int virtual; struct kitty_placement *next; }; +/* Map an application image ID to its source and placement views. */ struct kitty_source { - u_int app_id; - u_int server_id; + u_int app_id; /* Application ID. */ + u_int server_id; /* Referenced source image. */ struct kitty_placement *placements; struct kitty_source *next; }; +/* Kitty input state kept for the lifetime of a pane's parser. */ struct kitty_context { - struct kitty_state *transfer; + struct kitty_state *transfer; /* Pending upload, or NULL. */ struct kitty_source *sources; }; +/* An image uploaded to one terminal at its current cell geometry. */ struct kitty_image_cache { u_int server_id; u_int kitty_id; @@ -130,19 +135,22 @@ struct kitty_image_cache { struct kitty_image_cache *next; }; +/* One terminal placement, retained until its redraw replacement is ready. */ struct kitty_placement_cache { - u_int id; - u_int x; + u_int id; /* Terminal placement ID. */ + u_int x; /* Origin in terminal cells. */ u_int y; - u_int width; + u_int width; /* Size in cells. */ u_int height; u_int source_x; u_int source_y; int32_t z; + /* Delete after replacement placements have been drawn. */ int pending_delete; struct kitty_placement_cache *next; }; +/* Image uploads and ID allocation kept for the lifetime of a terminal. */ struct kitty_output { struct kitty_image_cache *images; u_int next_id; @@ -203,10 +211,7 @@ kitty_redraw_keep_piece(struct tty *tty, struct kitty_image_cache *entry, placement->z); } -/* - * Place the parts of a placement outside a redraw area again, since the - * redraw will not replace them. - */ +/* Preserve placement pieces outside the redraw area. */ static void kitty_redraw_keep(struct tty *tty, struct kitty_image_cache *entry, struct kitty_placement_cache *placement, u_int x, u_int y, u_int width, @@ -241,14 +246,7 @@ kitty_redraw_keep(struct tty *tty, struct kitty_image_cache *entry, px1 - ix1, iy1 - iy0); } -/* - * Mark placements intersecting a redraw area as stale rather than deleting - * them yet (see kitty_redraw_finish()) - some implementations free an - * image's pixel data once its last placement is gone, so an image only - * placed here would go blank before its replacement lands. A placement only - * partly inside the area is deleted as a whole, so place its outside parts - * again first or they would vanish from cells nothing redraws. - */ +/* Mark intersecting placements for deletion after their replacements land. */ void kitty_redraw_start(struct tty *tty, u_int x, u_int y, u_int width, u_int height) @@ -259,6 +257,8 @@ kitty_redraw_start(struct tty *tty, u_int x, u_int y, u_int width, if (ko == NULL) return; + + /* Retain pixel data by keeping placements alive until replacement. */ for (entry = ko->images; entry != NULL; entry = entry->next) { for (placement = entry->placements; placement != NULL; placement = placement->next) { @@ -267,28 +267,26 @@ kitty_redraw_start(struct tty *tty, u_int x, u_int y, u_int width, if (placement->x >= x + width || placement->x + placement->width <= x || placement->y >= y + height || - placement->y + placement->height <= y) + placement->y + placement->height <= y) { continue; + } placement->pending_delete = 1; + + /* Preserve pieces outside the redraw area. */ kitty_redraw_keep(tty, entry, placement, x, y, width, height); } } } -/* - * Delete placements marked stale by kitty_redraw_start() - called once any - * replacement placements have already been created, so an image already - * placed elsewhere in the same redraw is never left with none at all in - * between the two. - */ +/* Delete stale placements once replacement placements exist. */ void kitty_redraw_finish(struct tty *tty) { struct kitty_output *ko = tty->image_data; struct kitty_image_cache *entry; struct kitty_placement_cache **pp, *placement; - char s[64]; + char s[64]; if (ko == NULL) return; @@ -299,8 +297,8 @@ kitty_redraw_finish(struct tty *tty) continue; } xsnprintf(s, sizeof s, - "\033_Ga=d,d=i,i=%u,p=%u,q=2\033\\", entry->kitty_id, - placement->id); + "\033_Ga=d,d=i,i=%u,p=%u,q=2\033\\", + entry->kitty_id, placement->id); tty_puts(tty, s); *pp = placement->next; free(placement); @@ -350,12 +348,12 @@ kitty_free_stale_images(struct tty *tty) /* Save the successor before this entry may be removed. */ next = entry->next; if (image_find(entry->server_id) != NULL) { - /* Retained entries become the predecessor of the next one. */ + /* Retain this entry as the next one's predecessor. */ previous = entry; continue; } - /* Unlink stale entries, including the first entry in the list. */ + /* Unlink stale entries, including the list head. */ if (previous == NULL) ko->images = next; else @@ -418,7 +416,7 @@ kitty_upload(struct tty *tty, struct image *im) struct kitty_image_cache *entry; char control[128], encoded[4097]; const u_char *pixels; - u_char *padded; + u_char *padded, *padded_row; size_t offset, size, row, stride, image_size; int encodedlen; u_int id, width, height; @@ -434,12 +432,14 @@ kitty_upload(struct tty *tty, struct image *im) if ((uint64_t)upload_width * upload_height > IMAGE_SIZE_LIMIT / 4) return (NULL); + /* Reuse uploads only while the terminal cell size is unchanged. */ for (entry = ko->images; entry != NULL; entry = entry->next) { if (entry->server_id != image_get_id(im)) continue; if (entry->xpixel == tty->xpixel && - entry->ypixel == tty->ypixel) + entry->ypixel == tty->ypixel) { return (entry); + } kitty_delete(tty, entry->kitty_id); kitty_free_placements(entry); entry->server_id = 0; @@ -450,6 +450,8 @@ kitty_upload(struct tty *tty, struct image *im) entry->next = ko->images; ko->images = entry; } + + /* Allocate a nonzero terminal image ID. */ do { id = ++ko->next_id & 0xffffff; } while (id == 0); @@ -460,21 +462,18 @@ kitty_upload(struct tty *tty, struct image *im) entry->next_placement = 0; pixels = image_get_pixels(im, &stride, &image_size); - /* - * Pad the upload with duplicate edge pixels. Kitty linearly filters scaled - * textures against transparent border pixels, which otherwise darkens the - * outermost pixels of an opaque image. - */ + + /* Duplicate edge pixels to prevent dark borders from filtering. */ padded = xcalloc((size_t)upload_width * upload_height, 4); - for (row = 0; row < height; row++) - memcpy(padded + ((size_t)(row + 1) * upload_width + 1) * 4, + for (row = 0; row < height; row++) { + memcpy(padded + ((row + 1) * upload_width + 1) * 4, pixels + row * stride, (size_t)width * 4); + } for (row = 1; row <= canvas_height; row++) { - memcpy(padded + (size_t)row * upload_width * 4, - padded + ((size_t)row * upload_width + 1) * 4, 4); - memcpy(padded + ((size_t)row * upload_width + upload_width - 1) * 4, - padded + ((size_t)row * upload_width + upload_width - 2) * 4, - 4); + padded_row = padded + row * upload_width * 4; + memcpy(padded_row, padded_row + 4, 4); + memcpy(padded_row + (upload_width - 1) * 4, + padded_row + (upload_width - 2) * 4, 4); } memcpy(padded, padded + (size_t)upload_width * 4, (size_t)upload_width * 4); @@ -485,6 +484,8 @@ kitty_upload(struct tty *tty, struct image *im) width = upload_width; height = upload_height; image_size = (size_t)width * height * 4; + + /* Send base64 chunks, with image controls on the first chunk. */ for (offset = 0; offset < image_size; offset += size) { size = image_size - offset; if (size > KITTY_CHUNK_SIZE) @@ -518,7 +519,8 @@ kitty_draw_rect(struct tty *tty, const struct image_rect *rectangle) struct kitty_image_cache *entry; struct image *im; u_int source_x, source_y; - u_int width, height, destination_x, destination_y; + u_int width, height; + u_int destination_x, destination_y; int32_t z; im = image_rect_get_image(rectangle); @@ -581,6 +583,7 @@ kitty_control(struct kitty_state *ks, const u_char *buf, size_t len) int32_t signed_number; char key; + /* Read comma-separated keys and validate each control value. */ while (buf < end) { key = *buf++; if (buf == end || *buf++ != '=') @@ -612,22 +615,25 @@ kitty_control(struct kitty_state *ks, const u_char *buf, size_t len) break; case 'P': case 'Q': if (kitty_number((const char *)value, valuelen, - &number) != 0) + &number) != 0) { return (-1); + } if (number != 0) ks->unsupported = 1; break; case 'H': case 'V': if (kitty_signed_number((const char *)value, valuelen, - &signed_number) != 0) + &signed_number) != 0) { return (-1); + } if (signed_number != 0) ks->unsupported = 1; break; case 'z': if (kitty_signed_number((const char *)value, valuelen, - &ks->z) != 0) + &ks->z) != 0) { return (-1); + } break; case 'f': case 's': @@ -649,8 +655,9 @@ kitty_control(struct kitty_state *ks, const u_char *buf, size_t len) case 'X': case 'Y': if (kitty_number((const char *)value, valuelen, - &number) != 0) + &number) != 0) { return (-1); + } switch (key) { case 'f': ks->format = number; break; case 's': ks->width = number; break; @@ -857,14 +864,16 @@ kitty_delete_images(void *state, struct screen_write_ctx *ctx, if (kc == NULL) return; - if (how >= 'A') { - if (how <= 'Z') { - how += 'a' - 'A'; - release = 1; - } + + /* Uppercase selectors also release unreferenced source image data. */ + if (how >= 'A' && how <= 'Z') { + how += 'a' - 'A'; + release = 1; } if (how == 'r') placement_id = 0; + + /* Remove grid placements before checking virtual placements. */ for (source = kc->sources; source != NULL; source = source->next) (void)kitty_prune_placements(source, gd); image_clear_kitty(ctx, result); @@ -874,20 +883,18 @@ kitty_delete_images(void *state, struct screen_write_ctx *ctx, if (how == 'i') { if (source->app_id == result->image_id) selected = 1; - } else if (how == 'r') { - if (source->app_id >= result->x) { - if (source->app_id <= result->y) - selected = 1; - } + } else if (how == 'r' && source->app_id >= result->x && + source->app_id <= result->y) { + selected = 1; } if (selected) { for (pp = &source->placements; (placement = *pp) != NULL; ) { if (!placement->virtual) goto keep_placement; - if (placement_id != 0) { - if (placement->placement_id != placement_id) - goto keep_placement; + if (placement_id != 0 && + placement->placement_id != placement_id) { + goto keep_placement; } *pp = placement->next; image_free(placement->server_id); @@ -900,11 +907,9 @@ keep_placement: if (placement_id == 0) removed++; } - if (!release) - goto keep_source; - if (removed == 0) - goto keep_source; - if (source->placements != NULL) + + /* Keep source data until every retained placement is gone. */ + if (!release || removed == 0 || source->placements != NULL) goto keep_source; if (image_grid_has_image(gd, source->server_id)) goto keep_source; @@ -930,6 +935,7 @@ kitty_append(struct kitty_state *ks, const u_char *buf, size_t len) if (len > IMAGE_SIZE_LIMIT) return (-1); + /* Older Chafa chunks have padding with nonzero unused bits. */ if (len >= 4 && len % 4 == 0 && buf[len - 1] == '=') { offset = len - 2; @@ -979,14 +985,14 @@ kitty_raw(struct kitty_state *ks, u_char *data, size_t size) u_int bytes; bytes = (ks->format == 24 ? 3 : 4); - if (ks->width == 0) - return (NULL); - if (ks->height == 0) + if (ks->width == 0 || ks->height == 0) return (NULL); if ((uint64_t)ks->width * ks->height > IMAGE_SIZE_LIMIT / 4) return (NULL); expected = (size_t)ks->width * ks->height * bytes; raw = data; + + /* Decompress before checking the exact raster byte count. */ if (ks->compression == 'z') { rawlen = expected; raw = xmalloc(expected); @@ -1006,6 +1012,7 @@ kitty_raw(struct kitty_state *ks, u_char *data, size_t size) if (bytes == 4) return (raw); + /* Expand RGB input to the shared RGBA pixel format. */ pixels = xmalloc((size_t)ks->width * ks->height * 4); for (i = j = 0; i < expected; i += 3, j += 4) { pixels[j] = raw[i]; @@ -1035,24 +1042,23 @@ kitty_place_image(struct image *source, struct kitty_state *ks, u_int xpixel, image_get_size(source, &source_width, &source_height); view.x = ks->source_x; view.y = ks->source_y; - if (view.x >= source_width) - return (NULL); - if (view.y >= source_height) + + /* Clip the requested source rectangle before scaling it. */ + if (view.x >= source_width || view.y >= source_height) return (NULL); view.width = source_width - view.x; - if (ks->source_width != 0) { - if (view.width > ks->source_width) - view.width = ks->source_width; - } + if (ks->source_width != 0 && view.width > ks->source_width) + view.width = ks->source_width; view.height = source_height - view.y; - if (ks->source_height != 0) { - if (view.height > ks->source_height) - view.height = ks->source_height; - } + if (ks->source_height != 0 && view.height > ks->source_height) + view.height = ks->source_height; + + /* Choose placement dimensions in cells, preserving the aspect ratio. */ cell_width = (xpixel == 0 ? 8 : xpixel); cell_height = (ypixel == 0 ? 16 : ypixel); x_offset = ks->x_offset; y_offset = ks->y_offset; + /* Match Kitty by clamping offsets to the starting cell. */ if (x_offset >= cell_width) x_offset = cell_width - 1; @@ -1060,19 +1066,15 @@ kitty_place_image(struct image *source, struct kitty_state *ks, u_int xpixel, y_offset = cell_height - 1; view.sx = ks->columns; view.sy = ks->rows; - if (view.sx > USHRT_MAX) + if (view.sx > USHRT_MAX || view.sy > USHRT_MAX) return (NULL); - if (view.sy > USHRT_MAX) - return (NULL); - if (view.sx == 0) { - if (view.sy == 0) - natural_size = 1; - } + if (view.sx == 0 && view.sy == 0) + natural_size = 1; if (natural_size) { - if (x_offset > UINT_MAX - view.width) - return (NULL); - if (y_offset > UINT_MAX - view.height) + if (x_offset > UINT_MAX - view.width || + y_offset > UINT_MAX - view.height) { return (NULL); + } image_size_in_cells(view.width + x_offset, view.height + y_offset, cell_width, cell_height, &view.sx, &view.sy); @@ -1100,6 +1102,7 @@ kitty_place_image(struct image *source, struct kitty_state *ks, u_int xpixel, } canvas_width = (uint64_t)view.sx * cell_width; canvas_height = (uint64_t)view.sy * cell_height; + /* Preserve source resolution when shrinking the image. */ if (scale < 1) units = 1 / scale; @@ -1119,49 +1122,38 @@ kitty_place_image(struct image *source, struct kitty_state *ks, u_int xpixel, view.scaled_height = view.height; view.x_offset = floor(x_offset * units); view.y_offset = floor(y_offset * units); - if (view.x_offset > view.canvas_width) + if (view.x_offset > view.canvas_width || + view.scaled_width > view.canvas_width - view.x_offset || + view.y_offset > view.canvas_height || + view.scaled_height > view.canvas_height - view.y_offset) { return (NULL); - if (view.scaled_width > view.canvas_width - view.x_offset) - return (NULL); - if (view.y_offset > view.canvas_height) - return (NULL); - if (view.scaled_height > view.canvas_height - view.y_offset) - return (NULL); - if (ks->columns != 0) { - if (ks->rows != 0) { - view.x_offset += (view.canvas_width - view.x_offset - - view.scaled_width) / 2; - view.y_offset += (view.canvas_height - view.y_offset - - view.scaled_height) / 2; - } + } + + /* Centre the image when both placement dimensions were specified. */ + if (ks->columns != 0 && ks->rows != 0) { + view.x_offset += (view.canvas_width - view.x_offset - + view.scaled_width) / 2; + view.y_offset += (view.canvas_height - view.y_offset - + view.scaled_height) / 2; } /* A matching rectangle needs neither resampling nor padding. */ - if (x_offset == 0) { - if (y_offset == 0) { - if ((uint64_t)view.width * canvas_height == - (uint64_t)view.height * canvas_width) { - view.canvas_width = view.width; - view.scaled_width = view.width; - view.canvas_height = view.height; - view.scaled_height = view.height; - view.x_offset = 0; - view.y_offset = 0; - } - } + if (x_offset == 0 && y_offset == 0 && + (uint64_t)view.width * canvas_height == + (uint64_t)view.height * canvas_width) { + view.canvas_width = view.width; + view.scaled_width = view.width; + view.canvas_height = view.height; + view.scaled_height = view.height; + view.x_offset = 0; + view.y_offset = 0; } im = image_create_view(source, &view); - if (im != NULL) { - if (ks->no_cursor) - image_set_no_cursor(im); - } + if (im != NULL && ks->no_cursor) + image_set_no_cursor(im); return (im); } -/* - * Parse one Kitty graphics APC body (without the leading G). Only direct - * static images are accepted. The returned image retains immutable RGBA - * pixels for the lifetime of its placement. - */ +/* Parse a Kitty graphics APC body into a reply and optional image placement. */ struct image * kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, u_int ypixel, struct kitty_parse_result *result) @@ -1185,6 +1177,8 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, } memset(result, 0, sizeof *result); result->status = KITTY_PARSE_ERROR; + + /* Split controls from payload and resume any incomplete upload. */ ks = kc->transfer; semi = memchr(buf, ';', len); controllen = (semi == NULL ? len : (size_t)(semi - buf)); @@ -1206,10 +1200,10 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, } ks->more = 0; error = kitty_control(ks, buf, controllen); - if (ks->image_id == 0) { - if (ks->image_number == 0) - ks->placement_id = 0; - } + if (ks->image_id == 0 && ks->image_number == 0) + ks->placement_id = 0; + + /* Preserve reply fields even when the command cannot be accepted. */ result->image_id = ks->image_id; result->image_number = ks->image_number; result->quiet = ks->quiet; @@ -1235,16 +1229,18 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, result->status = KITTY_PARSE_UNSUPPORTED; goto fail; } - if (payloadlen != 0) { - if (kitty_append(ks, semi + 1, payloadlen) != 0) - goto fail; - } + + /* Accumulate decoded chunks until the final transmission. */ + if (payloadlen != 0 && kitty_append(ks, semi + 1, payloadlen) != 0) + goto fail; if (ks->more) { kc->transfer = ks; result->status = KITTY_PARSE_MORE; return (NULL); } kc->transfer = NULL; + + /* Placement and deletion commands need no new raster data. */ if (ks->action == 'p') { source = kitty_source_get(kc, ks->image_id); if (source == NULL) { @@ -1298,6 +1294,7 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, goto fail; } + /* Decode the complete PNG or raw pixel payload. */ decoded = ks->raw; decodedlen = ks->rawlen; ks->raw = NULL; @@ -1338,6 +1335,7 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, if (pixels == NULL) goto fail; + /* Pad the source canvas to whole cells before creating placements. */ cell_width = (xpixel == 0 ? 8 : xpixel); cell_height = (ypixel == 0 ? 16 : ypixel); image_size_in_cells(ks->width, ks->height, cell_width, cell_height, @@ -1353,8 +1351,12 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, free(pixels); else { result->status = KITTY_PARSE_OK; - if (ks->action != 'q') - result->replace_id = kitty_source_set(kc, ks->image_id, source); + + /* Queries validate the data without retaining an image. */ + if (ks->action != 'q') { + result->replace_id = kitty_source_set(kc, + ks->image_id, source); + } if (ks->action == 'q') im = NULL; else if (ks->action == 'T' && !ks->virtual) { @@ -1487,8 +1489,8 @@ kitty_placeholder_to_image(void *state, struct grid *gd, struct grid_cell *gc, struct kitty_context *kc = state; struct kitty_source *source; struct kitty_placement *placement; - struct kitty_placeholder left; - struct grid_cell left_cell; + struct kitty_placeholder left; + struct grid_cell left_cell; struct image *im; uint32_t value; size_t offset = 0; @@ -1498,15 +1500,19 @@ kitty_placeholder_to_image(void *state, struct grid *gd, struct grid_cell *gc, if (kc == NULL) return (0); + + /* Decode the placeholder base character and its coordinate marks. */ if (!kitty_placeholder_character(gc->data.data, gc->data.size, &offset, - &value)) + &value)) { return (0); + } if (value != 0x10eeee) return (0); while (offset < gc->data.size && nvalues < nitems(values)) { if (!kitty_placeholder_character(gc->data.data, gc->data.size, - &offset, &value)) + &offset, &value)) { return (0); + } if (!kitty_placeholder_index(value, &values[nvalues])) return (0); nvalues++; @@ -1521,23 +1527,19 @@ kitty_placeholder_to_image(void *state, struct grid *gd, struct grid_cell *gc, y = values[0]; if (nvalues >= 2) x = values[1]; + + /* Inherit missing coordinates from the matching left placeholder. */ if (grid_x != 0) { grid_view_get_cell(gd, grid_x - 1, grid_y, &left_cell); - if (left_cell.fg == gc->fg) { - if (left_cell.us == gc->us) { - inherit = image_grid_get_placeholder(gd, - grid_x - 1, gd->hsize + grid_y, &left); - } + if (left_cell.fg == gc->fg && left_cell.us == gc->us) { + inherit = image_grid_get_placeholder(gd, + grid_x - 1, gd->hsize + grid_y, &left); } } if (inherit) { - if (nvalues >= 1) { - if (left.source_y != y) - inherit = 0; - } - if (nvalues >= 2) { - if (left.source_x + 1 != x) - inherit = 0; + if ((nvalues >= 1 && left.source_y != y) || + (nvalues >= 2 && left.source_x + 1 != x)) { + inherit = 0; } } if (inherit) { @@ -1553,6 +1555,8 @@ kitty_placeholder_to_image(void *state, struct grid *gd, struct grid_cell *gc, return (0); id |= values[2] << 24; } + + /* Resolve the application image ID and its virtual placement. */ source = kitty_source_find(kc, id); if (source == NULL) return (0); @@ -1560,9 +1564,9 @@ kitty_placeholder_to_image(void *state, struct grid *gd, struct grid_cell *gc, placement = placement->next) { if (!placement->virtual) continue; - if (placement_id != 0) { - if (placement->placement_id != placement_id) - continue; + if (placement_id != 0 && + placement->placement_id != placement_id) { + continue; } break; } diff --git a/image-sixel.c b/image-sixel.c index 6186ea275..9a975056e 100644 --- a/image-sixel.c +++ b/image-sixel.c @@ -74,13 +74,8 @@ struct sixel_image { struct sixel_line *lines; }; +/* One colour's encoded columns and controls in the current six-row band. */ struct sixel_plane { - /* - * A sixel_plane represents a single-colour SIXEL matrix for - * the current six-row band. stream contains the six-bit columns - * and sixel controls such as repeats and gaps for this colour. - */ - /* Position of the next encoded colour plane. */ u_int next_x; u_int next_y; @@ -109,17 +104,14 @@ struct sixel_image_cache { struct sixel_image_cache *next; }; -/* - * Contiguous rows of one placement, held back so that they can be written as - * a single SIXEL instead of one per row. See sixel_draw_rect. - */ +/* Adjacent placement rows buffered for one SIXEL output sequence. */ struct sixel_pending { struct image *image; - u_int source_x; + u_int source_x; /* Origin in image cells. */ u_int source_y; - u_int width; + u_int width; /* Size in cells. */ u_int height; - u_int destination_x; + u_int destination_x; /* Origin in terminal cells. */ u_int destination_y; }; @@ -264,6 +256,7 @@ sixel_parse_attributes(struct sixel_image *si, const char *cp, const char *end) char *endptr; u_int x, y; + /* Skip the aspect ratio, then read the optional raster dimensions. */ last = cp; while (last != end) { if (*last != ';' && (*last < '0' || *last > '9')) @@ -318,6 +311,7 @@ sixel_parse_colour(struct sixel_image *si, const char *cp, const char *end) char *endptr; u_int c, type, c1, c2, c3; + /* Select the register before reading an optional colour definition. */ last = cp; while (last != end) { if (*last != ';' && (*last < '0' || *last > '9')) @@ -357,6 +351,7 @@ sixel_parse_colour(struct sixel_image *si, const char *cp, const char *end) return (NULL); } + /* Validate HLS or RGB components before extending the palette. */ if ((type != 1 && type != 2) || (type == 1 && (c1 > 360 || c2 > 100 || c3 > 100)) || (type == 2 && (c1 > 100 || c2 > 100 || c3 > 100))) { @@ -419,7 +414,7 @@ sixel_parse_repeat(struct sixel_image *si, const char *cp, const char *end) /* Parse SIXEL data into an indexed image. */ struct sixel_image * sixel_parse(const char *buf, size_t len, u_int p1, u_int p2, u_int cell_w, - u_int cell_h) + u_int cell_h) { struct sixel_image *si; const char *cp = buf, *end = buf + len; @@ -430,12 +425,13 @@ sixel_parse(const char *buf, size_t len, u_int p1, u_int p2, u_int cell_w, return (NULL); } - si = xcalloc (1, sizeof *si); + si = xcalloc(1, sizeof *si); si->cell_w = cell_w; si->cell_h = cell_h; si->p1 = p1; si->p2 = p2; + /* Decode controls and six-pixel columns into indexed rows. */ while (cp != end) { ch = *cp++; switch (ch) { @@ -590,12 +586,14 @@ sixel_colour_to_rgb(u_int colour, u_char *r, u_char *g, u_char *b) struct image * sixel_to_image(struct sixel_image *si) { - u_char *pixels, *pixel, r, g, b; - u_int x, y, c, sx, sy; struct image *im; + u_char *pixels, *pixel, r, g, b; + u_int x, y, c, sx, sy; if ((uint64_t)si->sx * si->sy > IMAGE_SIZE_LIMIT / 4) return (NULL); + + /* Convert palette indexes to shared RGBA pixels. */ pixels = xcalloc((size_t)si->sx * si->sy, 4); for (y = 0; y < si->sy; y++) { for (x = 0; x < si->sx; x++) { @@ -616,6 +614,7 @@ sixel_to_image(struct sixel_image *si) pixel[3] = 255; } } + /* Keep transparent cell padding in the logical canvas. */ sixel_size_in_cells(si, &sx, &sy); if ((uint64_t)sx * si->cell_w > UINT_MAX || (uint64_t)sy * si->cell_h > UINT_MAX) { @@ -639,19 +638,15 @@ sixel_scale(struct sixel_image *si, u_int cell_w, u_int cell_h, u_int ox, struct sixel_image *new; u_int cx, cy, raster_sx, raster_sy; u_int pox, poy, psx, psy, tsx, tsy, px, py; - uint64_t source_left, source_right, source_top, source_bottom; - uint64_t target_left, target_right, target_top, target_bottom; + uint64_t source_left, source_right; + uint64_t source_top, source_bottom; + uint64_t target_left, target_right; + uint64_t target_top, target_bottom; u_int x, y, i; - /* - * We want to get the section of the image at ox,oy in image cells and - * map it onto the same size in terminal cells. - */ - + /* Clip image cells before mapping them to terminal cells. */ sixel_size_in_cells(si, &cx, &cy); - if (ox >= cx) - return (NULL); - if (oy >= cy) + if (ox >= cx || oy >= cy) return (NULL); if (ox + sx >= cx) sx = cx - ox; @@ -669,11 +664,7 @@ sixel_scale(struct sixel_image *si, u_int cell_w, u_int cell_h, u_int ox, if (raster_sy > si->sy) raster_sy = si->sy; - /* - * Map complete source cells at their real pixel boundaries and clamp - * only the final partial cell to the raster. Dividing the raster evenly - * between cells would stretch every complete cell and squash the last. - */ + /* Map whole cells at pixel boundaries and clip the final cell. */ source_left = (uint64_t)ox * si->cell_w; source_right = (uint64_t)(ox + sx) * si->cell_w; source_top = (uint64_t)oy * si->cell_h; @@ -689,11 +680,7 @@ sixel_scale(struct sixel_image *si, u_int cell_w, u_int cell_h, u_int ox, psx = source_right - source_left; psy = source_bottom - source_top; - /* - * Preserve any partial final source cell. The grid still covers whole - * cells, but the SIXEL raster must end at the corresponding pixel offset - * rather than stretching to the cell boundary. - */ + /* Preserve the pixel extent of a partial final cell. */ target_right = ((uint64_t)raster_sx * cell_w + si->cell_w - 1) / si->cell_w; target_bottom = ((uint64_t)raster_sy * cell_h + si->cell_h - 1) / @@ -713,7 +700,8 @@ sixel_scale(struct sixel_image *si, u_int cell_w, u_int cell_h, u_int ox, if (tsx == 0 || tsy == 0) return (NULL); - new = xcalloc (1, sizeof *si); + /* Resample the crop and preserve its palette and raster attributes. */ + new = xcalloc(1, sizeof *si); new->cell_w = cell_w; new->cell_h = cell_h; new->p1 = si->p1; @@ -770,10 +758,11 @@ sixel_fit(struct sixel_image *si, u_int cell_w, u_int cell_h, u_int cells_x, canvas_height = (uint64_t)cells_y * cell_h; if (si->sx == 0 || si->sy == 0 || canvas_width == 0 || canvas_height == 0 || canvas_width > SIXEL_WIDTH_LIMIT || - canvas_height > SIXEL_HEIGHT_LIMIT) + canvas_height > SIXEL_HEIGHT_LIMIT) { return (NULL); + } - /* Use one scale factor so different terminal cell shapes do not distort. */ + /* Use one scale factor to preserve the raster's aspect ratio. */ if ((uint64_t)cell_w * si->cell_h <= (uint64_t)cell_h * si->cell_w) { width = ((uint64_t)si->sx * cell_w + si->cell_w / 2) / @@ -795,6 +784,7 @@ sixel_fit(struct sixel_image *si, u_int cell_w, u_int cell_h, u_int cells_x, if (height > canvas_height) height = canvas_height; + /* Resample the raster and pad the unused cell canvas. */ new = xcalloc(1, sizeof *new); new->cell_w = cell_w; new->cell_h = cell_h; @@ -875,6 +865,7 @@ sixel_print_compress_colors(struct sixel_image *si, struct sixel_plane *planes, struct sixel_line *sl; for (x = 0; x < si->sx; x++) { + /* Collect this column's six-bit pattern for each colour. */ for (i = 0; i < 6; i++) { pixels[i] = 0; if (y + i < si->sy) { @@ -887,6 +878,7 @@ sixel_print_compress_colors(struct sixel_image *si, struct sixel_plane *planes, } } + /* Compress patterns and gaps in active colour planes. */ for (i = 0; i < 6; i++) { if (pixels[i] == 0) continue; @@ -903,11 +895,13 @@ sixel_print_compress_colors(struct sixel_image *si, struct sixel_plane *planes, dx = x - plane->next_x; if (plane->pattern != plane->next_pattern || dx != 0) { - sixel_print_repeat(&plane->stream, &plane->stream_len, - &plane->stream_used, plane->count, + sixel_print_repeat(&plane->stream, + &plane->stream_len, &plane->stream_used, + plane->count, plane->pattern + 0x3f); - sixel_print_repeat(&plane->stream, &plane->stream_len, - &plane->stream_used, dx, '?'); + sixel_print_repeat(&plane->stream, + &plane->stream_len, &plane->stream_used, + dx, '?'); plane->pattern = plane->next_pattern; plane->count = 0; } @@ -952,14 +946,11 @@ sixel_print(struct sixel_image *si, struct sixel_image *map, size_t *size) sixel_print_add(&buf, &len, &used, tmp, tmplen); } - /* The colour panes in the current sixel-row band. */ + /* Track colour planes containing pixels in the current six-row band. */ planes = xcalloc(used_colours, sizeof *planes); - /* - * active records which colour planes actually contain pixels - * in the current sixel-row band. - */ active = xcalloc(used_colours, sizeof *active); + /* Emit the palette before the encoded colour planes. */ for (i = 0; i < ncolours; i++) { c = colours[i]; tmplen = xsnprintf(tmp, sizeof tmp, "#%u;%u;%u;%u;%u", @@ -973,6 +964,7 @@ sixel_print(struct sixel_image *si, struct sixel_image *map, size_t *size) plane->stream = xmalloc(plane->stream_len); } + /* Encode each six-row band using only its active colours. */ for (y = 0; y < si->sy; y += 6) { nactive = 0; sixel_print_compress_colors(si, planes, y, active, &nactive); @@ -1011,7 +1003,7 @@ sixel_print(struct sixel_image *si, struct sixel_image *map, size_t *size) return (buf); } -/* Split a 5-bit RGB histogram into an adaptive palette using median cut. */ +/* Update the occupied bounds and pixel count of a colour region. */ static void sixel_box_update(struct sixel_box *box, struct sixel_hgram *hg) { @@ -1025,7 +1017,8 @@ sixel_box_update(struct sixel_box *box, struct sixel_hgram *hg) for (red = box->red_min; red <= box->red_max; red++) { for (green = box->green_min; green <= box->green_max; green++) { - for (blue = box->blue_min; blue <= box->blue_max; blue++) { + for (blue = box->blue_min; blue <= box->blue_max; + blue++) { index = (red << 10)|(green << 5)|blue; entry = &hg[index]; if (entry->count == 0) @@ -1066,6 +1059,7 @@ sixel_box_split(struct sixel_box *box, struct sixel_box *new, u_int red, green, blue, index, channel, first, last, level; u_int red_range, green_range, blue_range, count = 0; + /* Split along the channel with the widest occupied range. */ red_range = box->red_max - box->red_min; green_range = box->green_max - box->green_min; blue_range = box->blue_max - box->blue_min; @@ -1080,7 +1074,8 @@ sixel_box_split(struct sixel_box *box, struct sixel_box *new, for (red = box->red_min; red <= box->red_max; red++) { for (green = box->green_min; green <= box->green_max; green++) { - for (blue = box->blue_min; blue <= box->blue_max; blue++) { + for (blue = box->blue_min; blue <= box->blue_max; + blue++) { index = (red << 10)|(green << 5)|blue; if (channel == 0) levels[red] += hg[index].count; @@ -1101,6 +1096,7 @@ sixel_box_split(struct sixel_box *box, struct sixel_box *new, first = box->blue_min; last = box->blue_max; } + /* Find the weighted median without emptying either resulting box. */ for (level = first; level < last; level++) { count += levels[level]; if (count >= box->count / 2) @@ -1132,9 +1128,9 @@ sixel_make_palette(struct sixel_hgram *hg, { struct sixel_box boxes[SIXEL_PALETTE_SIZE], new; struct sixel_box *box; - uint64_t best_score, score, red, green, blue, count; - u_int i, nboxes = 1, best, r, g, b, index; - u_int red_range, green_range, blue_range; + uint64_t best_score, score, red, green, blue, count; + u_int i, nboxes = 1, best, r, g, b, index; + u_int red_range, green_range, blue_range; memset(&boxes[0], 0, sizeof boxes[0]); boxes[0].red_max = boxes[0].green_max = boxes[0].blue_max = @@ -1143,6 +1139,7 @@ sixel_make_palette(struct sixel_hgram *hg, if (boxes[0].count == 0) return (0); + /* Repeatedly split the most populated and varied colour region. */ while (nboxes < SIXEL_PALETTE_SIZE) { best = nboxes; best_score = 0; @@ -1160,17 +1157,20 @@ sixel_make_palette(struct sixel_hgram *hg, } } if (best == nboxes || - !sixel_box_split(&boxes[best], &new, hg)) + !sixel_box_split(&boxes[best], &new, hg)) { break; + } memcpy(&boxes[nboxes++], &new, sizeof new); } + /* Average each region's colours into one palette entry. */ for (i = 0; i < nboxes; i++) { box = &boxes[i]; red = green = blue = count = 0; for (r = box->red_min; r <= box->red_max; r++) { for (g = box->green_min; g <= box->green_max; g++) { - for (b = box->blue_min; b <= box->blue_max; b++) { + for (b = box->blue_min; b <= box->blue_max; + b++) { index = (r << 10)|(g << 5)|b; red += hg[index].red; green += hg[index].green; @@ -1243,7 +1243,7 @@ sixel_from_image_pixel(const struct sixel_source *source, u_int source_x, /* Render an image rectangle as an indexed SIXEL image. */ static struct sixel_image * sixel_from_image(struct image *im, u_int cell_x, u_int cell_y, u_int cells_x, - u_int cells_y, u_int cell_w, u_int cell_h) + u_int cells_y, u_int cell_w, u_int cell_h) { struct sixel_image *si; struct sixel_hgram *hg, *entry; @@ -1252,9 +1252,11 @@ sixel_from_image(struct image *im, u_int cell_x, u_int cell_y, u_int cells_x, const u_char *pixel; uint16_t *cache; int *current, *next, *tmp; - int red_error, green_error, blue_error, alpha_error; + int red_error, green_error, blue_error; + int alpha_error; u_int x, y, sx, sy, index, error_index; - u_int source_x, source_y, source_width, source_height; + u_int source_x, source_y; + u_int source_width, source_height; u_int red, green, blue, alpha, colour, i, ncolours; uint64_t destination_width, destination_height; uint64_t content_width, content_height; @@ -1275,7 +1277,7 @@ sixel_from_image(struct image *im, u_int cell_x, u_int cell_y, u_int cells_x, content_height = ((uint64_t)source.height * destination_height + source.canvas_height - 1) / source.canvas_height; - /* Convert the requested cell rectangle to clipped output pixel bounds. */ + /* Clip the requested cell rectangle to output pixel bounds. */ left = (uint64_t)cell_x * cell_w; top = (uint64_t)cell_y * cell_h; right = ((uint64_t)cell_x + cells_x) * cell_w; @@ -1291,8 +1293,9 @@ sixel_from_image(struct image *im, u_int cell_x, u_int cell_y, u_int cells_x, sx = right - left; sy = bottom - top; if (sx == 0 || sy == 0 || sx > SIXEL_WIDTH_LIMIT || - sy > SIXEL_HEIGHT_LIMIT) + sy > SIXEL_HEIGHT_LIMIT) { return (NULL); + } /* Map the requested cell crop to the source image's pixel rectangle. */ image_get_pixel_rect(im, cell_x, cell_y, cells_x, cells_y, &source_x, @@ -1304,12 +1307,13 @@ sixel_from_image(struct image *im, u_int cell_x, u_int cell_y, u_int cells_x, hg = xcalloc(SIXEL_HISTOGRAM_SIZE, sizeof *hg); for (y = 0; y < sy; y++) { for (x = 0; x < sx; x++) { - pixel = sixel_from_image_pixel(&source, source_x, source_y, - source_width, source_height, sx, sy, x, y); + pixel = sixel_from_image_pixel(&source, source_x, + source_y, source_width, source_height, sx, sy, + x, y); if (pixel[3] == 0) continue; - /* Add this opaque pixel to its 5-bit RGB histogram bucket. */ + /* Count visible pixels in their RGB histogram bin. */ index = ((pixel[0] >> 3) << 10)| ((pixel[1] >> 3) << 5)|(pixel[2] >> 3); entry = &hg[index]; @@ -1324,7 +1328,7 @@ sixel_from_image(struct image *im, u_int cell_x, u_int cell_y, u_int cells_x, if (ncolours == 0) return (NULL); - /* Create the indexed SIXEL image and convert its palette to SIXEL RGB. */ + /* Create the indexed image with a SIXEL RGB palette. */ si = xcalloc(1, sizeof *si); si->cell_w = cell_w; si->cell_h = cell_h; @@ -1349,10 +1353,12 @@ sixel_from_image(struct image *im, u_int cell_x, u_int cell_y, u_int cells_x, next = xcalloc(((size_t)sx + 2) * 4, sizeof *next); for (y = 0; y < sy; y++) { for (x = 0; x < sx; x++) { - pixel = sixel_from_image_pixel(&source, source_x, source_y, - source_width, source_height, sx, sy, x, y); + pixel = sixel_from_image_pixel(&source, source_x, + source_y, source_width, source_height, sx, sy, + x, y); error_index = (x + 1) * 4; - /* SIXEL pixels are binary, so dither alpha separately. */ + + /* Dither alpha to SIXEL's binary transparency. */ alpha = sixel_clamp_colour((int)pixel[3] + current[error_index + 3] / 16); alpha_error = (int)alpha; @@ -1364,19 +1370,18 @@ sixel_from_image(struct image *im, u_int cell_x, u_int cell_y, u_int cells_x, current[error_index + 1] / 16); blue = sixel_clamp_colour((int)pixel[2] + current[error_index + 2] / 16); - colour = sixel_nearest_colour(palette, ncolours, cache, - red, green, blue); + colour = sixel_nearest_colour(palette, ncolours, + cache, red, green, blue); if (sixel_set_pixel(si, x, y, colour + 1) != 0) goto fail; - /* Calculate the RGB error introduced by palette quantization. */ + /* Find the colour error. */ red_error = (int)red - palette[colour].red; - green_error = (int)green - palette[colour].green; + green_error = (int)green - + palette[colour].green; blue_error = (int)blue - palette[colour].blue; - /* - * Diffuse the error with the Floyd-Steinberg 7/16, 3/16, - * 5/16, 1/16 kernel; the accumulated error is divided by 16. - */ + + /* Diffuse error with the 7:3:5:1 kernel. */ current[error_index + 4] += red_error * 7; current[error_index + 5] += green_error * 7; current[error_index + 6] += blue_error * 7; @@ -1411,7 +1416,7 @@ sixel_from_image(struct image *im, u_int cell_x, u_int cell_y, u_int cells_x, return (si); fail: - /* Discard a partially built image after an allocation or size failure. */ + /* Discard the partially built image after a size failure. */ free(current); free(next); free(cache); @@ -1484,7 +1489,7 @@ sixel_free_output(struct tty *tty, __unused int send) if (so == NULL) return; - /* The run is dropped, not written: the geometry it was measured at is gone. */ + /* Discard pending rows measured at the old terminal geometry. */ so->pending.image = NULL; for (cache = so->images; cache != NULL; cache = next) { next = cache->next; @@ -1503,7 +1508,7 @@ sixel_render_image(struct image *im, u_int cell_w, u_int cell_h) u_int sx, sy; image_get_size_in_cells(im, &sx, &sy); - /* Preserve SIXEL's original palette and indexed pixels when possible. */ + /* Preserve the original SIXEL palette and pixels when possible. */ original = image_get_sixel(im); if (original != NULL) si = sixel_fit(original, cell_w, cell_h, sx, sy); @@ -1519,14 +1524,15 @@ sixel_get_image(struct tty *tty, struct image *im) struct sixel_output *so = sixel_get_output(tty); struct sixel_image_cache **pp, *cache, **oldest; struct sixel_image *si; - size_t size; + size_t size; sixel_collect_images(so); for (cache = so->images; cache != NULL; cache = cache->next) { if (cache->server_id != image_get_id(im) || cache->cell_w != tty->xpixel || - cache->cell_h != tty->ypixel) + cache->cell_h != tty->ypixel) { continue; + } cache->age = ++so->age; return (cache->si); } @@ -1536,7 +1542,7 @@ sixel_get_image(struct tty *tty, struct image *im) return (NULL); size = sixel_image_size(si); if (size == 0 || size > IMAGE_SIZE_LIMIT) { - /* The renderer still has a usable image, but it is not cacheable. */ + /* Use the rendered image without caching it. */ return (si); } while (so->size > IMAGE_SIZE_LIMIT - size) { @@ -1617,14 +1623,7 @@ sixel_flush_output(struct tty *tty) free(data); } -/* - * Queue an image rectangle for SIXEL output. The redraw loop hands images to - * the backend one grid line at a time, so hold back a run of vertically - * adjacent rows and write them as one SIXEL instead of one per row. - * Anything that is not a continuation flushes the run first, and - * image_draw_flush() flushes what's left at the end of the redraw, so no - * other terminal output is reordered across a pending run. - */ +/* Queue adjacent placement rows for one SIXEL output sequence. */ void sixel_draw_rect(struct tty *tty, const struct image_rect *rectangle) { @@ -1647,6 +1646,7 @@ sixel_draw_rect(struct tty *tty, const struct image_rect *rectangle) return; } + /* Flush a completed run before queuing a different rectangle. */ sixel_flush_output(tty); sp->image = im; sp->source_x = source_x; diff --git a/image.c b/image.c index 0b4735404..e135fb9e9 100644 --- a/image.c +++ b/image.c @@ -35,19 +35,19 @@ struct image_rect { struct image *image; int32_t z; - u_int source_x; + u_int source_x; /* Origin in image cells. */ u_int source_y; - u_int sx; + u_int sx; /* Size in cells. */ u_int sy; - u_int destination_x; + u_int destination_x; /* Origin in terminal cells. */ u_int destination_y; }; /* One contiguous row of a placement in the grid. */ struct image_span { - u_int x; - u_int sx; - u_int source_x; + u_int x; /* Owning grid column. */ + u_int sx; /* Number of image cells. */ + u_int source_x; /* Origin in image cells. */ u_int source_y; struct image_line *line; struct image_placement *placement; @@ -73,11 +73,11 @@ struct image_placement { struct image_store *store; struct image *image; enum image_input input; - int placeholder; + int placeholder; /* Uses Unicode placeholders. */ u_int app_image_id; u_int app_placement_id; int32_t z; - uint64_t serial; + uint64_t serial; /* Layer order. */ struct image_spans spans; TAILQ_ENTRY(image_placement) entry; }; @@ -89,23 +89,26 @@ struct image_store { struct image_placements placements; }; +/* Spans saved while overlapping grid cells are moved. */ struct image_move { struct image_placement *placement; - u_int x; - u_int sx; - u_int source_x; + u_int x; /* Destination grid column. */ + u_int sx; /* Number of image cells. */ + u_int source_x; /* Origin in image cells. */ u_int source_y; }; +/* Pair source and destination placements during one grid copy. */ struct image_placement_map { struct image_placement *source; struct image_placement *destination; }; +/* Placement mappings kept only for the duration of a grid copy. */ struct image_copy_ctx { - struct grid *destination; - struct image_placement_map *maps; - size_t count; + struct grid *destination; + struct image_placement_map *maps; + size_t count; /* Placement mapping count. */ }; static struct images images = RB_INITIALIZER(&images); @@ -138,8 +141,9 @@ image_tty_find_backend(struct tty *tty) if (tty->term != NULL && tty->term->flags & TERM_KITTY) return (&image_backend_kitty); if (tty->term != NULL && tty->term->flags & TERM_SIXEL && - tty->xpixel != 0 && tty->ypixel != 0) + tty->xpixel != 0 && tty->ypixel != 0) { return (&image_backend_sixel); + } return (&image_backend_fallback); } @@ -203,8 +207,9 @@ image_backend_flags(struct tty *tty) /* Image scrolling is granted separately from text margin support. */ if ((tty->image_backend == &image_backend_sixel || tty->image_backend == &image_backend_kitty) && - (tty->term->flags & TERM_IMAGESCROLL)) + (tty->term->flags & TERM_IMAGESCROLL)) { flags |= IMAGE_BACKEND_SCROLLS; + } return (flags); } @@ -265,8 +270,9 @@ image_placement_cmp(const struct image_placement *a, if (a->input == IMAGE_INPUT_KITTY && a->z != b->z) return (a->z < b->z ? -1 : 1); if (a->input == IMAGE_INPUT_KITTY && - a->app_image_id != b->app_image_id) + a->app_image_id != b->app_image_id) { return (a->app_image_id < b->app_image_id ? -1 : 1); + } if (a->serial != b->serial) return (a->serial < b->serial ? -1 : 1); return (0); @@ -302,8 +308,9 @@ image_line_get(struct grid_line *gl) /* Create a logical image placement. */ static struct image_placement * -image_placement_create(struct grid *gd, struct image *im, enum image_input input, - u_int app_image_id, u_int app_placement_id, int32_t z) +image_placement_create(struct grid *gd, struct image *im, + enum image_input input, u_int app_image_id, u_int app_placement_id, + int32_t z) { struct image_store *store = image_store_get(gd); struct image_placement *placement; @@ -373,10 +380,7 @@ image_span_free(struct image_span *span) free(span); } -/* - * Remove a range from selected spans on a line - those of one placement if - * only is not NULL, otherwise those of the given input type. - */ +/* Remove a cell range from one placement or the selected input type. */ static void image_line_remove(struct image_line *line, u_int x, u_int width, enum image_input input, @@ -394,9 +398,10 @@ image_line_remove(struct image_line *line, u_int x, u_int width, if (only != NULL && span->placement != only) continue; if (input == IMAGE_INPUT_OVERWRITTEN) { - if (span->placement->input != IMAGE_INPUT_SIXEL) { - if (!span->placement->placeholder) - continue; + /* Keep ordinary Kitty placements on text writes. */ + if (span->placement->input != IMAGE_INPUT_SIXEL && + !span->placement->placeholder) { + continue; } } else if (input != IMAGE_INPUT_ALL) { if (span->placement->input != input) @@ -405,6 +410,8 @@ image_line_remove(struct image_line *line, u_int x, u_int width, span_end = span->x + span->sx; if (span_end <= x || span->x >= end) continue; + + /* Preserve pieces on either side of the removed cell range. */ if (span->x < x && span_end > end) { right = span_end - end; span->sx = x - span->x; @@ -453,9 +460,10 @@ image_grid_remove_overwritten_spans(struct grid *gd, u_int x, u_int y, return; if (height > gd->hsize + gd->sy - y) height = gd->hsize + gd->sy - y; - for (row = y; row < y + height; row++) + for (row = y; row < y + height; row++) { image_line_remove(gd->linedata[row].images, x, width, IMAGE_INPUT_OVERWRITTEN, NULL); + } image_store_prune(gd->images); } @@ -500,11 +508,14 @@ image_grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py, u_int start, end, span_end; if (gd->images == NULL || nx == 0 || px == dx || - py >= gd->hsize + gd->sy) + py >= gd->hsize + gd->sy) { return; + } line = gd->linedata[py].images; if (line == NULL) return; + + /* Save source spans before removing the overlapping cell ranges. */ end = px + nx; TAILQ_FOREACH(span, &line->spans, line_entry) { span_end = span->x + span->sx; @@ -521,11 +532,14 @@ image_grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py, moves[count].source_y = span->source_y; count++; } + + /* Replace both ranges and restore spans at their new columns. */ image_line_remove(line, px, nx, IMAGE_INPUT_ALL, NULL); image_line_remove(line, dx, nx, IMAGE_INPUT_ALL, NULL); - for (i = 0; i < count; i++) + for (i = 0; i < count; i++) { image_span_add(line, moves[i].placement, moves[i].x, moves[i].sx, moves[i].source_x, moves[i].source_y); + } free(moves); image_store_prune(gd->images); } @@ -594,8 +608,9 @@ image_grid_copy_area(struct grid *dst, u_int destination_x, if (dst == src || sx == 0 || sy == 0) return; if (destination_y >= dst->hsize + dst->sy || - source_y >= src->hsize + src->sy) + source_y >= src->hsize + src->sy) { return; + } if (sy > dst->hsize + dst->sy - destination_y) sy = dst->hsize + dst->sy - destination_y; if (sy > src->hsize + src->sy - source_y) @@ -604,6 +619,7 @@ image_grid_copy_area(struct grid *dst, u_int destination_x, if (end < source_x) end = UINT_MAX; + /* Copy clipped spans, sharing one destination placement per source. */ for (row = 0; row < sy; row++) { source_line = src->linedata[source_y + row].images; if (source_line == NULL) @@ -644,8 +660,9 @@ image_grid_area_has_images(struct grid *gd, u_int x, u_int y, u_int width, u_int row, end; if (gd->images == NULL || width == 0 || height == 0 || - y >= gd->hsize + gd->sy) + y >= gd->hsize + gd->sy) { return (0); + } end = x + width; if (height > gd->hsize + gd->sy - y) height = gd->hsize + gd->sy - y; @@ -670,8 +687,9 @@ image_grid_get_placeholder(struct grid *gd, u_int x, u_int y, struct image_span *span, *found = NULL; if (y >= gd->hsize + gd->sy || - (line = gd->linedata[y].images) == NULL) + (line = gd->linedata[y].images) == NULL) { return (0); + } TAILQ_FOREACH(span, &line->spans, line_entry) { if (!span->placement->placeholder) continue; @@ -703,6 +721,7 @@ image_place_cell_kitty(struct screen_write_ctx *ctx, struct image *im, struct image_line *line; struct image_span *span; + /* Reuse a matching placeholder placement in this grid. */ TAILQ_FOREACH_REVERSE(candidate, &store->placements, image_placements, entry) { if (!candidate->placeholder) @@ -721,6 +740,7 @@ image_place_cell_kitty(struct screen_write_ctx *ctx, struct image *im, image_id, placement_id, z); placement->placeholder = 1; } + /* Extend an adjacent source run or add a new span for this cell. */ line = image_line_get(&gd->linedata[gd->hsize + y]); TAILQ_FOREACH(span, &line->spans, line_entry) { if (span->placement == placement && span->x + span->sx == x && @@ -745,6 +765,7 @@ 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; + /* 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; @@ -763,6 +784,7 @@ image_sample(struct image *im, uint64_t sample_x, uint64_t sample_y, if (y1 > im->height) y1 = im->height; + /* 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; @@ -789,7 +811,7 @@ static void image_make_cells(struct image *im) { struct image_cell *cell; - uint64_t columns, rows; + uint64_t columns, rows, column, row; u_int x, y, sample_x, sample_y; columns = (uint64_t)im->sx * IMAGE_SAMPLE_COLUMNS; @@ -799,14 +821,15 @@ image_make_cells(struct image *im) 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, - (uint64_t)x * IMAGE_SAMPLE_COLUMNS + sample_x, - (uint64_t)y * IMAGE_SAMPLE_ROWS + sample_y, - columns, rows, + sample_x < IMAGE_SAMPLE_COLUMNS; + sample_x++) { + image_sample(im, column + sample_x, + row + sample_y, columns, rows, &cell->samples[sample_y][sample_x]); } } @@ -964,13 +987,15 @@ image_create(u_int width, u_int height, u_int canvas_width, struct image *im; if (width == 0 || height == 0 || canvas_width < width || - canvas_height < height || sx == 0 || sy == 0 || pixels == NULL) + canvas_height < height || sx == 0 || sy == 0 || pixels == NULL) { return (NULL); + } if ((uint64_t)width * height > SIZE_MAX / 4) return (NULL); if ((uint64_t)sx * sy > SIZE_MAX / sizeof *im->cells || - sx > USHRT_MAX || sy > USHRT_MAX) + sx > USHRT_MAX || sy > USHRT_MAX) { return (NULL); + } if ((uint64_t)sx * sy > IMAGE_SIZE_LIMIT / sizeof *im->cells) return (NULL); im = image_alloc(width, height, canvas_width, canvas_height, sx, sy, @@ -989,66 +1014,57 @@ image_create_view(struct image *source, const struct image_view *view) u_int padded_width, padded_height, x, y, source_x, source_y; int share_pixels = 1; + /* Validate the source crop and placement dimensions. */ if (source == NULL) return (NULL); - if (view->x >= source->width) + if (view->x >= source->width || view->y >= source->height) return (NULL); - if (view->y >= source->height) + if (view->width == 0 || view->height == 0 || view->scaled_width == 0 || + view->scaled_height == 0 || view->sx == 0 || view->sy == 0) { return (NULL); - if (view->width == 0) + } + if (view->width > source->width - view->x || + view->height > source->height - view->y) { return (NULL); - if (view->width > source->width - view->x) - return (NULL); - if (view->height == 0) - return (NULL); - if (view->height > source->height - view->y) - return (NULL); - if (view->scaled_width == 0) - return (NULL); - if (view->scaled_height == 0) - return (NULL); - if (view->sx == 0) - return (NULL); - if (view->sy == 0) - return (NULL); - if (view->sx > USHRT_MAX) - return (NULL); - if (view->sy > USHRT_MAX) + } + if (view->sx > USHRT_MAX || view->sy > USHRT_MAX) return (NULL); if ((uint64_t)view->sx * view->sy > SIZE_MAX / sizeof *im->cells) return (NULL); if ((uint64_t)view->sx * view->sy > - IMAGE_SIZE_LIMIT / sizeof *im->cells) + IMAGE_SIZE_LIMIT / sizeof *im->cells) { return (NULL); - if (view->x_offset > UINT_MAX - view->scaled_width) - return (NULL); - if (view->y_offset > UINT_MAX - view->scaled_height) + } + + /* Keep the padded raster inside the logical canvas. */ + if (view->x_offset > UINT_MAX - view->scaled_width || + view->y_offset > UINT_MAX - view->scaled_height) { return (NULL); + } padded_width = view->scaled_width + view->x_offset; padded_height = view->scaled_height + view->y_offset; - if (padded_width > view->canvas_width) - return (NULL); - if (padded_height > view->canvas_height) + if (padded_width > view->canvas_width || + padded_height > view->canvas_height) { return (NULL); + } /* Share pixels when no scaling or padding is needed. */ - if (view->scaled_width != view->width) - share_pixels = 0; - if (view->scaled_height != view->height) - share_pixels = 0; - if (view->x_offset != 0) - share_pixels = 0; - if (view->y_offset != 0) + if (view->scaled_width != view->width || + view->scaled_height != view->height || + view->x_offset != 0 || view->y_offset != 0) { share_pixels = 0; + } if (share_pixels) { im = image_alloc(view->width, view->height, view->canvas_width, view->canvas_height, view->sx, view->sy, source->stride, source->pixels + (size_t)view->y * source->stride + (size_t)view->x * 4); } else { + /* Resample the crop with transparent padding. */ if ((uint64_t)padded_width * padded_height > - IMAGE_SIZE_LIMIT / 4) + IMAGE_SIZE_LIMIT / 4) { return (NULL); + } pixels = xcalloc((size_t)padded_width * padded_height, 4); for (y = 0; y < view->scaled_height; y++) { source_y = view->y + (uint64_t)y * view->height / @@ -1076,6 +1092,8 @@ image_create_view(struct image *source, const struct image_view *view) (size_t)padded_width * 4, pixels); im->flags |= IMAGE_FLAG_OWN_PIXELS; } + + /* Retain the source for both shared pixels and placement identity. */ im->parent_id = source->id; im->source_id = source->source_id; image_ref(source->id); @@ -1140,8 +1158,9 @@ image_get_fallback_at(struct tty *tty, struct screen *s, u_int x, u_int y, struct image_placement *placement; if (image_backend_flags(tty) & IMAGE_BACKEND_GRAPHICAL || - y >= s->grid->sy) + y >= s->grid->sy) { return (0); + } line = s->grid->linedata[s->grid->hsize + y].images; if (line == NULL) return (0); @@ -1160,8 +1179,9 @@ image_get_fallback_at(struct tty *tty, struct screen *s, u_int x, u_int y, return (-1); } if (placement->z < IMAGE_Z_BELOW_BACKGROUND && - !COLOUR_DEFAULT(gc->bg)) + !COLOUR_DEFAULT(gc->bg)) { return (-1); + } } image_get_fallback_cell(tty, placement->image, found->source_x + x - found->x, found->source_y, gc, out); @@ -1178,8 +1198,9 @@ image_get_pixel_rect(const struct image *im, u_int x, u_int y, *px = *py = *pwidth = *pheight = 0; if (im == NULL || x >= im->sx || y >= im->sy || width == 0 || - height == 0) + height == 0) { return; + } if (width > im->sx - x) width = im->sx - x; if (height > im->sy - y) @@ -1245,7 +1266,10 @@ image_base64_decode(const char *data, size_t len, size_t limit, size_t *size) memcpy(copy, data, len); memset(copy + len, '=', padding); copy[padded] = '\0'; - out = xmalloc(needed == 0 ? 1 : needed); + if (needed == 0) + out = xmalloc(1); + else + out = xmalloc(needed); result = b64_pton(copy, out, needed); free(copy); if (result < 0) { @@ -1472,9 +1496,10 @@ image_redraw_area(struct screen_write_ctx *ctx, u_int px, u_int py, u_int nx, if (wp == NULL) return; - if (!image_grid_area_has_images(ctx->s->grid, px, ctx->s->grid->hsize + py, - nx, ny)) + if (!image_grid_area_has_images(ctx->s->grid, px, + ctx->s->grid->hsize + py, nx, ny)) { return; + } redraw_damage_window(wp->window, wp->xoff + px, wp->yoff + py, nx, ny); } @@ -1497,8 +1522,9 @@ image_redraw_scroll(struct screen_write_ctx *ctx) if (wp == NULL) return; if (!image_grid_area_has_images(s->grid, 0, s->grid->hsize + s->rupper, - screen_size_x(s), s->rlower - s->rupper + 1)) + screen_size_x(s), s->rlower - s->rupper + 1)) { return; + } redraw_damage_window_scroll(wp->window, wp->xoff, wp->yoff + s->rupper, screen_size_x(s), s->rlower - s->rupper + 1); @@ -1568,6 +1594,8 @@ image_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, TAILQ_FOREACH(span, &line->spans, line_entry) { placement = span->placement; blank_only = 0; + + /* Emulate Kitty's negative layers with SIXEL. */ if (placement->input == IMAGE_INPUT_KITTY && placement->z < 0) { if (backend == &image_backend_sixel && @@ -1590,6 +1618,7 @@ image_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, px, atx, aty); continue; } + /* Draw below-text SIXEL layers only on cells without text. */ while (start < span_end) { while (start < span_end && image_cell_has_text(s->grid, start, py)) @@ -1598,9 +1627,10 @@ image_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, while (draw_end < span_end && !image_cell_has_text(s->grid, draw_end, py)) draw_end++; - if (start < draw_end) + if (start < draw_end) { image_draw_span(backend, tty, span, start, draw_end, px, atx, aty); + } start = draw_end; } } @@ -1619,19 +1649,16 @@ image_cell_has_alpha(struct image *im, u_int x, u_int y) pixels = im->pixels; for (yy = py; yy < py + sy; yy++) { for (xx = px; xx < px + sx; xx++) { - if (pixels[(size_t)yy * im->stride + (size_t)xx * 4 + 3] != 0) + if (pixels[(size_t)yy * im->stride + + (size_t)xx * 4 + 3] != 0) { return (1); + } } } return (0); } -/* - * Return whether every visible pixel of one cell of an old image is also - * visible in the same cell of a new image, so that drawing the new image - * over the old one leaves nothing of the old cell showing. Only works if - * both images divide their cells into the same number of pixels. - */ +/* Return whether a new cell's pixels completely cover the old cell. */ static int image_cell_covers(struct image *new, u_int nx, u_int ny, struct image *old, u_int ox, u_int oy) @@ -1640,8 +1667,9 @@ image_cell_covers(struct image *new, u_int nx, u_int ny, struct image *old, const u_char *np, *op; if (new->canvas_width / new->sx != old->canvas_width / old->sx || - new->canvas_height / new->sy != old->canvas_height / old->sy) + new->canvas_height / new->sy != old->canvas_height / old->sy) { return (0); + } image_get_pixel_rect(new, nx, ny, 1, 1, &npx, &npy, &nsx, &nsy); image_get_pixel_rect(old, ox, oy, 1, 1, &opx, &opy, &osx, &osy); @@ -1660,12 +1688,7 @@ image_cell_covers(struct image *new, u_int nx, u_int ny, struct image *old, return (1); } -/* - * Remove the cells at one column of a line that a new SIXEL image completely - * covers. SIXEL is a single bitmap: drawing over another image replaces its - * pixels for good, so there is no point keeping the old cell to be drawn - * again underneath the new one on every repaint. - */ +/* Discard cells of older SIXEL placements fully covered by a new image. */ static void image_line_cover(struct image_line *line, struct image_placement *placement, u_int x, u_int source_x, u_int source_y) @@ -1680,8 +1703,9 @@ image_line_cover(struct image_line *line, struct image_placement *placement, if (x < span->x || x >= span->x + span->sx) continue; if (!image_cell_covers(placement->image, source_x, source_y, - old->image, span->source_x + x - span->x, span->source_y)) + old->image, span->source_x + x - span->x, span->source_y)) { continue; + } image_line_remove(line, x, 1, IMAGE_INPUT_ALL, old); } } @@ -1707,12 +1731,7 @@ image_extend_row(struct image_line *line, struct image_placement *placement, } } -/* - * Extend existing image placements to reveal more of an image after a pane - * grows wider. image_write() only creates spans for as much as fit at the - * time - unlike height, there is no "scroll right" to recover a clipped - * width later, so this is the only way the rest is ever shown. - */ +/* Extend image placements to reveal clipped columns when a pane grows. */ void image_grid_resize_width(struct grid *gd, u_int new_sx) { @@ -1734,6 +1753,7 @@ image_grid_resize_width(struct grid *gd, u_int new_sx) if (line == NULL) continue; + /* Collect placements before modifying their spans. */ nseen = 0; TAILQ_FOREACH(span, &line->spans, line_entry) { found = 0; @@ -1755,6 +1775,7 @@ image_grid_resize_width(struct grid *gd, u_int new_sx) if (placement->placeholder) continue; + /* Recover the origin and last visible source column. */ cx = end_x = source_y = 0; found = 0; TAILQ_FOREACH(span, &line->spans, line_entry) { @@ -1786,7 +1807,8 @@ image_grid_resize_width(struct grid *gd, u_int new_sx) /* Place an image at the cursor using the supplied input semantics. */ static void image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg, - enum image_input input, u_int app_image_id, u_int app_placement_id, int32_t z) + enum image_input input, u_int app_image_id, u_int app_placement_id, + int32_t z) { struct screen *s = ctx->s; struct grid *gd = s->grid; @@ -1804,6 +1826,7 @@ image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg, if (sx == 0) return; + /* Scroll images that move the cursor, or clip stationary placements. */ if (im->flags & IMAGE_FLAG_NO_CURSOR) { if (sy > screen_size_y(s) - cy) sy = screen_size_y(s) - cy; @@ -1833,12 +1856,8 @@ image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg, placement = image_placement_create(gd, im, input, app_image_id, app_placement_id, z); - /* - * Give the origin_y rows scrolled into history spans too, so they - * show when scrolled back to instead of appearing blank - cap to - * gd->hsize since screen_write_scrollup() may not have created a - * history row for every one of them. - */ + + /* Attach scrolled-off image rows to the available history lines. */ hist_origin_y = origin_y; if (hist_origin_y > gd->hsize) hist_origin_y = gd->hsize; @@ -1848,6 +1867,8 @@ image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg, image_extend_row(line, placement, cx, origin_y - hist_origin_y + y, 0, sx); } + + /* Attach visible runs, discarding SIXEL cells they completely cover. */ for (y = 0; y < sy; y++) { line = image_line_get(&gd->linedata[gd->hsize + cy + y]); for (x = 0; x < sx; x += run) { @@ -1857,8 +1878,9 @@ image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg, } for (run = 1; x + run < sx; run++) { if (!image_cell_has_alpha(im, x + run, - origin_y + y)) + origin_y + y)) { break; + } } if (input == IMAGE_INPUT_SIXEL) { for (i = 0; i < run; i++) { @@ -1870,6 +1892,8 @@ image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg, origin_y + y); } } + + /* Schedule a redraw and apply the input protocol's cursor movement. */ image_store_prune(gd->images); image_redraw_area(ctx, cx, cy, sx, sy); if (!(im->flags & IMAGE_FLAG_NO_CURSOR)) { diff --git a/input.c b/input.c index 4b6d66561..7b9c77681 100644 --- a/input.c +++ b/input.c @@ -2812,15 +2812,11 @@ input_reply_kitty(struct input_ctx *ictx, if (result->quiet >= 2) return; - if (result->quiet == 1) { - if (strcmp(message, "OK") == 0) - return; - } - if (result->image_id == 0) { - if (result->image_number == 0) { - if (result->action != 'q') - return; - } + if (result->quiet == 1 && strcmp(message, "OK") == 0) + return; + if (result->image_id == 0 && result->image_number == 0 && + result->action != 'q') { + return; } if (result->image_number != 0) { if (result->image_id != 0) { @@ -2893,8 +2889,9 @@ input_exit_apc(struct input_ctx *ictx) #ifdef ENABLE_IMAGES if (ictx->input_len > 1 && ictx->input_buf[0] == 'G' && input_handle_kitty(ictx, ictx->input_buf + 1, - ictx->input_len - 1)) + ictx->input_len - 1)) { return; + } #endif if (wp != NULL && diff --git a/resize.c b/resize.c index a286264a4..e5d95b3d9 100644 --- a/resize.c +++ b/resize.c @@ -395,12 +395,14 @@ recalculate_size(struct window *w, int now) */ if (w->flags & WINDOW_RESIZE) { if (!now && changed && w->new_sx == sx && w->new_sy == sy && - w->new_xpixel == xpixel && w->new_ypixel == ypixel) + w->new_xpixel == xpixel && w->new_ypixel == ypixel) { changed = 0; + } } else { if (!now && changed && w->sx == sx && w->sy == sy && - w->xpixel == xpixel && w->ypixel == ypixel) + w->xpixel == xpixel && w->ypixel == ypixel) { changed = 0; + } } /* diff --git a/screen-redraw.c b/screen-redraw.c index f110f75bd..3191a7721 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1171,8 +1171,9 @@ redraw_damage_window_flags(struct window *w, u_int x, u_int y, u_int sx, TAILQ_FOREACH(rd, &w->damage, entry) { if (x > rd->x + rd->sx || rd->x > x + sx || - y > rd->y + rd->sy || rd->y > y + sy) + y > rd->y + rd->sy || rd->y > y + sy) { continue; + } x0 = (x < rd->x) ? x : rd->x; y0 = (y < rd->y) ? y : rd->y; @@ -1203,6 +1204,7 @@ redraw_damage_window_flags(struct window *w, u_int x, u_int y, u_int sx, redraw_collapse_damage(w); } +/* Record ordinary window damage. */ void redraw_damage_window(struct window *w, u_int x, u_int y, u_int sx, u_int sy) { @@ -1647,12 +1649,7 @@ redraw_draw_pane_lines(struct redraw_draw_ctx *dctx, struct window_pane *wp, bottom = scene->sy; #ifdef ENABLE_IMAGES - /* - * Only erase cells this pane currently owns in the scene, not its raw - * geometry - a floating pane may be occluding part of this pane's - * rectangle, and erasing under it would leave those cells blank with - * nothing to redraw them back in. - */ + /* Erase only pane-owned spans so overlapping panes remain intact. */ if (flags & REDRAW_PANE) { for (y = top; y < bottom; y++) { line = &scene->lines[y]; @@ -1671,6 +1668,7 @@ redraw_draw_pane_lines(struct redraw_draw_ctx *dctx, struct window_pane *wp, } #endif + /* Draw images behind text, then text, then images above it. */ for (phase = REDRAW_IMAGES_BEFORE; phase <= REDRAW_IMAGES_AFTER; phase++) { for (y = top; y < bottom; y++) { @@ -1682,18 +1680,20 @@ redraw_draw_pane_lines(struct redraw_draw_ctx *dctx, struct window_pane *wp, if (flags & REDRAW_PANE) { spans = &line->spans[REDRAW_SPAN_PANE]; TAILQ_FOREACH(span, spans, entry) { - if (span->data.p.wp == wp) + if (span->data.p.wp == wp) { redraw_draw_span(dctx, span, cy, phase); + } } } if (phase == REDRAW_TEXT && (flags & REDRAW_PANE_SCROLLBAR)) { spans = &line->spans[REDRAW_SPAN_SCROLLBAR]; TAILQ_FOREACH(span, spans, entry) { - if (span->data.sb.wp == wp) + if (span->data.sb.wp == wp) { redraw_draw_span(dctx, span, cy, phase); + } } } } @@ -1718,6 +1718,7 @@ redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags) enum redraw_image_phase phase; u_int y, cy, type; + /* Draw each image layer around the text spans. */ for (phase = REDRAW_IMAGES_BEFORE; phase <= REDRAW_IMAGES_AFTER; phase++) { for (y = 0; y < scene->sy; y++) { @@ -1728,8 +1729,9 @@ redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags) cy = y; for (type = 0; type < REDRAW_SPAN_TYPES; type++) { if (phase != REDRAW_TEXT && - type != REDRAW_SPAN_PANE) + type != REDRAW_SPAN_PANE) { continue; + } if (!REDRAW_IS_ALL(flags)) { switch (type) { case REDRAW_SPAN_PANE: @@ -2050,8 +2052,9 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) if ((flags & REDRAW_PANE) && (image_backend_flags(tty) & (IMAGE_BACKEND_GRAPHICAL|IMAGE_BACKEND_CLIPPED)) == - IMAGE_BACKEND_GRAPHICAL) + IMAGE_BACKEND_GRAPHICAL) { redraw_damage_window_pane_status(scene->w); + } #endif if (flags & REDRAW_PANE) { @@ -2226,14 +2229,7 @@ redraw_draw_damage_rectangle(struct redraw_draw_ctx *dctx, u_int x, u_int y, return; #ifdef ENABLE_IMAGES - /* - * Remove stale Kitty placements this redraw is about to replace, the - * same as redraw_draw_pane_lines() does for a full pane redraw - a - * placement persists until explicitly deleted, unlike a plain SIXEL - * overwrite. Every span type is included, not just panes, since a - * moved floating pane can leave a placement over cells that now - * belong to something else. - */ + /* Retire placements on all damaged spans, including uncovered cells. */ for (yy = y; yy < y + sy; yy++) { line = &scene->lines[yy]; if (dctx->flags & REDRAW_STATUS_TOP) @@ -2256,6 +2252,7 @@ redraw_draw_damage_rectangle(struct redraw_draw_ctx *dctx, u_int x, u_int y, } #endif + /* Compose image layers and text across the damaged spans. */ for (phase = REDRAW_IMAGES_BEFORE; phase <= REDRAW_IMAGES_AFTER; phase++) { for (yy = y; yy < y + sy; yy++) { @@ -2266,8 +2263,9 @@ redraw_draw_damage_rectangle(struct redraw_draw_ctx *dctx, u_int x, u_int y, cy = yy; for (type = 0; type < REDRAW_SPAN_TYPES; type++) { if (phase != REDRAW_TEXT && - type != REDRAW_SPAN_PANE) + type != REDRAW_SPAN_PANE) { continue; + } if (type == REDRAW_SPAN_STATUS) continue; spans = &line->spans[type]; @@ -2321,17 +2319,14 @@ redraw_client_damage_rect(struct client *c, struct redraw_draw_ctx *dctx, u_int x0, y0, x1, y1; #ifdef ENABLE_IMAGES - /* - * A scroll this terminal is trusted to have done itself has already - * moved everything in the region, text and images alike, so there is - * nothing to draw. - */ + /* Skip scroll damage already handled by this terminal. */ if ((rd->flags & REDRAW_DAMAGE_SCROLL) && (image_backend_flags(&c->tty) & IMAGE_BACKEND_SCROLLS) && c->tty.image_scroll_window == w && c->tty.image_scroll_epoch == w->image_scroll_epoch && - !c->tty.image_scroll_failed) + !c->tty.image_scroll_failed) { return; + } #endif x0 = (rd->x > ox) ? rd->x : ox; y0 = (rd->y > oy) ? rd->y : oy; diff --git a/screen-write.c b/screen-write.c index 1d5b42fa6..39ab397eb 100644 --- a/screen-write.c +++ b/screen-write.c @@ -403,9 +403,10 @@ screen_write_image_damage(struct screen_write_ctx *ctx, u_int x, u_int y, u_int sx, u_int sy) { image_redraw_area(ctx, x, y, sx, sy); - if (ctx->flags & SCREEN_WRITE_INPUT) + if (ctx->flags & SCREEN_WRITE_INPUT) { image_grid_remove_overwritten_spans(ctx->s->grid, x, ctx->s->grid->hsize + y, sx, sy); + } } #endif @@ -1491,9 +1492,10 @@ screen_write_alignmenttest(struct screen_write_ctx *ctx) #ifdef ENABLE_IMAGES image_redraw_all(ctx); - if (ctx->flags & SCREEN_WRITE_INPUT) + if (ctx->flags & SCREEN_WRITE_INPUT) { image_grid_remove_overwritten_spans(s->grid, 0, s->grid->hsize, screen_size_x(s), screen_size_y(s)); + } #endif for (yy = 0; yy < screen_size_y(s); yy++) { diff --git a/tmux.h b/tmux.h index 9da242578..5cdd0d081 100644 --- a/tmux.h +++ b/tmux.h @@ -1070,6 +1070,7 @@ struct style { #define TTY_ACS_IMAGE_SEXTANT_FIRST 0x92 #define TTY_ACS_IMAGE_SEXTANT_LAST 0xcd +/* Averaged RGBA colour and brightness used by the text image backend. */ struct image_sample { u_char red; u_char green; @@ -1079,20 +1080,25 @@ struct image_sample { }; #define IMAGE_SAMPLE_COLUMNS 2 #define IMAGE_SAMPLE_ROWS 6 +/* Whole-cell and subcell samples generated lazily for one image cell. */ struct image_cell { struct image_sample whole; struct image_sample samples[IMAGE_SAMPLE_ROWS][IMAGE_SAMPLE_COLUMNS]; }; +/* Immutable pixels and derived output data shared by image placements. */ struct image { u_int id; u_int references; u_int flags; + /* Retained parent (zero for sources) and original source IDs. */ u_int parent_id; u_int source_id; + /* Pixel dimensions, including the logical transparent canvas. */ u_int width; u_int height; u_int canvas_width; u_int canvas_height; + /* Grid dimensions and pixel buffer lengths in bytes. */ u_int sx; u_int sy; size_t stride; @@ -1107,18 +1113,19 @@ struct image { RB_HEAD(images, image); #define IMAGE_SIZE_LIMIT (64 * 1024 * 1024) +/* Source crop and padded destination canvas for creating an image view. */ struct image_view { - u_int x; + u_int x; /* Crop origin in source pixels. */ u_int y; - u_int width; + u_int width; /* Crop size in source pixels. */ u_int height; - u_int scaled_width; + u_int scaled_width; /* Resampled crop size in pixels. */ u_int scaled_height; - u_int canvas_width; + u_int canvas_width; /* Padded canvas size in pixels. */ u_int canvas_height; - u_int sx; + u_int sx; /* Placement size in cells. */ u_int sy; - u_int x_offset; + u_int x_offset; /* Crop origin in canvas pixels. */ u_int y_offset; }; @@ -1130,13 +1137,14 @@ enum kitty_parse_status { KITTY_PARSE_UNSUPPORTED }; +/* Reply fields and placement actions returned by one Kitty command. */ struct kitty_parse_result { u_int image_id; u_int image_number; - u_int replace_id; + u_int replace_id; /* Old server ID, or zero. */ u_int placement_id; u_int quiet; - u_int x; + u_int x; /* Deletion coordinates. */ u_int y; int32_t z; char action; @@ -1144,9 +1152,10 @@ struct kitty_parse_result { enum kitty_parse_status status; }; +/* Resolved source cell and application IDs of one Unicode placeholder. */ struct kitty_placeholder { struct image *image; - u_int source_x; + u_int source_x; /* Origin in image cells. */ u_int source_y; u_int image_id; u_int placement_id; @@ -4416,7 +4425,7 @@ void image_grid_copy_area(struct grid *, u_int, u_int, struct grid *, u_int, u_int, u_int, u_int); void image_grid_resize_width(struct grid *, u_int); int image_grid_line_has_images(const struct grid_line *); - int image_grid_area_has_images(struct grid *, u_int, u_int, u_int, +int image_grid_area_has_images(struct grid *, u_int, u_int, u_int, u_int); int image_grid_get_placeholder(struct grid *, u_int, u_int, struct kitty_placeholder *); diff --git a/tty-acs.c b/tty-acs.c index 71455c866..9c0470fc5 100644 --- a/tty-acs.c +++ b/tty-acs.c @@ -94,6 +94,7 @@ static const struct tty_acs_entry tty_acs_table[] = { #ifdef ENABLE_IMAGES static char tty_acs_image_sextants[60][5]; +/* Encode the Unicode sextant characters used by the image fallback. */ static void tty_acs_image_sextants_init(void) { @@ -110,6 +111,7 @@ tty_acs_image_sextants_init(void) } } +/* Return the sextant ACS key for a six-bit foreground mask. */ u_char tty_acs_image_sextant(u_int mask) { diff --git a/tty-draw.c b/tty-draw.c index 00f1d3760..696c5bc2e 100644 --- a/tty-draw.c +++ b/tty-draw.c @@ -272,18 +272,18 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, /* Get the current cell. */ grid_view_get_cell(gd, px + i, py, &gc); #ifdef ENABLE_IMAGES - if (kitty_cell_is_placeholder(&gc)) { - if (image_grid_get_placeholder(gd, - px + i, gd->hsize + py, - &placeholder)) - utf8_set(&gc.data, ' '); + if (kitty_cell_is_placeholder(&gc) && + image_grid_get_placeholder(gd, px + i, + gd->hsize + py, &placeholder)) { + utf8_set(&gc.data, ' '); } #endif gcp = &gc; #ifdef ENABLE_IMAGES if (image_get_fallback_at(tty, s, px + i, py, &gc, - &image_gc) == 1) + &image_gc) == 1) { gcp = &image_gc; + } #endif /* Work out empty cells. */ diff --git a/tty.c b/tty.c index a4ac8f0f4..06ba2203f 100644 --- a/tty.c +++ b/tty.c @@ -158,9 +158,10 @@ void tty_set_size(struct tty *tty, u_int sx, u_int sy, u_int xpixel, u_int ypixel) { #ifdef ENABLE_IMAGES - int geometry_changed; + int geometry_changed = 0; - geometry_changed = (tty->xpixel != xpixel || tty->ypixel != ypixel); + if (tty->xpixel != xpixel || tty->ypixel != ypixel) + geometry_changed = 1; #endif tty->sx = sx; tty->sy = sy; @@ -1940,14 +1941,12 @@ tty_cmd_cell(struct tty *tty, const struct tty_ctx *ctx) struct grid_cell placeholder_cell; struct kitty_placeholder placeholder; - if (kitty_cell_is_placeholder(cell)) { - if (image_grid_get_placeholder(ctx->s->grid, ctx->ocx, - ctx->s->grid->hsize + ctx->ocy, &placeholder)) { - memcpy(&placeholder_cell, cell, - sizeof placeholder_cell); - utf8_set(&placeholder_cell.data, ' '); - cell = &placeholder_cell; - } + if (kitty_cell_is_placeholder(cell) && + image_grid_get_placeholder(ctx->s->grid, ctx->ocx, + ctx->s->grid->hsize + ctx->ocy, &placeholder)) { + memcpy(&placeholder_cell, cell, sizeof placeholder_cell); + utf8_set(&placeholder_cell.data, ' '); + cell = &placeholder_cell; } #endif diff --git a/window-copy.c b/window-copy.c index d82bf88b4..82d29b4af 100644 --- a/window-copy.c +++ b/window-copy.c @@ -298,7 +298,7 @@ struct window_copy_mode_data { u_int oy; /* number of lines scrolled up */ u_int image_base; /* hsize - oy images were last drawn for */ - int image_base_set; + int image_base_set; /* image_base is valid */ int image_refresh; /* current redraw needs image refresh */ u_int selx; /* beginning of selection */ @@ -5591,13 +5591,7 @@ window_copy_write_one(struct window_mode_entry *wme, grid_get_cell(gd, fx, fy, &gc); if (fx + gc.data.width <= nx) { #ifdef ENABLE_IMAGES - /* - * Write image-covered cells directly into the grid, - * skipping window_copy_update_style() (a highlight - * must not sweep over the image) and - * screen_write_cell() (its image-damage call would - * re-damage the image on every redraw for nothing). - */ + /* Write image cells without styling or damage. */ if (image_grid_area_has_images(gd, fx, fy, gc.data.width, 1)) { grid_view_set_cell(ctx->s->grid, px + fx, py, @@ -5856,13 +5850,7 @@ window_copy_write_line(struct window_mode_entry *wme, content_sx, &mgc, &cgc, &mkgc, &clgc); #ifdef ENABLE_IMAGES - /* - * Copy the backing line's image layers separately: the text write - * above knows nothing about image content. Only redraw them when the - * view has actually moved (data->image_refresh) - otherwise they are - * already correct and redrawing would just flash them on every - * unrelated redraw. - */ + /* Copy image spans; refresh only when the history view moves. */ image_grid_free_line(s->grid, &s->grid->linedata[s->grid->hsize + py]); image_grid_copy_area(s->grid, width, s->grid->hsize + py, @@ -5922,31 +5910,21 @@ window_copy_redraw_selection(struct window_mode_entry *wme, u_int old_y) } #ifdef ENABLE_IMAGES -/* - * Only rows whose underlying history position has moved since the last - * call need their images recomposited, to avoid flashing them on every - * unrelated redraw. Every caller of window_copy_write_line()/ - * window_copy_write_lines() must call this first - it is not implied by - * them, since some write directly rather than via - * window_copy_redraw_lines(). - */ +/* Refresh images only when the visible history range has moved. */ static void window_copy_update_image_refresh(struct window_copy_mode_data *data) { u_int base; base = screen_hsize(data->backing) - data->oy; - data->image_refresh = !data->image_base_set || base != data->image_base; + data->image_refresh = 0; + if (!data->image_base_set || base != data->image_base) + data->image_refresh = 1; data->image_base = base; data->image_base_set = 1; } -/* - * Whether any part of the currently visible backing range carries image - * data. A scrolled insert/delete-line fast path only shifts character - * cells, leaving image content stale, so callers should fall back to a - * full window_copy_redraw_screen() when this returns true. - */ +/* Return whether the visible backing range contains image spans. */ static int window_copy_visible_has_images(struct window_copy_mode_data *data) {